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

在命令式 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添加到存储属性以生成 Publisher,但是不能为计算属性添加 @Published

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


// ...
}

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

在组合中,将 Publisher添加到计算属性的合适等价物是什么?

36459 次浏览

使用下游怎么样?

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

这样,订阅就可以从上游获得元素,然后可以用 sinkassign来做 didSet的想法。

你可以在你的可观察对象中声明一个 通过主题:

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 (will Set 可能更好)中,你将使用一个叫做 发送()的方法

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


[...]
}

你可以在 数据流讲座里查一下

扫描(::) 通过向闭包提供当前元素以及闭包返回的最后一个值,从上游发布者转换元素。

您可以使用 scanner ()来获取最新的和当前的值。 例如:

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

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

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

对于基于 @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")
}
}
}
}
}

我也得到了同样的错误,并结束了这个话题。然而,因为我是新的斯威夫特我不明白大多数的代码,如订阅者,做集和我试图解决自己的问题,它工作。我希望这对你也有帮助。

  @Published var userIsLoggedIn = Bool()
    

init() {
self.userIsLoggedIn = (currentUser != nil)
}

一个简单的解决办法:

@Published private(set) var hiddenSelectedName: String = ""
var selectedName: String {
get {
return hiddenSelectedName
}
set(newVal) {
if hiddenSelectedName != newVal {
hiddenSelectedName = newVal
// call methods and other stuff you need here...
}
}
}
}