将 SwiftUI 按钮绑定到 AnySubscriber,类似于 RxCocoa 的按钮点击。

7
我使用以下基于UIViewControllerRxSwift/RxCocoa的代码片段,编写一个非常简单的MVVM模式,将UIButton的点击事件与某些Observable工作绑定并监听结果:
import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {

    @IBOutlet weak var someButton: UIButton!

    var viewModel: ViewModel!
    private var disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        viewModel = ViewModel()
        setupBindings()
    }

    private func setupBindings() {
        someButton.rx.tap
        .bind(to: self.viewModel.input.trigger)
        .disposed(by: disposeBag)

        viewModel.output.result
            .subscribe(onNext: { element in
            print("element is \(element)")
            }).disposed(by: disposeBag)
    }
}

class ViewModel {

    struct Input {
        let trigger: AnyObserver<Void>
    }

    struct Output {
        let result: Observable<String>
    }

    let input: Input
    let output: Output

    private let triggerSubject = PublishSubject<Void>()

    init() {
        self.input = Input(trigger: triggerSubject.asObserver())
        let resultObservable = triggerSubject.flatMap { Observable.just("TEST") }
        self.output = Output(result: resultObservable)
    }
}

代码可以成功编译和运行,但是我需要在 SwiftUI 中将此模式变成组合模式,因此我将该代码转换为以下内容:

import SwiftUI
import Combine

struct ContentView: View {
    var viewModel: ViewModel
    var subscriptions = Set<AnyCancellable>()

    init(viewModel: ViewModel) {
        self.viewModel = viewModel
        setupBindings()
    }

    var body: some View {

        Button(action: {
            // <---- how to trigger viewModel's trigger from here
        }, label: {
            Text("Click Me")
        })
    }

    private func setupBindings() {
        self.viewModel.output.result.sink(receiveValue: { value in
            print("value is \(value)")
            })
            .store(in: &subscriptions) // <--- doesn't compile due to immutability of ContentView
    }
}

class ViewModel {

    struct Input {
        let trigger: AnySubscriber<Void, Never>
    }

    struct Output {
        let result: AnyPublisher<String, Never>
    }

    let input: Input
    let output: Output

    private let triggerSubject = PassthroughSubject<Void, Never>()

    init() {
        self.input = Input(trigger: AnySubscriber(triggerSubject))

        let resultPublisher = triggerSubject
            .flatMap { Just("TEST") }
            .eraseToAnyPublisher()

        self.output = Output(result: resultPublisher)
    }
}

由于两个错误,此示例无法编译(在代码中已注释):

(1)问题1:如何像上面的RxSwift案例一样,从按钮的操作闭包触发发布者的工作?

(2)问题2与架构设计有关,而不是编译错误:错误消息为:... Cannot pass immutable value as inout argument: 'self' is immutable ..., 这是因为SwiftUI视图是结构体,它们仅通过各种绑定(@State@ObservedObject等)进行更改,我有两个与问题2相关的子问题:

[A]: 在SwiftUI视图中“sink”一个发布者是否被认为是一种不好的做法?这可能需要一些解决方法来将存储在

[B]: 在MVVM架构模式方面,对于SwiftUI/Combine项目来说,哪种更好:使用具有[Input[Subscribers],Output[AnyPublishers]]模式的ViewModel,还是使用具有[@Published]属性的ObservableObject ViewModel?


2
如果有人开始回答,他们会重复官方文档,因此可以直接从SwiftUI:State and Data Flow开始。简而言之,使用ObservableObject-它是本地、简单且自动与SwiftUI集成的。 - Asperi
2个回答

3

最近我也在想如何做到这一点,因为我们不是要在SwiftUI中编写视图。

我创建了一个助手对象,它封装了从函数调用到发布者的转换。我称之为“Relay”。

@available(iOS 13.0, *)
struct Relay<Element> {
    var call: (Element) -> Void { didCall.send }
    var publisher: AnyPublisher<Element, Never> { 
        didCall.eraseToAnyPublisher() 
    }

    // MARK: Private

    private let didCall = PassthroughSubject<Element, Never>()
}

在您的情况下,您可以声明一个私有Relay并像这样使用它;
    Button(action: relay.call, 
    label: {
        Text("Click Me")
    })

然后,您可以随心所欲地使用它。

relay.publisher

这很棒!它帮助我将一个 UViewController(它是 UIHostingController 的子类)绑定到 SwiftUI 视图的按钮点击上,通过在视图中公开 didTapLogin: Relay<Void>。 - ErickES7

3

我曾经也遇到过理解最佳mvvm方法的问题。 建议您也参考这个主题Best data-binding practice in Combine + SwiftUI?

我将发布我的工作示例。应该很容易转换为您想要的内容。

SwiftUI视图:

struct ContentView: View {
    @State private var dataPublisher: String = "ggg"
    @State private var sliderValue: String = "0"
    @State private var buttonOutput: String = "Empty"


    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)
            Text(self.sliderValue)
            Slider(value: viewModel.$sliderBinding, in: 0...100, step: 1)
            Button(action: {
                self.viewModel.buttonBinding = ()
            }, label: {
                Text("Click Me")
            })
            Text(self.buttonOutput)
        }
        .onReceive(output.dataPublisher) { value in
            self.dataPublisher = value
        }
        .onReceive(output.slider) { (value) in
            self.sliderValue = "\(value)"
        }
        .onReceive(output.resultPublisher) { (value) in
            self.buttonOutput = value
        }
    }
}

抽象视图模型:

protocol ViewModelProtocol {
    associatedtype Output
    associatedtype Input

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

视图模型:

final class SwiftUIViewModel: ViewModelProtocol {
    struct Output {
        let dataPublisher: AnyPublisher<String, Never>
        let slider: AnyPublisher<Double, Never>
        let resultPublisher: AnyPublisher<String, Never>
    }

    typealias Input = Void

    @SubjectBinding var sliderBinding: Double = 0.0
    @SubjectBinding var buttonBinding: Void = ()

    func bind(_ input: Void) -> Output {
        let dataPublisher = URLSession.shared.dataTaskPublisher(for: URL(string: "https://www.google.it")!)
            .delay(for: 5.0, scheduler: DispatchQueue.main)
            .map{ "Just for testing - \($0)"}
            .replaceError(with: "An error occurred")
            .receive(on: DispatchQueue.main)
            .share()
            .eraseToAnyPublisher()

        let resultPublisher = _buttonBinding.anyPublisher()
            .dropFirst()
            .flatMap { Just("TEST") }
            .share()
            .eraseToAnyPublisher()

        return Output(dataPublisher: dataPublisher,
                      slider: _sliderBinding.anyPublisher(),
                      resultPublisher: resultPublisher)
    }
}

SubjectBinding 属性包装器:

@propertyWrapper
struct SubjectBinding<Value> {
    private let subject: CurrentValueSubject<Value, Never>

    init(wrappedValue: Value) {
        subject = CurrentValueSubject<Value, Never>(wrappedValue)
    }

    func anyPublisher() -> AnyPublisher<Value, Never> {
        return subject.eraseToAnyPublisher()
    }

    var wrappedValue: Value {
        get {
            return subject.value
        }
        set {
            subject.value = newValue
        }
    }

    var projectedValue: Binding<Value> {
        return Binding<Value>(get: { () -> Value in
            return self.subject.value
        }) { (value) in
            self.subject.value = value
        }
    }
}

请访问 https://github.com/serbats/Reactive-Combine-MVVM-Templates 查看一些 MVVM Xcode 模板。 - sergiy batsevych

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接