Un equivalente alle proprietà calcolate usando @Published in Swift Combine?


20

In imperativo Swift, è comune utilizzare proprietà calcolate per fornire un comodo accesso ai dati senza duplicare lo stato.

Diciamo che ho creato questa classe per l'uso imperativo di MVC:

class ImperativeUserManager {
    private(set) var currentUser: User? {
        didSet {
            if oldValue != currentUser {
                NotificationCenter.default.post(name: NSNotification.Name("userStateDidChange"), object: nil)
                // Observers that receive this notification might then check either currentUser or userIsLoggedIn for the latest state
            }
        }
    }

    var userIsLoggedIn: Bool {
        currentUser != nil
    }

    // ...
}

Se voglio creare un equivalente reattivo con Combine, ad esempio per l'uso con SwiftUI, posso facilmente aggiungere @Publishedproprietà memorizzate per generare messaggi Publisher, ma non per proprietà calcolate.

    @Published var userIsLoggedIn: Bool { // Error: Property wrapper cannot be applied to a computed property
        currentUser != nil
    }

Ci sono varie soluzioni alternative che mi vengono in mente. Potrei invece archiviare la mia proprietà calcolata e mantenerla aggiornata.

Opzione 1: utilizzo di un osservatore proprietà:

class ReactiveUserManager1: ObservableObject {
    @Published private(set) var currentUser: User? {
        didSet {
            userIsLoggedIn = currentUser != nil
        }
    }

    @Published private(set) var userIsLoggedIn: Bool = false

    // ...
}

Opzione 2: utilizzo di a Subscribernella mia classe:

class ReactiveUserManager2: ObservableObject {
    @Published private(set) var currentUser: User?
    @Published private(set) var userIsLoggedIn: Bool = false

    private var subscribers = Set<AnyCancellable>()

    init() {
        $currentUser
            .map { $0 != nil }
            .assign(to: \.userIsLoggedIn, on: self)
            .store(in: &subscribers)
    }

    // ...
}

Tuttavia, queste soluzioni alternative non sono eleganti come le proprietà calcolate. Duplicano lo stato e non aggiornano entrambe le proprietà contemporaneamente.

Quale sarebbe un vero e proprio equivalente all'aggiunta di una Publishera una proprietà calcolata in Combina?



1
Le proprietà calcolate sono il tipo di proprietà che sono proprietà derivate. I loro valori dipendono dai valori del dipendente. Solo per questo motivo, si può dire che non sono mai pensati per comportarsi come un ObservableObject. Supponi intrinsecamente che un ObservableObjectoggetto dovrebbe essere in grado di avere un'abilità mutante che, per definizione, non è il caso della proprietà calcolata .
nayem,

Hai trovato una soluzione a questo? Sono nella stessa identica situazione, voglio evitare lo stato ed essere ancora in grado di pubblicare
erotsppa il

Risposte:


2

Che ne dici di usare downstream?

lazy var userIsLoggedInPublisher: AnyPublisher = $currentUser
                                          .map{$0 != nil}
                                          .eraseToAnyPublisher()

In questo modo, l'abbonamento otterrà l'elemento dall'upstream, quindi è possibile utilizzare sinko assignfare l' didSetidea.


2

Crea un nuovo editore iscritto alla proprietà che desideri monitorare.

@Published var speed: Double = 88

lazy var canTimeTravel: AnyPublisher<Bool,Never> = {
    $speed
        .map({ $0 >= 88 })
        .eraseToAnyPublisher()
}()

Sarai quindi in grado di osservarlo come la tua @Publishedproprietà.

private var subscriptions = Set<AnyCancellable>()


override func viewDidLoad() {
    super.viewDidLoad()

    sourceOfTruthObject.$canTimeTravel.sink { [weak self] (canTimeTravel) in
        // Do something…
    })
    .store(in: &subscriptions)
}

Non direttamente correlato ma utile, tuttavia, è possibile tenere traccia di più proprietà in questo modo combineLatest.

@Published var threshold: Int = 60

@Published var heartData = [Int]()

/** This publisher "observes" both `threshold` and `heartData`
 and derives a value from them.
 It should be updated whenever one of those values changes. */
lazy var status: AnyPublisher<Status,Never> = {
    $threshold
       .combineLatest($heartData)
       .map({ threshold, heartData in
           // Computing a "status" with the two values
           Status.status(heartData: heartData, threshold: threshold)
       })
       .receive(on: DispatchQueue.main)
       .eraseToAnyPublisher()
}()

0

Devi dichiarare un oggetto Passthrough nel tuo oggetto Observable:

class ReactiveUserManager1: ObservableObject {

    //The PassthroughSubject provides a convenient way to adapt existing imperative code to the Combine model.
    var objectWillChange = PassthroughSubject<Void,Never>()

    [...]
}

E nel didSet (willSet potrebbe essere migliore) del tuo var @Published userai un metodo chiamato send ()

class ReactiveUserManager1: ObservableObject {

    //The PassthroughSubject provides a convenient way to adapt existing imperative code to the Combine model.
    var objectWillChange = PassthroughSubject<Void,Never>()

    @Published private(set) var currentUser: User? {
    willSet {
        userIsLoggedIn = currentUser != nil
        objectWillChange.send()
    }

    [...]
}

Puoi verificarlo nel WWDC Data Flow Talk


Dovresti importare Combine
Nicola Lauritano il

In che modo differisce dall'opzione 1 nella domanda stessa?
nayem,

Non c'è alcun PassthroughSubject nell'opzione 1
Nicola Lauritano,

Bene, non era quello che ho chiesto in realtà. @Publishedwrapper ed PassthroughSubjectentrambi hanno lo stesso scopo in questo contesto. Presta attenzione a ciò che hai scritto e a ciò che l'OP ha effettivamente voluto ottenere. La tua soluzione è la migliore alternativa all'opzione 1 ?
nayem,

0

scan ( : :) Trasforma gli elementi dall'editore a monte fornendo l'elemento corrente a una chiusura insieme all'ultimo valore restituito dalla chiusura.

È possibile utilizzare scan () per ottenere il valore più recente e attuale. Esempio:

@Published var loading: Bool = false

init() {
// subscriber connection

 $loading
        .scan(false) { latest, current in
                if latest == false, current == true {
                    NotificationCenter.default.post(name: NSNotification.Name("userStateDidChange"), object: nil) 
        }
                return current
        }
         .sink(receiveValue: { _ in })
         .store(in: &subscriptions)

}

Il codice sopra è equivalente a questo: (meno Combina)

  @Published var loading: Bool = false {
            didSet {
                if oldValue == false, loading == true {
                    NotificationCenter.default.post(name: NSNotification.Name("userStateDidChange"), object: nil)
                }
            }
        }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.