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

时间:2019-10-02 14:24:38

标签: ios swift swiftui reactive combine

在命令式Swift中,通常使用计算属性在不复制状态的情况下方便地访问数据。

比方说,我将此类用于命令式MVC:

@Published

如果我想使用Combine创建一个等效的反应,例如为了与SwiftUI一起使用,我可以轻松地将Publisher添加到存储的属性中以生成 @Published var userIsLoggedIn: Bool { // Error: Property wrapper cannot be applied to a computed property currentUser != nil } ,但不能为计算的属性添加。

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

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

    // ...
}

我可以想到多种解决方法。我可以改为存储我的计算属性,并保持其更新。

选项1:使用属性观察器:

Subscriber

选择2:在自己的课堂上使用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

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

/es/添加到Combine中的计算属性中等同于什么?

5 个答案:

答案 0 :(得分:8)

对于基于@Published属性的计算属性,您无需执行任何操作。您可以像这样使用它:

class UserManager: ObservableObject {
  @Published
  var currentUser: User?

  var userIsLoggedIn: Bool {
    currentUser != nil
  }
}

@Published的{​​{1}}属性包装器中发生的事情是,它将在更改时调用currentUser中的objectWillChange.send()。 SwiftUI视图并不关心ObservedObject的哪些属性已更改,它只会重新计算视图并在必要时重新绘制。

工作示例:

@ObservedObject

和一个SwiftUI演示视图:

class UserManager: ObservableObject {
  @Published
  var currentUser: String?

  var userIsLoggedIn: Bool {
    currentUser != nil
  }

  func logOut() {
    currentUser = nil
  }

  func logIn() {
    currentUser = "Demo"
  }
}

答案 1 :(得分:5)

创建一个新发布者,该发布者已订阅要跟踪的媒体资源。

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

答案 2 :(得分:1)

您应该在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(可能更好的willSet)中,您将使用称为 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

中进行检查

答案 3 :(得分:0)

如何使用下游?

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

通过这种方式,订阅将从上游获取元素,然后您可以使用sinkassign来实现didSet的想法。

答案 4 :(得分: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)
                }
            }
        }