Эквивалент вычисляемых свойств с использованием @Published в Swift Combine?

20

В императивном Swift обычно используют вычисляемые свойства, чтобы обеспечить удобный доступ к данным без дублирования состояния.

Допустим, у меня есть этот класс для императивного использования 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
    }

    // ...
}

Если я хочу создать реактивный эквивалент в Combine, например, для использования с SwiftUI, я могу легко добавить @Publishedсохраненные свойства для генерации Publishers, но не для вычисляемых свойств.

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

Есть различные обходные пути, о которых я мог подумать. Вместо этого я могу сохранить вычисленное свойство и обновлять его.

Вариант 1. Использование наблюдателя свойства:

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

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

    // ...
}

Вариант 2: Использование Subscriberв моем собственном классе:

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

    // ...
}

Однако эти обходные пути не так элегантны, как вычисляемые свойства. Они дублируют состояние и не обновляют оба свойства одновременно.

Что будет правильным эквивалентом добавления Publisherк вычисляемому свойству в Combine?

rberggreen
источник
1
Вычисленные свойства - это вид свойств, которые являются производными свойствами. Их значения зависят от значений зависимых. Только по этой причине можно сказать, что они никогда не должны вести себя как ObservableObject. Вы по своей сути считать , что ObservableObjectобъект должен иметь возможность иметь мутирует способность , которая, по определению, не является случаем для компьютерной собственности .
Наем
Вы нашли решение для этого? Я нахожусь в точно такой же ситуации, я хочу избежать государства и все еще иметь возможность публиковать
erotsppa

Ответы:

2

Как насчет использования downstream?

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

Таким образом, подписка получит элемент из апстрима, затем вы можете использовать sinkили assignсделать didSetидею.

ytyubox
источник
2

Создайте нового издателя, подписавшегося на свойство, которое вы хотите отслеживать.

@Published var speed: Double = 88

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

Затем вы сможете наблюдать это так же, как ваша @Publishedсобственность.

private var subscriptions = Set<AnyCancellable>()


override func viewDidLoad() {
    super.viewDidLoad()

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

Прямо не связано, но полезно, тем не менее, вы можете отслеживать несколько свойств таким образом 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
источник
0

Вы должны объявить объект PassthroughSubject в вашем ObservableObject:

class ReactiveUserManager1: ObservableObject {

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

    [...]
}

И в didSet (willSet может быть лучше) вашего @Published var вы будете использовать метод 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()
    }

    [...]
}

Вы можете проверить это в WWDC Data Flow Talk.

Никола Лауритано
источник
Вы должны импортировать комбинат
Никола Lauritano
Чем это отличается от варианта 1 в самом вопросе?
Найм
В
варианте
Ну, это не то, что я спросил на самом деле. @PublishedОбертка и PassthroughSubjectоба служат той же цели в этом контексте. Обратите внимание на то, что вы написали и чего на самом деле хотел достичь ОП. Является ли ваше решение лучшей альтернативой варианту 1 ?
Наем
0

scan ( : :) Преобразует элементы из вышестоящего издателя, предоставляя текущий элемент замыканию вместе с последним значением, возвращаемым замыканием.

Вы можете использовать scan (), чтобы получить последнее и текущее значение. Пример:

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

}

Выше код эквивалентен этому: (меньше объединить)

  @Published var loading: Bool = false {
            didSet {
                if oldValue == false, loading == true {
                    NotificationCenter.default.post(name: NSNotification.Name("userStateDidChange"), object: nil)
                }
            }
        }
здравко здравкин
источник