Combine + SwiftUI 中的最佳数据绑定实践?
Posted
技术标签:
【中文标题】Combine + SwiftUI 中的最佳数据绑定实践?【英文标题】:Best data-binding practice in Combine + SwiftUI? 【发布时间】:2020-01-10 13:08:34 【问题描述】:在 RxSwift 中,很容易将 Driver
或 View Model
中的 Observable
绑定到 ViewController
中的某个观察者(即 UILabel
)。
我通常更喜欢构建一个管道,使用 从其他可观察对象创建的可观察对象,而不是“强制”推送值,例如通过 PublishSubject
)。
让我们使用这个例子:从网络获取一些数据后更新UILabel
RxSwift + RxCocoa 示例
final class RxViewModel
private var dataObservable: Observable<Data>
let stringDriver: Driver<String>
init()
let request = URLRequest(url: URL(string:"https://www.google.com")!)
self.dataObservable = URLSession.shared
.rx.data(request: request).asObservable()
self.stringDriver = dataObservable
.asDriver(onErrorJustReturn: Data())
.map _ in return "Network data received!"
final class RxViewController: UIViewController
private let disposeBag = DisposeBag()
let rxViewModel = RxViewModel()
@IBOutlet weak var rxLabel: UILabel!
override func viewDidLoad()
super.viewDidLoad()
rxViewModel.stringDriver.drive(rxLabel.rx.text).disposed(by: disposeBag)
结合 + UIKit 示例
在基于 UIKit 的项目中,您似乎可以保持相同的模式:
视图模型公开发布者 视图控制器将其 UI 元素绑定到这些发布者final class CombineViewModel: ObservableObject
private var dataPublisher: AnyPublisher<URLSession.DataTaskPublisher.Output, URLSession.DataTaskPublisher.Failure>
var stringPublisher: AnyPublisher<String, Never>
init()
self.dataPublisher = URLSession.shared
.dataTaskPublisher(for: URL(string: "https://www.google.it")!)
.eraseToAnyPublisher()
self.stringPublisher = dataPublisher
.map (_, _) in return "Network data received!"
.replaceError(with: "Oh no, error!")
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
final class CombineViewController: UIViewController
private var cancellableBag = Set<AnyCancellable>()
let combineViewModel = CombineViewModel()
@IBOutlet weak var label: UILabel!
override func viewDidLoad()
super.viewDidLoad()
combineViewModel.stringPublisher
.flatMap Just($0)
.assign(to: \.text, on: self.label)
.store(in: &cancellableBag)
SwiftUI 呢?
SwiftUI 依赖于像 @Published
这样的属性包装器和像 ObservableObject
、ObservedObject
这样的协议来自动处理绑定(截至 Xcode 11b7)。
由于 (AFAIK) 属性包装器不能“动态创建”,因此您无法使用相同的模式重新创建上面的示例。 以下无法编译
final class WrongViewModel: ObservableObject
private var dataPublisher: AnyPublisher<URLSession.DataTaskPublisher.Output, URLSession.DataTaskPublisher.Failure>
@Published var stringValue: String
init()
self.dataPublisher = URLSession.shared
.dataTaskPublisher(for: URL(string: "https://www.google.it")!)
.eraseToAnyPublisher()
self.stringValue = dataPublisher.map ... . ??? <--- WRONG!
我能想到的最接近的方法是在您的视图模型中订阅(UGH!) 并立即更新您的属性,这感觉完全不正确和被动。
final class SwiftUIViewModel: ObservableObject
private var cancellableBag = Set<AnyCancellable>()
private var dataPublisher: AnyPublisher<URLSession.DataTaskPublisher.Output, URLSession.DataTaskPublisher.Failure>
@Published var stringValue: String = ""
init()
self.dataPublisher = URLSession.shared
.dataTaskPublisher(for: URL(string: "https://www.google.it")!)
.eraseToAnyPublisher()
dataPublisher
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: _ in ) (_, _) in
self.stringValue = "Network data received!"
.store(in: &cancellableBag)
struct ContentView: View
@ObservedObject var viewModel = SwiftUIViewModel()
var body: some View
Text(viewModel.stringValue)
在这个新的 UIViewController-less 世界中,“旧的绑定方式”会被遗忘和取代吗?
【问题讨论】:
我不认为有任何内置的方法可以做你想做的事。 This 是某人制作的辅助函数,您可能会觉得有趣。 在 SwiftUI 中有两种处理异步数据的方法,或者可能有两种变体。您可以按照 Benjamin 的建议使用 onReceive 或将数据保存在类中并发送 objectWillChange 消息。我都用过,它们都很容易使用。我见过的 onReceive 的最大缺点是,由于视图的状态发生变化,它可能会受到正文被重新读取的影响,请参阅***.com/questions/57796877/…,如果两个计时器都是 1 秒,则会出现问题- 【参考方案1】:我发现一种优雅的方法是将发布者上的错误替换为Never
,然后使用assign
(assign
仅在Failure == Never
时有效)。
你的情况……
dataPublisher
.receive(on: DispatchQueue.main)
.map _ in "Data received" //for the sake of the demo
.replaceError(with: "An error occurred") //this sets Failure to Never
.assign(to: \.stringValue, on: self)
.store(in: &cancellableBag)
【讨论】:
【参考方案2】:我认为这里缺少的部分是您忘记了您的 SwiftUI 代码是功能。在 MVVM 范式中,我们将功能部分拆分为视图模型,并将副作用保留在视图控制器中。使用 SwiftUI,副作用被推到了 UI 引擎本身的更高位置。
我还没有对 SwiftUI 搞砸太多,所以我不能说我理解所有的后果,但与 UIKit 不同的是,SwiftUI 代码不直接操作屏幕对象,而是创建一个结构来执行操作传递给 UI 引擎。
【讨论】:
【参考方案3】:在发布上一个答案后阅读这篇文章:https://nalexn.github.io/swiftui-observableobject/
并决定采取同样的方式。使用@State,不要使用@Published
通用 ViewModel 协议:
protocol ViewModelProtocol
associatedtype Output
associatedtype Input
func bind(_ input: Input) -> Output
ViewModel 类:
final class SwiftUIViewModel: ViewModelProtocol
struct Output
var dataPublisher: AnyPublisher<String, Never>
typealias Input = Void
func bind(_ input: Void) -> Output
let dataPublisher = URLSession.shared.dataTaskPublisher(for: URL(string: "https://www.google.it")!)
.map "Just for testing - \($0)"
.replaceError(with: "An error occurred")
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
return Output(dataPublisher: dataPublisher)
SwiftUI 视图:
struct ContentView: View
@State private var dataPublisher: String = "ggg"
let viewModel: SwiftUIViewModel
let output: SwiftUIViewModel.Output
init(viewModel: SwiftUIViewModel)
self.viewModel = viewModel
self.output = viewModel.bind(())
var body: some View
VStack
Text(self.dataPublisher)
.onReceive(output.dataPublisher) value in
self.dataPublisher = value
【讨论】:
检查 github.com/serbats/Reactive-Combine-MVVM-Templates 以获得一些 MVVM Xcode 模板【参考方案4】:我最终做出了一些妥协。在 viewModel 中使用 @Published
,但在 SwiftUI 视图中订阅。
像这样:
final class SwiftUIViewModel: ObservableObject
struct Output
var dataPublisher: AnyPublisher<String, Never>
@Published var dataPublisher : String = "ggg"
func bind() -> Output
let dataPublisher = URLSession.shared.dataTaskPublisher(for: URL(string: "https://www.google.it")!)
.map "Just for testing - \($0)"
.replaceError(with: "An error occurred")
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
return Output(dataPublisher: dataPublisher)
和 SwiftUI:
struct ContentView: View
private var cancellableBag = Set<AnyCancellable>()
@ObservedObject var viewModel: SwiftUIViewModel
init(viewModel: SwiftUIViewModel)
self.viewModel = viewModel
let bindStruct = viewModel.bind()
bindStruct.dataPublisher
.assign(to: \.dataPublisher, on: viewModel)
.store(in: &cancellableBag)
var body: some View
VStack
Text(self.viewModel.dataPublisher)
【讨论】:
以上是关于Combine + SwiftUI 中的最佳数据绑定实践?的主要内容,如果未能解决你的问题,请参考以下文章
SwiftUI + Combine:如何将数据分配给带有动画的模型
SwiftUI ObjectBinding 不会使用 combine 接收来自可绑定对象的 didchange 更新
将 AppKit/UIKit 中的 Key-Value Observation 转换为 Combine 和 SwiftUI