ViewModel需要具备以下特性:
- 可插拔;
- 可测试;
- 采用绑定机制的MVVM模式会更加强大,所以ViewModel要充分利用RxSwift;
把ViewModel当做黑箱,它可以接收输入,并产生输出,这就是定义ViewModel最好的原则。
方案一 (不采用Subjects
)
定义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)
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时,可以获得全部所需的资源。
这时,你就需要采用第二种方案了!
第二种方案 (采用Subjects
)
定义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: ":-(")
self.output = Output(greeting: greeting)
self.input = Input(name: nameSubject.asObserver(), validate: validateSubject.asObserver())
}
}
这里有几点值得注意的内容:
- ViewModel的任务还是输入Input产出Output;
-
Subjects
是private
的,所以你只能通过input和output属性与ViewModel交互; - 兼具可插拔、可测试的特性,并且充分利用了RxSwift的绑定机制;
View部分的实现:
/// Every view interacting with a `SayHelloViewModel` instance can conform to this.
protocol SayHelloViewModelBindable {
var disposeBag: DisposeBag? { get }
func bind(to viewModel: SayHelloViewModel)
}
/// TableViewCells
final class TextFieldCell: UITableViewCell, SayHelloViewModelBindable {
@IBOutlet weak var nameTextField: UITextField!
var disposeBag: DisposeBag?
override func prepareForReuse() {
super.prepareForReuse()
// Clean Rx subscriptions
disposeBag = nil
}
func bind(to viewModel: SayHelloViewModel) {
let bag = DisposeBag()
nameTextField.rx
.text
.orEmpty
.bind(to: viewModel.input.name)
.disposed(by: bag)
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)
.disposed(by: bag)
disposeBag = bag
}
}
/// View
class TableViewController: UIViewController, UITableViewDataSource {
static let cellIdentifiers = [
"TextFieldCell",
"ButtonCell",
"GreetingCell"
]
@IBOutlet weak var tableView: UITableView!
private let viewModel = SayHelloViewModel()
private let bag = DisposeBag()
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
转载请注明出处,谢谢~
网友评论