| Swift , iOS , MVVM , Rx , 架构
ViewModel
需要具备以下特性:
MVVM
模式会更加强大,所以 ViewModel
要充分利用 RxSwift
;把 ViewModel
当做黑箱,它可以接收输入,并产生输出,这就是定义 ViewModel
最好的原则。
本文将提供两种可行的方案,希望能够带给您一些启发~
定义 ViewModelType
协议
protocol ViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}
这种方案简单易行,只需要 一次性提供Input
给 ViewModel
,然后 ViewModel
即可给出 Output
。
让我们创建示例Demo:
输入内容,然后点击Validate按钮。最后,显示校验结果。
创建 SayHelloViewModel,它需要知道输入的文本以及按钮点击事件,这就是Input。
然后 Output 是文本内容。
final class SayHelloViewModel: ViewModelType {
struct Input {
let name: Observable<String>
let validate: Observable<Void>
}
struct Output {
let greeting: Driver<String>
}
func transform(input: Input) -> Output {
let greeting = input.validate
.withLatestFrom(input.name)
.map { name in
return "Hello \(name)!"
}
.startWith("")
.asDriver(onErrorJustReturn: ":-(")
return Output(greeting: greeting)
}
}
创建SayHelloViewController:
final class SayHelloViewController: UIViewController {
@IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var validateButton: UIButton!
@IBOutlet weak var greetingLabel: UILabel!
private let viewModel = SayHelloViewModel()
private let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
}
private func bindViewModel() {
// 构建输入
let inputs = SayHelloViewModel.Input(name: nameTextField.rx.text.orEmpty.asObservable(),
validate: validateButton.rx.tap.asObservable())
// 根据输入构建输出
let outputs = viewModel.transform(input: inputs)
// 绑定输出和UI控件的属性
outputs.greeting
.drive(greetingLabel.rx.text)
.disposed(by: bag)
}
}
ViewModel
应该是可插拔的,那么我们可以把之前定义的 ViewModel
用于 其他 View
吗?
现在,如果我们尝试将之前的 ViewModel
用于带有 TableView 的 View,会发生什么事情?
/// TableViewCells
final class TextFieldCell: UITableViewCell {
@IBOutlet weak var nameTextField: UITextField!
}
final class ButtonCell: UITableViewCell {
@IBOutlet weak var validateButton: UIButton!
}
final class GreetingCell: UITableViewCell {
@IBOutlet weak var greetingLabel: UILabel!
}
/// ViewController
final class SayHelloViewController: UIViewController, UITableViewDataSource {
static let cellIdentifiers = [
"TextFieldCell",
"ButtonCell",
"GreetingCell"
]
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return TableViewController.cellIdentifiers.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Classic dequeue work
}
private let viewModel = SayHelloViewModel()
private let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
}
private func bindViewModel() {
// Let's discuss about this
let inputs = SayHelloViewModel.Input(name: 😱😱, validate: 😱😱)
}
}
然而,我们甚至无法为ViewModel提供Input。
因为我们不能在创建 ViewModel
时就获取到 UITableView
的内容。
所以,使用这种方案有一个前提条件:在创建 ViewModel
的 Input
时,可以获得全部所需的资源。
这时,你就需要采用第二种方案了!
定义 ViewModelType
协议:
protocol ViewModelType {
associatedtype Input
associatedtype Output
var input: Input { get }
var output: Output { get }
}
这样,我们就可以完全自由地选择何时提供输入、何时订阅输出了。
Subject
可以同时充当Observer
和Observable
,把命令式的编程变为 Rx 的函数响应式编程。
定义采用 Subject
的ViewModel:
final class SayHelloViewModel: ViewModelType {
let input: Input
let output: Output
struct Input {
let name: AnyObserver<String>
let validate: AnyObserver<Void>
}
struct Output {
let greeting: Driver<String>
}
private let nameSubject = ReplaySubject<String>.create(bufferSize: 1)
private let validateSubject = PublishSubject<Void>()
init() {
let greeting = validateSubject
.withLatestFrom(nameSubject)
.map { name in
return "Hello \(name)!"
}
.asDriver(onErrorJustReturn: ":-(")
// 外部无法更改输入与输出,而且 AnyObserver 类型只能用于输入,Driver 类型只能用于输出
self.output = Output(greeting: greeting)
self.input = Input(name: nameSubject.asObserver(), validate: validateSubject.asObserver())
}
}
这里有几点值得注意的内容:
ViewModel
的任务还是输入 Input
产出 Output
;Subjects
是 private
的,所以你只能通过 input 和 output 属性与 ViewModel
交互;可插拔
、可测试
的特性,并且充分利用了 RxSwift的绑定机制
;
View部分的实现:
/// 每个和 SayHelloViewModel 进行交互的 View 都需要遵循这个协议
protocol SayHelloViewModelBindable {
var disposeBag: DisposeBag? { get }
func bind(to viewModel: SayHelloViewModel)
}
final class TextFieldCell: UITableViewCell, SayHelloViewModelBindable {
@IBOutlet weak var nameTextField: UITextField!
var disposeBag: DisposeBag?
override func prepareForReuse() {
super.prepareForReuse()
// 注意释放 Rx 订阅
disposeBag = nil
}
func bind(to viewModel: SayHelloViewModel) {
let bag = DisposeBag()
nameTextField.rx
.text
.orEmpty
.bind(to: viewModel.input.name) // 动态地将输入框的内容绑定到 viewModel 的输入上
.disposed(by: bag)
// 更新绑定时,需要将旧的绑定废弃。旧的 disposeBag 引用计数为 0 时,旧的订阅即可被释放
disposeBag = bag
}
}
final class ButtonCell: UITableViewCell, SayHelloViewModelBindable {
@IBOutlet weak var validateButton: UIButton!
var disposeBag: DisposeBag?
override func prepareForReuse() {
super.prepareForReuse()
// 同上
disposeBag = nil
}
func bind(to viewModel: SayHelloViewModel) {
let bag = DisposeBag()
validateButton.rx
.tap
.bind(to: viewModel.input.validate) // 同上
.disposed(by: bag)
// 同上
disposeBag = bag
}
}
final class GreetingCell: UITableViewCell, SayHelloViewModelBindable {
@IBOutlet weak var greetingLabel: UILabel!
var disposeBag: DisposeBag?
override func prepareForReuse() {
super.prepareForReuse()
disposeBag = nil
}
func bind(to viewModel: SayHelloViewModel) {
let bag = DisposeBag()
viewModel.output.greeting
.drive(greetingLabel.rx.text) // 动态地将 viewModel 的 ouput 绑定到 label 上
.disposed(by: bag)
disposeBag = bag
}
}
final class TableViewController: UIViewController, UITableViewDataSource {
static let cellIdentifiers = [
"TextFieldCell",
"ButtonCell",
"GreetingCell"
]
@IBOutlet weak var tableView: UITableView!
private let viewModel = SayHelloViewModel()
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return TableViewController.cellIdentifiers.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: TableViewController.cellIdentifiers[indexPath.row])
(cell as? SayHelloViewModelBindable)?.bind(to: viewModel)
return cell!
}
}
你需要根据自己的需要来决定采用哪一种方案。
第一种方案 简单易行
,但是有一定的 局限性
。
第二种方案 兼容性强
,但是定义及使用都略显 繁琐
。
参考内容:
RxSwift + MVVM: how to feed ViewModels
觉得不错?点个赞呗~
本文链接:RxSwift + MVVM 如何构建 ViewModel ?
转载声明:本站文章如无特别说明,皆为原创。转载请注明:Ficow Shen's Blog