Um equivalente às propriedades calculadas usando @Published no Swift Combine?

20

No Swift imperativo, é comum usar propriedades computadas para fornecer acesso conveniente aos dados sem duplicar o estado.

Digamos que eu tenha essa classe feita para uso obrigatório do 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 eu quiser criar um equivalente reativo ao Combine, por exemplo, para uso com o SwiftUI, posso adicionar facilmente @Publishedàs propriedades armazenadas para gerar Publishers, mas não para propriedades calculadas.

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

Existem várias soluções alternativas em que pude pensar. Eu poderia tornar minha propriedade computada armazenada e mantê-la atualizada.

Opção 1: Usando um observador de propriedades:

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

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

    // ...
}

Opção 2: usando a Subscriberna minha própria 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)
    }

    // ...
}

No entanto, essas soluções alternativas não são tão elegantes quanto as propriedades calculadas. Eles duplicam o estado e não atualizam as duas propriedades simultaneamente.

O que seria um equivalente adequado para adicionar a Publishera uma propriedade computada em Combine?

rberggreen
fonte
11
Propriedades computadas são o tipo de propriedades derivadas. Seus valores dependem dos valores do dependente. Por esse motivo, pode-se dizer que eles nunca devem agir como um ObservableObject. Você supõe inerentemente que um ObservableObjectobjeto possa ter capacidade de mutação que, por definição, não é o caso da Propriedade Computada .
Nayem 03/10/19
Você encontrou uma solução para isso? Estou exatamente na mesma situação, quero evitar o estado e ainda ser capaz de publicar
erotsppa

Respostas:

2

Que tal usar a jusante?

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

Dessa forma, a assinatura obterá um elemento do upstream, e você poderá usar sinkou assignfazer a didSetideia.

ytyubox
fonte
2

Crie um novo editor inscrito na propriedade que você deseja acompanhar.

@Published var speed: Double = 88

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

Você poderá observá-lo como sua @Publishedpropriedade.

private var subscriptions = Set<AnyCancellable>()


override func viewDidLoad() {
    super.viewDidLoad()

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

Não diretamente relacionado, mas útil, você pode acompanhar várias propriedades dessa maneira 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
fonte
0

Você deve declarar um PassthroughSubject no seu ObservableObject:

class ReactiveUserManager1: ObservableObject {

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

    [...]
}

E no didSet (willSet poderia ser melhor) do seu var @Published, você usará um método chamado 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()
    }

    [...]
}

Você pode verificá-lo no WWDC Data Flow Talk

Nicola Lauritano
fonte
Você deve importar Combine
Nicola Lauritano 3/19/19
Como isso é diferente da opção 1 na própria pergunta?
Nayem 03/10/19
Não há nenhum PassthroughSubject na opção1
Nicola Lauritano 3/19/19
Bem, não foi isso que eu pedi, na verdade. @Publishedwrapper e PassthroughSubjectambos servem ao mesmo propósito neste contexto. Preste atenção no que você escreveu e no que o OP realmente queria alcançar. Sua solução serve como a melhor alternativa que a opção 1 realmente?
Nayem 03/10/19
0

scan ( : :) Transforma elementos do editor upstream, fornecendo o elemento atual a um fechamento junto com o último valor retornado pelo fechamento.

Você pode usar scan () para obter o valor mais recente e atual. Exemplo:

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

}

O código acima é equivalente a isso: (menos Combinar)

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