簡體   English   中英

等效於在 Swift 中使用 @Published 的計算屬性組合?

[英]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屬性包裝器中發生的情況是,它將在更改時調用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 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()

這樣,訂閱就會從上游獲取元素,然后你可以使用sinkassign來做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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM