¿Un equivalente a las propiedades calculadas usando @Published en Swift Combine?

20

En el imperativo Swift, es común usar propiedades calculadas para proporcionar un acceso conveniente a los datos sin duplicar el estado.

Digamos que tengo esta clase hecha para uso MVC imperativo:

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 quiero crear un equivalente reactivo con Combine, por ejemplo, para usar con SwiftUI, puedo agregar fácilmente @Publisheda las propiedades almacenadas para generar Publishers, pero no para las propiedades calculadas.

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

Hay varias soluciones que se me ocurren. Podría hacer que mi propiedad calculada se almacene en su lugar y mantenerla actualizada.

Opción 1: Usar un observador de propiedades:

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

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

    // ...
}

Opción 2: Usar a Subscriberen mi propia clase:

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

    // ...
}

Sin embargo, estas soluciones alternativas no son tan elegantes como las propiedades calculadas. Duplican el estado y no actualizan ambas propiedades simultáneamente.

¿Cuál sería un equivalente apropiado para agregar Publishera una propiedad calculada en Combine?

Rberggreen
fuente
1
Las propiedades calculadas son el tipo de propiedades que son propiedades derivadas. Sus valores dependen de los valores del dependiente. Solo por esta razón, se puede decir que nunca deben actuar como un ObservableObject. Inherentemente asume que un ObservableObjectobjeto debería tener una capacidad de mutación que, por definición, no es el caso de la propiedad calculada .
nayem
¿Ha encontrado una solución para esto? Estoy exactamente en la misma situación, quiero evitar el estado y aún poder publicar
erotsppa

Respuestas:

2

¿Qué tal el uso aguas abajo?

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

De esta manera, la suscripción obtendrá un elemento de upstream, luego puede usar sinko assignhacer la didSetidea.

ytyubox
fuente
2

Cree un nuevo editor suscrito a la propiedad que desea rastrear.

@Published var speed: Double = 88

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

Entonces podrá observarlo como si fuera su @Publishedpropiedad.

private var subscriptions = Set<AnyCancellable>()


override func viewDidLoad() {
    super.viewDidLoad()

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

No está directamente relacionado pero es útil, sin embargo, puede rastrear múltiples propiedades de esa manera 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
fuente
0

Debes declarar un PassthroughSubject en tu ObservableObject:

class ReactiveUserManager1: ObservableObject {

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

    [...]
}

Y en el didSet (willSet podría ser mejor) de su var @Published usará un método llamado 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()
    }

    [...]
}

Puede verificarlo en la charla de flujo de datos WWDC

Nicola Lauritano
fuente
Debe importar Combine
Nicola Lauritano
¿Cómo es esto diferente de la opción 1 en la pregunta en sí?
nayem
No hay ningún PassthroughSubject en la opción1
Nicola Lauritano
Bueno, eso no fue lo que pregunté en realidad. @Publishedwrapper y PassthroughSubjectambos sirven el mismo propósito en este contexto. Presta atención a lo que escribiste y a lo que el OP realmente quería lograr. ¿Su solución sirve como la mejor alternativa que la opción 1 en realidad?
nayem
0

scan ( : :) Transforma elementos del publicador ascendente al proporcionar el elemento actual a un cierre junto con el último valor devuelto por el cierre.

Puede usar scan () para obtener el valor más reciente y actual. Ejemplo:

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

}

El código anterior es equivalente a esto: (menos Combinar)

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