ViewModel需要具備以下特性:
- 可插拔;
- 可測(cè)試出爹;
- 采用綁定機(jī)制的MVVM模式會(huì)更加強(qiáng)大庄吼,所以ViewModel要充分利用RxSwift缎除;
把ViewModel當(dāng)做黑箱,它可以接收輸入总寻,并產(chǎn)生輸出器罐,這就是定義ViewModel最好的原則。
方案一 (不采用Subjects
)
定義ViewModelType協(xié)議
protocol ViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}
這種方案簡(jiǎn)單易行渐行,只需要一次性提供Input給ViewModel轰坊,然后ViewModel即可給出Output。
讓我們創(chuàng)建示例Demo:
輸入內(nèi)容殊轴,然后點(diǎn)擊Validate按鈕衰倦。最后袒炉,顯示校驗(yàn)結(jié)果旁理。
創(chuàng)建SayHelloViewModel,它需要知道輸入的文本以及按鈕點(diǎn)擊事件我磁,這就是Input孽文。
然后Output是文本內(nèi)容。
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)
}
}
創(chuàng)建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應(yīng)該是可插拔的夺艰,那么我們可以把之前定義的ViewModel用于其他View嗎芋哭?
現(xiàn)在,如果我們嘗試將之前的ViewModel用于帶有TableView的View郁副,會(huì)發(fā)生什么事情减牺?
/// 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: ????)
}
}
然而,我們甚至無(wú)法為ViewModel提供Input存谎。
因?yàn)槲覀儾荒茉趧?chuàng)建ViewModel時(shí)就獲取到UITableView的內(nèi)容拔疚。
所以,使用這種方案有一個(gè)前提條件:在創(chuàng)建ViewModel的Input時(shí)既荚,可以獲得全部所需的資源稚失。
這時(shí),你就需要采用第二種方案了恰聘!
第二種方案 (采用Subjects
)
定義ViewModelType協(xié)議:
protocol ViewModelType {
associatedtype Input
associatedtype Output
var input: Input { get }
var output: Output { get }
}
這樣句各,我們就可以完全自由地選擇何時(shí)提供輸入、何時(shí)訂閱輸出了晴叨。
Subject
可以同時(shí)充當(dāng)Observer
和Observable
凿宾,把命令式的編程變?yōu)镽x的函數(shù)式編程。
定義采用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())
}
}
這里有幾點(diǎn)值得注意的內(nèi)容:
- ViewModel的任務(wù)還是輸入Input產(chǎn)出Output兼蕊;
-
Subjects
是private
的初厚,所以你只能通過(guò)input和output屬性與ViewModel交互; - 兼具可插拔遍略、可測(cè)試的特性惧所,并且充分利用了RxSwift的綁定機(jī)制骤坐;
View部分的實(shí)現(xiàn):
/// 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!
}
}
你需要根據(jù)自己的需要來(lái)決定采用哪一種方案。
第一種方案簡(jiǎn)單易行下愈,但是有一定的局限性纽绍。
第二種方案兼容性強(qiáng),但是定義及使用都略顯繁瑣势似。
參考文章:
RxSwift + MVVM: how to feed ViewModels
轉(zhuǎn)載請(qǐng)注明出處拌夏,謝謝~