使用 Combine 和 SwiftUI 显示变化值的最简洁方式是啥?

Posted

技术标签:

【中文标题】使用 Combine 和 SwiftUI 显示变化值的最简洁方式是啥?【英文标题】:What is the most concise way to display a changing value with Combine and SwiftUI?使用 Combine 和 SwiftUI 显示变化值的最简洁方式是什么? 【发布时间】:2019-10-26 16:29:38 【问题描述】:

我正试图围绕 SwiftUI 和 Combine。我想用一个值使 UI 中的一些文本保持最新。例如,在这种情况下,它是设备的电池电量。

这是我的代码。首先,这似乎是实现我想要做的事情的相当多的代码,所以我想知道我是否可以不使用它。另外,这段代码曾经在夏天运行,但现在它崩溃了,可能是由于 SwiftUI 和 Combine 的变化。

如何解决此问题以与当前版本的 SwiftUI 和 Combine 一起使用?而且,是否可以减少这里的代码量来做同样的事情?

import SwiftUI
import Combine

class ViewModel: ObservableObject 

    var willChange = PassthroughSubject<Void, Never>()

    var batteryLevelPublisher = UIDevice.current
        .publisher(for: \.batteryLevel)
        .receive(on: RunLoop.main)

    lazy var batteryLevelSubscriber = Subscribers.Assign(object: self,
                                                         keyPath: \.batteryLevel)

    var batteryLevel: Float = UIDevice.current.batteryLevel 
        didSet 
            willChange.send()
        
    

    init() 
        batteryLevelPublisher.subscribe(batteryLevelSubscriber)
    


struct ContentView: View 

    @ObservedObject var viewModel = ViewModel()

    var body: some View 
        Text("\(Int(round(viewModel.batteryLevel * 100)))%")
    

【问题讨论】:

【参考方案1】:

要在 iPadOS Swift Playground 中粘贴的最小工作示例。

基本上它是与 SwiftUI 和 Combine 的最新更改相一致的代码。

对您想要在视图中观察的任何属性使用 @Published 属性包装器(文档:默认情况下,ObservableObject 会合成一个 objectWillChange 发布者,该发布者会在其任何 @Published 属性更改之前发出更改的值。)。这避免了使用自定义设置器和 objectWillChange。

cancellable 是 Publishers.Assign 的输出,它可用于手动取消订阅,为了最佳实践,它将存储在“CancellableBag”中,从而在 deinit 上取消订阅。这种做法的灵感来自其他响应式框架,例如 RxSwift 和 ReactiveUI。

我发现在不打开电池电量通知的情况下,KVO 发布者的电池电量只会发出一次 -1.0。

// iPadOS playground
import SwiftUI
import Combine
import PlaygroundSupport

class BatteryModel : ObservableObject 
    @Published var level = UIDevice.current.batteryLevel
    private var cancellableSet: Set<AnyCancellable> = []

    init () 
        UIDevice.current.isBatteryMonitoringEnabled = true
        assignLevelPublisher()
    

    private func assignLevelPublisher() 
        _ = UIDevice.current
            .publisher(for: \.batteryLevel)
            .assign(to: \.level, on: self)
            .store(in: &self.cancellableSet)
    

struct ContentView: View 

    @ObservedObject var batteryModel = BatteryModel()

    var body: some View 
        Text("\(Int(round(batteryModel.level * 100)))%")
    


let host = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = host

【讨论】:

我认为将发布者逻辑排除在 ContentView 和 ViewModel 之外会更好,因为所有数据都来自 ObservedObject。是否可以使用类似的方法并做到这一点?我似乎无法弄清楚如何将.assign 用于可观察对象上的属性。 感谢您更新使用模型!即使您认为基本概念的解释不适合此处,如果您可以添加任何链接到您认为有助于解释它的答案,我将不胜感激。我已经准备好了很多 Apple 的文档,但是在不理解为什么在特定示例中使用某些东西的情况下,我无法理解这些概念。例如,我不明白.storecancellableSet 在这里做什么。我原以为我可以使用.assign 来更新level var。 这里为什么需要.storecancellableSet?为什么不能使用.assign 更新level var? 我想 assign 返回 AnyCancelable 它必须存储在某个地方,因为它会在 deinit 上自动取消订阅 @BenSinclair 我发现如果不使用.store,则assign 中的代码只能工作一次。对此还有什么解释吗?【参考方案2】:

这是一个适用于任何支持 KVO 的对象的通用解决方案:

class KeyPathObserver<T: NSObject, V>: ObservableObject 
  @Published var value: V
  private var cancel = Set<AnyCancellable>()

  init(_ keyPath: KeyPath<T, V>, on object: T) 
    value = object[keyPath: keyPath]
    object.publisher(for: keyPath)
      .assign(to: \.value, on: self)
      .store(in: &cancel)
  

因此,要在您的视图中监控电池电量(例如),您需要像这样添加@ObservedObject

@ObservedObject batteryLevel = KeyPathObserver(\.batteryLevel, on: UIDevice.current)

然后您可以直接或通过@ObservedObjectvalue 属性访问该值

batteryLevel.value

【讨论】:

以上是关于使用 Combine 和 SwiftUI 显示变化值的最简洁方式是啥?的主要内容,如果未能解决你的问题,请参考以下文章

使用 SwiftUI 和 Combine 根据授权状态有条件地显示视图?

异步下载图像时SwiftUI和Combine工作不顺畅

SwiftUI + Combine:如何将数据分配给带有动画的模型

使用 SwiftUI+Combine 的数据库延迟加载

如何使用 Combine 框架和 SwiftUI 发布网络请求的数据

在 SwiftUI 中使用 Combine 进行映射、捕获错误和分配