[英]An equivalent to computed properties using @Published in Swift Combine?
在命令式 Swift 中,通常使用计算属性来方便地访问数据,而无需复制 state。
假设我有这个 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
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:在我自己的 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 { $0 != nil }
.assign(to: \.userIsLoggedIn, on: self)
.store(in: &subscribers)
}
// ...
}
然而,这些变通方法并不像计算属性那样优雅。 它们复制 state 并且它们不会同时更新这两个属性。
什么是适当的等价于将Publisher
者添加到组合中的计算属性?
您无需对基于@Published
属性的计算属性执行任何操作。 你可以像这样使用它:
class UserManager: ObservableObject {
@Published
var currentUser: User?
var userIsLoggedIn: Bool {
currentUser != nil
}
}
currentUser
的@Published
属性包装器中发生的情况是,它将在更改时调用ObservedObject
的objectWillChange.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 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()
}()
使用下游怎么样?
lazy var userIsLoggedInPublisher: AnyPublisher = $currentUser
.map{$0 != 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()的方法
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( : :) 通过将当前元素与闭包返回的最后一个值一起提供给闭包来转换来自上游发布者的元素。
您可以使用 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)
}
上面的代码等价于:(少合并)
@Published var loading: Bool = false {
didSet {
if oldValue == false, loading == true {
NotificationCenter.default.post(name: NSNotification.Name("userStateDidChange"), object: 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...
}
}
}
}
我也遇到了同样的错误,最终进入了这个话题。 但是,因为我是 Swift 的新手,所以我不了解订阅者、didSet 等大多数代码,我尝试自己解决问题,并且成功了。 我希望它对你也有帮助。
@Published var userIsLoggedIn = Bool() init() { self.userIsLoggedIn = (currentUser != nil) }
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.