Еквівалент обчислюваним властивостям з використанням @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до обчислюваної властивості в "Комбінат"?



1
Обчислені властивості - вид властивостей, які є похідними властивостями. Їх значення залежать від значень залежного. Тільки з цієї причини можна сказати, що вони ніколи не мають на меті поводитись як подібні ObservableObject. Ви за своєю суттю припускаєте, що ObservableObjectоб’єкт повинен мати здатність мутувати, що, за визначенням, не має відношення до обчислюваної властивості .
Найєм

Ви знайшли рішення для цього? Я в точно такій же ситуації, я хочу уникати стану і все ще можу публікувати
erotsppa

Відповіді:


2

Як щодо використання вниз за течією?

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

Таким чином, підписка отримає елемент з висхідного потоку, тоді ви можете використовувати sinkабо assignробити didSetідею.


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

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 може бути кращим) вашого @Pubished 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


Вам слід імпортувати комбайн
Нікола Лаурітано

Чим це відрізняється від варіанту 1 у самому питанні?
Найєм

У варіанті немає жодної програми PassthroughSubject
Nicola Lauritano

Ну, про це я не питав насправді. @Publishedоболонка і PassthroughSubjectобидва служать одній і тій же цілі в цьому контексті. Зверніть увагу на те, що ви написали, і чого насправді хотів досягти ОП. Чи є ваше рішення найкращою альтернативою, ніж варіант 1 насправді?
Найєм

0

сканування ( : :) Перетворює елементи видавця вище, надаючи поточний елемент закриттю, а також останнє значення, повернене закриттям.

Ви можете використовувати 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)
                }
            }
        }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.