Combine + SwiftUI中的最佳数据绑定实践?

时间:2019-09-08 13:58:18

标签: ios swift swiftui rx-swift combine

在RxSwift中,将Driver中的ObservableView Model绑定到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之类的属性包装器和ObservableObjectObservedObject之类的协议来自动处理绑定(自 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 的世界中,是否会忘记并替换“绑定的旧方法”?

4 个答案:

答案 0 :(得分:3)

我发现的一种优雅方法是用Never替换发布者上的错误,然后使用assignassign仅在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)

答案 1 :(得分:2)

我认为这里遗漏的部分是您忘记了您的SwiftUI代码功能。在MVVM范例中,我们将功能部分拆分为视图模型,并将副作用保留在视图控制器中。借助SwiftUI,副作用会进一步推向UI引擎本身。

我还没有对SwiftUI造成太多混乱,所以我不能说我理解所有的后果,但是与UIKit不同,SwiftUI代码不会直接操作屏幕对象,而是创建了一个结构来在操作时进行操作。传递给UI引擎。

答案 2 :(得分:0)

我最终做出了一些妥协。在viewModel中使用@Published,但在SwiftUI View中进行订阅。 像这样:

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)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(viewModel: SwiftUIViewModel())
    }
}

答案 3 :(得分:0)

发布上一个答案后,请重新考虑所有内容并将订阅放入viewModel中 这可能会更好,因为所有的Combine代码都在viewModel中。

常规ViewModel协议:

protocol ViewModelProtocol {
    associatedtype Output

    func bind() -> Output
    func bindProperties(_ output: Output)
}

extension ViewModelProtocol {
    func setupBindings() {
        let output = bind()

        bindProperties(output)
    }
}

ViewModel类:

final class SwiftUIViewModel: ViewModelProtocol, ObservableObject {
    //Outputs
    @Published var dataPublisher : String = "ggg"

    //Inputs
    private let someButtonPressedSubject = PassthroughSubject<Void, Never>()

    func someButtonPressed() {
        someButtonPressedSubject.send()
    }

    struct Output {
        var dataPublisher: AnyPublisher<String, Never>
    }

    private var cancellableBag = Set<AnyCancellable>()

    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)
    }

    func bindProperties(_ output: Output) {
        output.dataPublisher
            .assign(to: \.dataPublisher, on: self)
            .store(in: &cancellableBag)
    }
}

SwiftUI视图:

struct ContentView: View {

    @ObservedObject var viewModel: SwiftUIViewModel

    init(viewModel: SwiftUIViewModel) {
        self.viewModel = viewModel

        viewModel.setupBindings()
    }

    var body: some View {
        VStack {
            Text(self.viewModel.dataPublisher)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(viewModel: SwiftUIViewModel())
    }
}