Odpowiednik obliczonych właściwości za pomocą @Published w Swift Combine?

20

W trybie imperatywnym Szybki często stosuje się właściwości obliczeniowe, aby zapewnić wygodny dostęp do danych bez powielania stanu.

Powiedzmy, że mam tę klasę do bezwzględnego użycia 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
    }

    // ...
}

Jeśli chcę utworzyć reaktywny ekwiwalent za pomocą Łączenia, np. Do użytku ze SwiftUI, mogę łatwo dodać @Publisheddo przechowywanych właściwości w celu wygenerowania Publishers, ale nie dla właściwości obliczonych.

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

Istnieją różne obejścia, o których mógłbym pomyśleć. Mógłbym zamiast tego zapisać moją właściwość obliczoną i aktualizować ją.

Opcja 1: Korzystanie z obserwatora właściwości:

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

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

    // ...
}

Opcja 2: Korzystanie z Subscriberwłasnej klasy:

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

    // ...
}

Jednak te obejścia nie są tak eleganckie, jak obliczone właściwości. Powielają stan i nie aktualizują obu właściwości jednocześnie.

Jaki byłby odpowiedni odpowiednik dodania Publisherdo obliczonej właściwości w Combine?

rberggreen
źródło
1
Właściwości obliczane są rodzajem właściwości, które są pochodnymi właściwości. Ich wartości zależą od wartości osoby zależnej. Tylko z tego powodu można powiedzieć, że nigdy nie mają zachowywać się jak ObservableObject. Zakładasz z natury, że ObservableObjectobiekt powinien mieć zdolność mutowania, co z definicji nie ma miejsca w przypadku właściwości obliczonej .
nayem
Czy znalazłeś rozwiązanie tego problemu? Jestem w dokładnie takiej samej sytuacji, chcę uniknąć stanu i nadal móc publikować
erotsppa

Odpowiedzi:

2

Co powiesz na użycie downstream?

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

W ten sposób subskrypcja otrzyma element z góry, a następnie możesz użyć sinklub assignzrobić didSetpomysł.

ytyubox
źródło
2

Utwórz nowego wydawcę subskrybującego usługę, którą chcesz śledzić.

@Published var speed: Double = 88

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

Będziesz wtedy mógł obserwować go podobnie jak swoją @Publishedwłasność.

private var subscriptions = Set<AnyCancellable>()


override func viewDidLoad() {
    super.viewDidLoad()

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

Nie są bezpośrednio powiązane, ale są przydatne, można w ten sposób śledzić wiele właściwości 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()
}()
Pomme2Poule
źródło
0

Powinieneś zadeklarować PassthroughSubject w swoim ObservableObject:

class ReactiveUserManager1: ObservableObject {

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

    [...]
}

A w didSet (willSet może być lepiej) w twoim @Published var będziesz używał metody o nazwie 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()
    }

    [...]
}

Możesz to sprawdzić w rozmowie o przepływie danych WWDC

Nicola Lauritano
źródło
Powinieneś zaimportować Kombajn
Nicola Lauritano
Czym różni się to od opcji 1 w samym pytaniu?
nayem,
W opcji 1 nie ma żadnego PassthroughSubject
Nicola Lauritano
Właściwie to nie o to prosiłem. @Publishedopakowanie i PassthroughSubjectoba służą w tym samym celu. Zwróć uwagę na to, co napisałeś i co tak naprawdę chciał osiągnąć PO. Czy twoje rozwiązanie stanowi lepszą alternatywę niż opcja 1 ?
nayem
0

scan ( : :) Przekształca elementy z wcześniejszego wydawcy, dostarczając bieżący element do zamknięcia wraz z ostatnią wartością zwróconą przez zamknięcie.

Możesz użyć scan (), aby uzyskać najnowszą i aktualną wartość. Przykład:

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

}

Powyższy kod jest równoważny z tym: (mniej Połącz)

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