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


20

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 exemple pour une utilisation avec SwiftUI, je peux facilement ajouter @Publishedaux propriétés stockées pour générer des 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 mettre à 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 Subscriberdans 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 l'équivalent approprié d'ajouter un Publisherà une propriété calculée dans Combine?



1
Les propriétés calculées sont le type de propriétés qui sont des propriétés dérivées. Leurs valeurs dépendent des valeurs de la personne à charge. Pour cette seule raison, on peut dire qu'ils ne sont jamais destinés à agir comme un ObservableObject. Vous supposez intrinsèquement qu'un ObservableObjectobjet devrait pouvoir avoir une capacité de mutation qui, par définition, n'est pas le cas pour la propriété calculée .
nayem

Avez-vous trouvé une solution à cela? Je suis exactement dans la même situation, je veux éviter l'état et toujours pouvoir publier
erotsppa

Réponses:


2

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 en amont, alors vous pouvez utiliser sinkou assignpour faire l' didSetidée.


2

Créez un nouvel éditeur abonné à la propriété que vous souhaitez suivre.

@Published var speed: Double = 88

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

Vous pourrez alors l'observer un peu comme votre @Publishedpropriété.

private var subscriptions = Set<AnyCancellable>()


override func viewDidLoad() {
    super.viewDidLoad()

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

Pas directement lié, mais néanmoins utile, vous pouvez suivre plusieurs propriétés de cette façon avec 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

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 @Published, 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 la discussion sur le flux de données WWDC


Vous devez importer Combine
Nicola Lauritano

En quoi est-ce différent de l' option 1 de la question elle-même?
nayem

Il n'y a pas de PassthroughSubject dans l'
option1

Eh bien, ce n'est pas ce que j'ai demandé. @Publishedwrapper et les PassthroughSubjectdeux servent le même objectif dans ce contexte. Faites attention à ce que vous avez écrit et à ce que le PO voulait réellement réaliser. Votre solution est-elle la meilleure alternative à l' option 1 ?
nayem

0

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)
                }
            }
        }
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.