Ios 将SwiftUI按钮绑定到任何订户,如RxCocoa';s按钮点击

Ios 将SwiftUI按钮绑定到任何订户,如RxCocoa';s按钮点击,ios,swiftui,rx-swift,combine,rx-cocoa,Ios,Swiftui,Rx Swift,Combine,Rx Cocoa,我使用以下基于UIViewController和RxSwift/RxCocoa的代码段编写一个非常简单的MVVM模式,绑定UIButton点击事件,触发一些可观察的工作并监听结果: import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { @IBOutlet weak var someButton: UIButton! var viewModel: ViewMode

我使用以下基于
UIViewController
RxSwift/RxCocoa
的代码段编写一个非常简单的MVVM模式,绑定
UIButton
点击事件,触发一些
可观察的
工作并监听结果:

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)
    }
}
由于两个错误(在代码中注释),此示例无法编译:

(1)问题1:如何像上面的
RxSwift
那样从按钮的动作关闭触发出版商的作品

(2)问题2与建筑设计有关,而不是编译错误: 错误显示:
。。。无法传递不可变值,因为inout参数:“self”是不可变的…
,这是因为
SwiftUI
视图是结构,它们被设计为只能通过各种绑定进行更改(
@State
@ObservedObject
等…),我有两个子问题与问题2相关:

[A] :在
SwiftUI
视图中接收发布者是否被认为是一种不良做法?在
视图
的结构范围中存储
可取消的
可能需要一些变通方法

[B] :在MVVM体系结构模式方面,对于
SwiftUI/Combine
项目,哪一个更好:使用带有[Input[Subscribers]、Output[AnyPublishers]]模式的ViewModel,还是使用
observeObject
ViewModel与[
@Published
属性]?

我在理解最佳mvvm方法时遇到了同样的问题。 建议您也查看此线程

将发布我的工作示例。应该很容易转换成你想要的

快捷视图:

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)
    }
}
视图模型:

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)
    }
}
最终类SwiftUIViewModel:ViewModelProtocol{
结构输出{
让dataPublisher:AnyPublisher
让滑块:AnyPublisher
让结果发布者:AnyPublisher
}
typealias输入=无效
@SubjectBinding变量sliderBinding:Double=0.0
@SubjectBinding变量buttonBinding:Void=()
func bind(u输入:Void)->输出{
让dataPublisher=URLSession.shared.dataTaskPublisher(用于:URL(字符串):https://www.google.it")!)
.delay(对于:5.0,调度程序:DispatchQueue.main)
.map{“仅用于测试-\($0)”}
.replaceError(带有“发生错误”)
.receive(在:DispatchQueue.main上)
.share()
.删除任何发布者()
让resultPublisher=\u buttonBinding.anyPublisher()
.dropFirst()
.flatMap{Just(“TEST”)}
.share()
.删除任何发布者()
返回输出(dataPublisher:dataPublisher,
滑块:\ u sliderBinding.anyPublisher(),
结果发布者:结果发布者)
}
}
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
        }
    }
}
@propertyWrapper
结构主题绑定{
私人租赁主体:CurrentValueSubject
init(wrappedValue:Value){
主题=当前值主题(wrappedValue)
}
func anyPublisher()->anyPublisher{
返回subject.eraseToAnyPublisher()
}
var wrappedValue:Value{
得到{
返回subject.value
}
设置{
subject.value=newValue
}
}
var projectedValue:绑定{
返回绑定(在中获取:{()->值
返回自我主体价值
}){(值)在
self.subject.value=值
}
}
}

所以我最近也在想,既然我们没有开始在SwiftUI中编写视图,我该怎么做

我制作了一个helper对象来封装从函数调用到发布服务器的转换。我称之为接力赛

@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.publisher

如果有人开始回答,他们会重复官方文件,所以只需从头开始。简而言之,使用
observateObject
-它是本机的、简单的,并且自动与SwiftUI集成。检查一些MVVM Xcode模板这很好!通过在视图中公开didTapLogin:Relay,帮助我将UViewController(UIHostingController的子类)绑定到SwiftUI视图的按钮点击