相当于在 Swift Combine 中使用 @Published 计算的属性?
Posted
技术标签:
【中文标题】相当于在 Swift Combine 中使用 @Published 计算的属性?【英文标题】:An equivalent to computed properties using @Published in Swift Combine? 【发布时间】:2020-01-31 20:26:56 【问题描述】:在命令式 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
s,但不适用于计算属性。
@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
添加到Combine 中的计算属性的适当等效方法是什么?
【问题讨论】:
Updating a @Published variable based on changes in an observed variable的可能重复 计算属性是一种派生属性。它们的值取决于依赖项的值。仅出于这个原因,可以说他们永远不会表现得像ObservableObject
。您天生就假设ObservableObject
对象应该能够具有变异能力,根据定义,Computed Property 并非如此。
您找到解决方案了吗?我处于完全相同的情况,我想避免状态并且仍然能够发布
感谢private(set)
解决方案。对我帮助很大。
让所有subscribers
保持在一个好主意!我会采纳的
【参考方案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查看它
【讨论】:
你应该导入 Combine 这与问题本身的选项1有何不同? option1中没有PassthroughSubject objectWillChange 不再需要声明它现在自动带有 ObservableObject 协议。【参考方案2】:您无需对基于@Published
属性的计算属性执行任何操作。你可以像这样使用它:
class UserManager: ObservableObject
@Published
var currentUser: User?
var userIsLoggedIn: Bool
currentUser != nil
在currentUser
的@Published
属性包装器中发生的情况是,它会在更改时调用ObservedObject
的objectWillChange.send()
。 SwiftUI 视图不关心@ObservedObject
s 的哪些属性发生了变化,它只会重新计算视图并在必要时重绘。
工作示例:
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")
【讨论】:
使用您的解决方案(没有private (set)
),currentUser 可以从用户管理器类外部访问,这在 OOP 方面不是一个好的做法。
谢谢你的解释,很高兴知道
对于 SwiftUI 视图观察到可观察对象的特定上下文,这是一个很好的答案。但是如果你想要Published
的所有特性——一个可以在任意上下文中读取和订阅的属性——你需要在计算属性之外创建一个映射的发布者。
rberggreen,是的,这是专门针对@Published
与ObservableObject
结合使用的情况的答案。对于没有ObservableObject
的情况,您需要创建映射。【参考方案3】:
创建订阅您要跟踪的属性的新发布者。
@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()
()
【讨论】:
我相信在你的第二个 sn-p 中应该是sourceOfTruthObject.canTimeTravel
而不是 sourceOfTruthObject.$canTimeTravel
。也许在撰写本文时情况有所不同。
试图理解为什么会这样lazy
@SergioBost 我相信您在初始化时无法访问threshold
。因此,lazy
会告诉编译器这不可能发生,并且仍然以您想要的方式声明它(即:不在init()
中)【参考方案4】:
扫描(::) 通过将当前元素与闭包返回的最后一个值一起提供给闭包来转换来自上游发布者的元素。
您可以使用 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)
上面的代码等价于:(less Combine)
@Published var loading: Bool = false
didSet
if oldValue == false, loading == true
NotificationCenter.default.post(name: NSNotification.Name("userStateDidChange"), object: nil)
【讨论】:
【参考方案5】:如何使用下游?
lazy var userIsLoggedInPublisher: AnyPublisher = $currentUser
.map$0 != nil
.eraseToAnyPublisher()
这样订阅会从上游获取元素,那么你可以使用sink
或assign
来做didSet
的想法。
【讨论】:
以上是关于相当于在 Swift Combine 中使用 @Published 计算的属性?的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 Combine + Swift 复制 PromiseKit 风格的链式异步流
Xcode 11 中 Swift Combine.framework 的可选链接
使用 Swift 和 Combine 链接 + 压缩多个网络请求