Ein Äquivalent zu berechneten Eigenschaften mit @Published in Swift Combine?


20

Im zwingenden Swift ist es üblich, berechnete Eigenschaften zu verwenden, um einen bequemen Zugriff auf Daten zu ermöglichen, ohne den Status zu duplizieren.

Angenommen, ich habe diese Klasse für die zwingende Verwendung von MVC erstellt:

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
    }

    // ...
}

Wenn ich mit Combine ein reaktives Äquivalent erstellen möchte, z. B. zur Verwendung mit SwiftUI, kann ich @Publishedgespeicherte Eigenschaften problemlos hinzufügen , um Publishers zu generieren , jedoch nicht für berechnete Eigenschaften.

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

Es gibt verschiedene Problemumgehungen, an die ich denken könnte. Ich könnte stattdessen meine berechnete Eigenschaft speichern und auf dem neuesten Stand halten.

Option 1: Verwenden eines Eigenschaftsbeobachters:

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

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

    // ...
}

Option 2: Verwenden von a Subscriberin meiner eigenen Klasse:

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

    // ...
}

Diese Problemumgehungen sind jedoch nicht so elegant wie berechnete Eigenschaften. Sie duplizieren den Status und aktualisieren nicht beide Eigenschaften gleichzeitig.

Was wäre ein angemessenes Äquivalent zum Hinzufügen von a Publisherzu einer berechneten Eigenschaft in Combine?



1
Berechnete Eigenschaften sind die Art von Eigenschaften, die abgeleitete Eigenschaften sind. Ihre Werte hängen von den Werten der Abhängigen ab. Allein aus diesem Grund kann man sagen, dass sie niemals dazu gedacht sind, sich wie ein Mensch zu verhalten ObservableObject. Sie gehen von Natur aus davon aus, dass ein ObservableObjectObjekt mutieren kann, was per Definition für die berechnete Eigenschaft nicht der Fall ist .
Nayem

Haben Sie eine Lösung dafür gefunden? Ich bin in genau der gleichen Situation, ich möchte Staat vermeiden und trotzdem veröffentlichen können
erotsppa

Antworten:


2

Wie wäre es mit Downstream?

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

Auf diese Weise erhält das Abonnement ein Element aus dem Upstream, dann können Sie die Idee verwenden sinkoder ausführen.assigndidSet


2

Erstellen Sie einen neuen Herausgeber, der die Eigenschaft abonniert hat, die Sie verfolgen möchten.

@Published var speed: Double = 88

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

Sie können es dann ähnlich wie Ihr @PublishedEigentum beobachten.

private var subscriptions = Set<AnyCancellable>()


override func viewDidLoad() {
    super.viewDidLoad()

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

Nicht direkt verwandt, aber dennoch nützlich, können Sie auf diese Weise mehrere Eigenschaften verfolgen 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

Sie sollten ein PassthroughSubject in Ihrem ObservableObject deklarieren :

class ReactiveUserManager1: ObservableObject {

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

    [...]
}

Und im didSet (willSet könnte besser sein) Ihrer @Published var verwenden Sie eine Methode namens 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()
    }

    [...]
}

Sie können dies im WWDC Data Flow Talk überprüfen


Sie sollten Combine
Nicola Lauritano

Wie unterscheidet sich dies von der Option 1 in der Frage selbst?
Nayem

Es gibt kein PassthroughSubject in der Option1
Nicola Lauritano

Nun, das habe ich eigentlich nicht gefragt. @PublishedWrapper und PassthroughSubjectbeide dienen in diesem Zusammenhang demselben Zweck. Achten Sie darauf, was Sie geschrieben haben und was das OP eigentlich erreichen wollte. Dient Ihre Lösung als bessere Alternative als die Option 1 ?
Nayem

0

scan ( : :) Transformiert Elemente aus dem Upstream-Publisher, indem das aktuelle Element zusammen mit dem letzten vom Abschluss zurückgegebenen Wert für einen Abschluss bereitgestellt wird.

Sie können scan () verwenden, um den neuesten und aktuellen Wert abzurufen. Beispiel:

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

}

Der obige Code entspricht dem: (weniger Kombinieren)

  @Published var loading: Bool = false {
            didSet {
                if oldValue == false, loading == true {
                    NotificationCenter.default.post(name: NSNotification.Name("userStateDidChange"), object: nil)
                }
            }
        }
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.