相当于在 Swift Combine 中使用 @Published 的计算属性?

An equivalent to computed properties using @Published in Swift Combine?

在命令式 Swift 中,通常使用计算属性来方便地访问数据而无需复制状态。

假设我有这个 class 用于命令式 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 添加到存储的属性以生成 Publisher,但不能用于计算属性。

    @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:在我自己的 class 中使用 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 { [=14=] != nil }
            .assign(to: \.userIsLoggedIn, on: self)
            .store(in: &subscribers)
    }

    // ...
}

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

在 Combine 中向计算的 属性 添加 Publisher 的适当等价物是什么?

使用下游怎么样?

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

这样订阅会从上游获取元素,然后可以用sink或者assign来实现didSet的思路

您可以在 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()[ 的方法=13=]

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

中查看

扫描(::) 通过将当前元素连同闭包返回的最后一个值提供给闭包来转换来自上游发布者的元素。

您可以使用 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)

}

以上代码等同于:(减去 Combine)

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

创建订阅您要跟踪的 属性 的新发布者。

@Published var speed: Double = 88

lazy var canTimeTravel: AnyPublisher<Bool,Never> = {
    $speed
        .map({ [=10=] >= 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()
}()

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

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

  var userIsLoggedIn: Bool {
    currentUser != nil
  }
}

currentUser@Published 属性 包装器中发生的事情是它会在更改时调用 ObservedObjectobjectWillChange.send()。 SwiftUI 视图不关心 @ObservedObject 的哪些属性发生了变化,它只会重新计算视图并在必要时重绘。

工作示例:

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

  var userIsLoggedIn: Bool {
    currentUser != nil
  }

  func logOut() {
    currentUser = nil
  }

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

还有一个 SwiftUI 演示视图:

struct ContentView: View {

  @ObservedObject
  var userManager = UserManager()

  var body: some View {
    VStack( spacing: 50) {
      if userManager.userIsLoggedIn {
        Text( "Logged in")
        Button(action: userManager.logOut) {
          Text("Log out")
        }
      } else {
        Text( "Logged out")
        Button(action: userManager.logIn) {
          Text("Log in")
        }
      }
    }
  }
}

我也遇到了同样的错误,并最终进入了这个主题。但是,因为我是 Swift 的新手,所以我不理解大多数代码,例如 subscriber、didSet,我尝试自己解决问题并且成功了。希望对你也有帮助。

  @Published var userIsLoggedIn = Bool()
    
    init() {
        self.userIsLoggedIn = (currentUser != nil)
    }