Я использую следующий фрагмент кода на основе UIViewController
и RxSwift/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)
}
}
Он хорошо компилируется и работает. Однако мне нужно Combin
изменить этот шаблон с помощью 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]: считается ли sink
использование издателя в SwiftUI
просмотре плохой практикой? что может потребовать некоторого обходного пути для сохранения cancellable
в области видимости структуры View
?
[B]: какой из них лучше для SwiftUI/Combine
проектов с точки зрения архитектурного паттерна MVVM: использование ViewModel с паттерном [Input [Subscribers], Output [AnyPublishers]] или ObservableObject
ViewModel с [@Published
properties]?
ObservableObject
- это нативный, простой и автоматически интегрируется со SwiftUI. - person Asperi   schedule 22.01.2020