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

rbe*_*een 55 ios swift reactive swiftui combine

在命令式 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
    }

    // ...
}
Run Code Online (Sandbox Code Playgroud)

如果我想使用Combine 创建一个反应式等效项,例如用于SwiftUI,我可以轻松添加@Published到存储的属性以生成Publishers,但不能用于计算属性。

    @Published var userIsLoggedIn: Bool { // Error: Property wrapper cannot be applied to a computed property
        currentUser != nil
    }
Run Code Online (Sandbox Code Playgroud)

我可以想到各种解决方法。我可以将我的计算属性存储起来并保持更新。

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

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

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

    // ...
}
Run Code Online (Sandbox Code Playgroud)

选项 2:Subscriber在我自己的班级中使用 a :

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

    // ...
}
Run Code Online (Sandbox Code Playgroud)

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

Publisher在Combine中将a 添加到计算属性的正确等效项是什么?

las*_*sej 31

You don't need to do anything for computed properties that are based on @Published properties. You can just use it like this:

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

  var userIsLoggedIn: Bool {
    currentUser != nil
  }
}

Run Code Online (Sandbox Code Playgroud)

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

工作示例:

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

  var userIsLoggedIn: Bool {
    currentUser != nil
  }

  func logOut() {
    currentUser = nil
  }

  func logIn() {
    currentUser = "Demo"
  }
}
Run Code Online (Sandbox Code Playgroud)

以及 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")
        }
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 对于通过 SwiftUI 视图观察可观察对象的特定上下文,这是一个很好的答案。但是,如果您想要“Published”的所有功能(可以在任意上下文中读取和订阅的属性),除了计算属性之外,您还需要创建一个映射的发布者。 (4认同)

Pom*_*ule 11

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

@Published var speed: Double = 88

lazy var canTimeTravel: AnyPublisher<Bool,Never> = {
    $speed
        .map({ $0 >= 88 })
        .eraseToAnyPublisher()
}()
Run Code Online (Sandbox Code Playgroud)

然后,您将能够像您的@Published财产一样观察它。

private var subscriptions = Set<AnyCancellable>()


override func viewDidLoad() {
    super.viewDidLoad()

    sourceOfTruthObject.$canTimeTravel.sink { [weak self] (canTimeTravel) in
        // Do something…
    })
    .store(in: &subscriptions)
}
Run Code Online (Sandbox Code Playgroud)

没有直接关系,但仍然是有用的,你可以跟踪多个属性与方法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()
}()
Run Code Online (Sandbox Code Playgroud)

  • 我相信第二个片段中应该是“sourceOfTruthObject.canTimeTravel”而不是“sourceOfTruthObject.$canTimeTravel”。也许在撰写本文时情况有所不同。 (2认同)

小智 5

使用下游怎么样?

lazy var userIsLoggedInPublisher: AnyPublisher = $currentUser
                                          .map{$0 != nil}
                                          .eraseToAnyPublisher()
Run Code Online (Sandbox Code Playgroud)

这样,订阅就会从上游获取元素,然后你就可以使用sinkorassign来实现这个didSet想法。