根据观察到的变量的变化更新@Published 变量

Updating a @Published variable based on changes in an observed variable

我有一个可以观察到的AppState:

class AppState: ObservableObject {

    private init() {}
    static let shared = AppState()

    @Published fileprivate(set) var isLoggedIn = false

}

视图模型应根据状态决定显示哪个视图 (isLoggedIn):

class HostViewModel: ObservableObject, Identifiable {

    enum DisplayableContent {
        case welcome
        case navigationWrapper
    }

    @Published var containedView: DisplayableContent = AppState.shared.isLoggedIn ? .navigationWrapper : .welcome

}

最终 HostView 观察 containedView 属性 并基于它显示正确的视图。

我的问题是上面的代码没有观察到 isLoggedIn,我似乎无法找到一种方法来做到这一点。我很确定有一个简单的方法,但经过 4 小时的反复试验,我希望这里的社区可以帮助我。

当您将 ObservedObject 添加到视图时,SwiftUI 会为 objectWillChange 发布者添加一个接收器,您需要执行相同的操作。由于 objectWillChange 是在 isLoggedIn 更改之前发送的,因此添加一个在其 didSet 中发送的发布者可能是个好主意。由于您对初始值和更改感兴趣,因此 CurrentValueSubject<Bool, Never> 可能是最好的。然后,在您的 HostViewModel 中,您需要订阅 AppState 的新发布者并使用发布的值更新 containedView。使用 assign 会导致引用循环,所以 sink 和对 self 的弱引用是最好的。

没有代码,但非常简单。最后一个要注意的陷阱是将 sink 的返回值保存到 AnyCancellable? 中,否则您的订阅者将消失。

DISCLAIMER:

It is not full solution to the problem, it won't trigger objectWillChange, so it's useless for ObservableObject. But it may be useful for some related problems.

主要想法是创建 propertyWrapper,它将根据链接 Publisher:

中的变化更新 属性 值
@propertyWrapper
class Subscribed<Value, P: Publisher>: ObservableObject where P.Output == Value, P.Failure == Never {
    private var watcher: AnyCancellable?

    init(wrappedValue value: Value, _ publisher: P) {
        self.wrappedValue = value
        watcher = publisher.assign(to: \.wrappedValue, on: self)
    }

    @Published
    private(set) var wrappedValue: Value {
        willSet {
            objectWillChange.send()
        }
    }

    private(set) lazy var projectedValue = self.$wrappedValue
}

用法:

class HostViewModel: ObservableObject, Identifiable {

    enum DisplayableContent {
        case welcome
        case navigationWrapper
    }

    @Subscribed(AppState.shared.$isLoggedIn.map({ [=11=] ? DisplayableContent.navigationWrapper : .welcome }))
    var contained: DisplayableContent = .welcome

    // each time `AppState.shared.isLoggedIn` changes, `contained` will change it's value
    // and there's no other way to change the value of `contained`
}

工作解决方案:

在使用 Combine 两周后,我现在再次修改了我以前的解决方案(请参阅编辑历史记录),这是我现在能想到的最好的解决方案。这仍然不完全是我的想法,因为 contained 不是订阅者和发布者同时,但我认为 AnyCancellable 总是需要的。如果有人知道实现我的愿景的方法,请告诉我。

class HostViewModel: ObservableObject, Identifiable {

    @Published var contained: DisplayableContent
    private var containedUpdater: AnyCancellable?

    init() {
        self.contained = .welcome
        setupPipelines()
    }

    private func setupPipelines() {
        self.containedUpdater = AppState.shared.$isLoggedIn
            .map { [=10=] ? DisplayableContent.mainContent : .welcome }
            .assign(to: \.contained, on: self)
    }

}

extension HostViewModel {

    enum DisplayableContent {
        case welcome
        case mainContent
    }

}

订阅嵌入式 ObservedObject@Published 变量更改的通用解决方案是将 objectWillChange 通知传递给父对象。

示例:

import Combine

class Parent: ObservableObject {

  @Published
  var child = Child()

  var sink: AnyCancellable?

  init() {
    sink = child.objectWillChange.sink(receiveValue: objectWillChange.send)
  }
}

class Child: ObservableObject {
  @Published
  var counter: Int = 0

  func increase() {
    counter += 1
  }
}

SwiftUI 演示使用:

struct ContentView: View {

  @ObservedObject
  var parent = Parent()

  var body: some View {
    VStack(spacing: 50) {
      Text( "\(parent.child.counter)")
      Button( action: parent.child.increase) {
        Text( "Increase")
      }
    }
  }
}