26

在 RxSwift 中,很容易将 aDriver或 a Observablein a绑定到 a 中的View Model某个观察者ViewController(即 a UILabel)。

我通常更喜欢构建一个管道,使用从其他 observables 创建的 observables,而不是“强制性地”推送值,比如通过 a PublishSubject)。

让我们使用这个例子:从网络获取一些数据后更新 aUILabel


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!)强制更新您的 property,这根本感觉不正确和被动。

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 的新世界中,“旧的绑定方式”会被遗忘和取代吗?

4

4 回答 4

8

我发现一种优雅的方法是将发布者上的错误替换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)
于 2019-09-14T08:30:13.267 回答
4

我认为这里缺少的部分是您忘记了您的 SwiftUI 代码有效的。在 MVVM 范式中,我们将功能部分拆分为视图模型,并将副作用保留在视图控制器中。使用 SwiftUI,副作用被推到了 UI 引擎本身的更高位置。

我还没有对 SwiftUI 搞砸太多,所以我不能说我理解所有的后果,但与 UIKit 不同的是,SwiftUI 代码不直接操作屏幕对象,而是创建一个结构,当传递给用户界面引擎。

于 2019-09-08T21:35:34.877 回答
2

发布之前的答案后阅读这篇文章:https ://nalexn.github.io/swiftui-observableobject/

并决定做同样的事情。使用@State,不要使用@Published

通用 ViewModel 协议:

protocol ViewModelProtocol {
    associatedtype Output
    associatedtype Input

    func bind(_ input: Input) -> Output
}

视图模型类:

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

于 2019-12-16T14:32:54.417 回答
1

我最终做出了一些妥协。在 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)
        }
    }
}

于 2019-12-16T12:58:11.117 回答