等同于在Swift Combine中使用@Published的计算属性?


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

    // ...
}

但是,这些解决方法不如计算属性好。它们复制状态,并且不会同时更新两个属性。

与将PublisherCombine 添加到Combine中的计算属性相比,什么是适当的等效方法?



1
计算属性是属于派生属性的一种属性。它们的值取决于依赖项的值。仅出于这个原因,可以说它们绝不会像那样表现ObservableObject。本质上,您假设一个ObservableObject对象应该具有变异能力,根据定义,该属性不是Computed Property的情况。
nayem

您找到解决方案了吗?我处于完全相同的情况下,我想避免出现状态,但仍然可以发布
erotsppa

Answers:


2

下游使用怎么样?

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

这样,订阅将从上游获取元素,然后您可以使用sinkassign进行操作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

您应该在ObservableObject中声明一个PassthroughSubject

class ReactiveUserManager1: ObservableObject {

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

    [...]
}

@Published var的didSet(可能会更好)中,您将使用一个名为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数据流讨论中进行检查


您应导入合并
尼古拉Lauritano

这与问题本身的选项1有何不同?
nayem

选项1中没有任何PassthroughSubject
Nicola Lauritano,

好吧,那不是我真正问的。@Published包装器和PassthroughSubject两者在此上下文中具有相同的目的。注意您编写的内容以及OP实际想要实现的内容。您的解决方案是否可以比选项1更好地替代?
nayem

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)
                }
            }
        }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.