[英]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.