web-dev-qa-db-fra.com

Un équivalent aux propriétés calculées utilisant @Published dans Swift Combine?

Dans Swift impératif, il est courant d'utiliser des propriétés calculées pour fournir un accès pratique aux données sans dupliquer l'état.

Disons que j'ai cette classe conçue pour une utilisation impérative de 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
    }

    // ...
}

Si je veux créer un équivalent réactif avec Combine, par ex. pour une utilisation avec SwiftUI, je peux facilement ajouter @Published aux propriétés stockées pour générer Publishers, mais pas pour les propriétés calculées.

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

Il existe différentes solutions de contournement auxquelles je pourrais penser. Je pourrais plutôt stocker ma propriété calculée et la maintenir à jour.

Option 1: Utilisation d'un observateur de propriétés:

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

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

    // ...
}

Option 2: utiliser un Subscriber dans ma propre 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)
    }

    // ...
}

Cependant, ces solutions de contournement ne sont pas aussi élégantes que les propriétés calculées. Ils dupliquent l'état et ne mettent pas à jour les deux propriétés simultanément.

Quel serait un équivalent approprié à l'ajout d'un Publisher à une propriété calculée dans Combine?

19
rberggreen

Que diriez-vous d'utiliser en aval?

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

De cette façon, l'abonnement obtiendra l'élément de l'amont, alors vous pouvez utiliser sink ou assign pour faire l'idée didSet.

1
ytyubox

Vous devez déclarer un PassthroughSubject dans votre ObservableObject:

class ReactiveUserManager1: ObservableObject {

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

    [...]
}

Et dans le didSet (willSet pourrait être meilleur) de votre @ Var publiée , vous utiliserez une méthode appelée 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()
    }

    [...]
}

Vous pouvez le vérifier dans WWDC Data Flow Talk

0
Nicola Lauritano

scan (: :) Transforme les éléments de l'éditeur en amont en fournissant l'élément actuel à une fermeture avec la dernière valeur renvoyée par la fermeture.

Vous pouvez utiliser scan () pour obtenir la valeur la plus récente et actuelle. Exemple:

@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)

}

Le code ci-dessus est équivalent à ceci: (moins Combiner)

  @Published var loading: Bool = false {
            didSet {
                if oldValue == false, loading == true {
                    NotificationCenter.default.post(name: NSNotification.Name("userStateDidChange"), object: nil)
                }
            }
        }
0
zdravko zdravkin