注意:这篇文章假设你有RxSwift的基础而且对MVVM有大致的理解。若是你没有,网上有数不清的入门教程先了解一下。ios
另注:我最近很关注Brandon Williams和Stephen Celis提出的Point-Free若是你尚未看过能够看一下。他们提出的内容对于Swift开发者来讲绝对是必不可少的,订阅它们3多是您今年最有价值的投资。git
求大佬们点个关注,会按期写原创和翻译国外最新文章,跟大佬们一块儿学习进步,有问题或者建议欢迎加微信ruiwendelll,拉你们进技术交流群,一块儿探讨学习,谢谢了!github
关于如何结合RxSwift和MVVM已经有了太多文章和讨论。在Grailed,咱们一直热衷于与社区一块儿创新,并有动力改进咱们的代码并创造更好的代码,为咱们的客户提供更可靠的产品。基于这个目标,咱们已经一直在使用一种MVVM的模式,它寻求函数式编程和RxSwift,以提供可靠性,可测试性和稳定性。咱们喜好它的不少房买你,可是咱们遇到了不少MVVM开发者很熟悉的一系列问题。编程
有时候如何组建你的代码会变得很不清晰。在网上的几十种MVVM架构变种中,彷佛每个对View层如何与ViewModel进行交互都不相同。缺少明确的模式可能会使MVVm中的开发感到特殊和不一致,这将致使可维护性问题。swift
ViewModel可能会变得难以置信的啰嗦。这都是因为MVVM中结构的缺少,有些选择将更明确的输入输出合约写在ViewModel里面,但根据传统这是以大量样板为代价来实现的。设计模式
同时,有些版本不阻止ViewModel的消费者错误使用它们的API。从你的View层订阅ViewModel的输入众所周知是违反设计模式的,可是我看到不一样的生产代码库都存在这一现象。理想状况下,编译器会防止咱们犯这个错误。安全
因为Swift类和结构体的初始化规则ViewModel可能很难去设置。好比,当你不得不在你的类或者结构体中设置输出Observable为属性时,那些输出取决于Subject的输入。你有时候会遇到你不能引用self直到属性初始化完成以前的状况,可是你不呢个初始化你的属性,由于你须要引用self。bash
忘记将你的ViewModel的输入和输出绑定而编译器也不会在咱们出错的时候帮助咱们也是很常见的。在它帮助咱们不让咱们犯这么愚蠢的错误时,编译是是咱们的朋友。微信
如今咱们已经了解了使用RxSwift进行MVVM模式编程的一些难点。让咱们经过用一种流行的MVVM方式写的简单例子来看看咱们如何能够作得更好。网络
全部开发人员都知道编写纯函数能够是代码更具备可测试性和理解性,不然会变得难以理解,咱们要实现它若是不是不可能。问题是咱们写的太多类型的代码不能彻底适合纯函数,所以咱们独立将代码尽量多的部分建模成为纯函数。这种看法促使不少MVVM开发人员建立他们的ViewModel,以便拥有一组明确的输入和输出,从而咱们可能将它们看做更相似于函数的东西。
函数的输入叫作Subject,输出是callback,变量或者Observable。View的业务逻辑在ViewModel中被建模为输入和输出 的转换,多数状况下在ViewModel的初始化器中。Kickstarter open source app多是第一个也是最著名的迭代。即便它使用RactiveSwift写的,可是这些想法很是类似。这些开源的应用程序打开了许多开发人员的眼界,关于如何使用MVVM来实现APP的可测试性和稳定性。
首先让咱们来建立一个经典的RxSwift例子,一个简单的具备姓名和密码输入框的登录界面,固然还有一个登录按钮。咱们想让姓名和密码输入框都被填充的时候登录按钮才可用,当用户点击按钮时咱们但愿弹出相似登录成功的提示(咱们将在这里进行硬编码,并在之后的文章中讨论如何处理网络请求)。这是一个咱们遵循Kickstarter 应用程序中列出的模式编写此代码的示例。
// ************** View Model **************
protocol LoginViewModelInputs {
func usernameChanged(_ username: String)
func passwordChanged(_ password: String)
func loginTapped()
}
protocol LoginViewModelOutputs {
var loginButtonEnabled: Observable<Bool> { get }
var showSuccessMessage: Observable<String> { get }
}
protocol LoginViewModelType {
var inputs: LoginViewModelInputs { get }
var outputs: LoginViewModelOutputs { get }
}
class LoginViewModel: LoginViewModelInputs, LoginViewModelOutputs, LoginViewModelType {
let loginButtonEnabled: Observable<Bool>
let showSuccessMessage: Observable<String>
init() {
self.loginButtonEnabled = Observable.combineLatest(
_usernameChanged,
_passwordChanged
) { username, password in
!username.isEmpty && !password.isEmpty
}
self.showSuccessMessage = _loginTapped
.map { "Login Successful" }
}
private let _usernameChanged = PublishRelay<String>()
func usernameChanged(_ username: String) {
_usernameChanged.accept(username)
}
private let _passwordChanged = PublishRelay<String>()
func passwordChanged(_ password: String) {
_passwordChanged.accept(password)
}
private let _loginTapped = PublishRelay<Void>()
func loginTapped() {
_loginTapped.accept(())
}
var inputs: LoginViewModelInputs { return self }
var outputs: LoginViewModelOutputs { return self }
}
// ************** View Controller **************
class LoginViewController: UIViewController {
private let usernameTextField = UITextField()
private let passwordTextField = UITextField()
private let loginButton = UIButton()
private let viewModel: LoginViewModelType
private let disposeBag = DisposeBag()
init() {
self.viewModel = LoginViewModel()
super.init(nibName: nil, bundle: nil)
disposeBag.insert(
// Inputs
loginButton.rx.tap.asObservable()
.subscribe(onNext: viewModel.inputs.loginTapped),
usernameTextField.rx.text.orEmpty
.subscribe(onNext: viewModel.inputs.usernameChanged),
passwordTextField.rx.text.orEmpty
.subscribe(onNext: viewModel.inputs.passwordChanged),
// Outputs
viewModel.outputs.loginButtonEnabled
.bind(to: loginButton.rx.isEnabled),
viewModel.outputs.showSuccessMessage
.subscribe(onNext: { message in
print(message)
// Show some alert here
})
)
}
}
复制代码
对于如此小的屏幕,这里有不少内容,因此花一点时间来消化它。我真的很喜欢这种风格:
总的来讲,我认为这种权衡取舍是值得的。咱们在一个小的额外样板上花了点时间并在咱们的ViewModel中有些杂乱,从而获得了一个很清楚的关于ViweModel能的功能,它很是明确而且容易推理。这种易于推理使咱们可以将ViewModel视为纯函数的抽象形式,咱们传递输入和输出。
由于这些输入一般是用户操做,而输出是反作用。这对于你为一系列用户行为并确保它们产生的反作用能像你指望的那用触发而写出更高级的测试用例有好处。这容许您编写更普遍的高级“功能”测试集,测试应用程序除了更传统的单元测试以外用户将如何使用它。
如今我怕们提高了代码的可测试性,并为咱们的ViewModel提供了安全且容易理解的API,可是咱们还有不少想作的。咱们是否能够消除这些样板而不用牺牲明确性和安全性。
答案是yes!为了获得这个解决方案,回过头去看一下第一原则是值得的。咱们但愿想对待纯函数同样对待咱们的ViewModel。咱们但愿有明确的输入和输出来方便测试。Swift中有一个简单又没什么代价的方式去接收输入返回输出:functions。因此咱们可使用函数做为咱们的ViewModel而不是一个类或结构体。想一想咱们应该怎么作。
不是一个LoginViewModelInputs代理,咱们能够把输入做为一个函数的参数进行传递。一样,不用LoginViewModelOutputs代理,咱们能够从函数获得返回的输出Observable。这听起来很奇怪,让咱们来看下例子。
// ************** View Model **************
func loginViewModel(
usernameChanged: Observable<String>,
passwordChanged: Observable<String>,
loginTapped: Observable<Void>
) -> (
loginButtonEnabled: Observable<Bool>,
showSuccessMessage: Observable<String>
) {
let loginButtonEnabled = Observable.combineLatest(
usernameChanged,
passwordChanged
) { username, password in
!username.isEmpty && !password.isEmpty
}
let showSuccessMessage = loginTapped
.map { "Login Successful!" }
return (
loginButtonEnabled,
showSuccessMessage
)
}
// ************** View Controller **************
class LoginViewController: UIViewController {
private let usernameTextField = UITextField()
private let passwordTextField = UITextField()
private let loginButton = UIButton()
private let disposeBag = DisposeBag()
init() {
super.init(nibName: nil, bundle: nil)
let (
loginButtonEnabled,
showSuccessMessage
) = loginViewModel(
usernameChanged: usernameTextField.rx.text.orEmpty.asObservable(),
passwordChanged: passwordTextField.rx.text.orEmpty.asObservable(),
loginTapped: loginButton.rx.tap.asObservable()
)
disposeBag.insert(
loginButtonEnabled
.bind(to: loginButton.rx.isEnabled),
showSuccessMessage
.subscribe(onNext: { message in
print(message)
// Show some alert here
})
)
}
}
复制代码
咱们的ViewModel神奇的转换成了函数,并且ViewController不用再调LoginViewModel.init()
,咱们调用loginViewModel(usernameChanged:passwordChanged:loginTapped:)
。咱们看看作了什么修改。
一旦咱们克服了不适用对象的陌生感。基本上对于每种可衡量的方式这都是一个巨大的进步。在咱们犯错时让编译器提醒咱们是一个重大的进步,而且还消除了大部分模板代码。
把ViewModel建模成为函数一开始看起来可能很疯狂,可是咱们在应用程序中始终将业务逻辑做为函数进行编写,若是不是一大块业务逻辑,还有什么是ViewModel呢?当我抛弃对ViewModel的传统观点并拥抱新朋友--函数时,咱们为编写反应式MVVM的许多困境发现了一个简单而优雅的解决方案。
经过代理实现的这一编码风格来自于Kickstarter pagination的逻辑,已经在生产中存在了三年多,而且已经在其代码库的近几十个地方重复使用。像上面示例所展现的同样,这种方法不是特定域ViewModel,而是在大规模生产代码库中通过实战测试,而不只仅是像咱们的登录界面这样的demo
Swift和其余开发社区都都存在一种共同模式,即建立一个封装一些数据并提供该数据访问器的对象。不管什么时候咱们东可使用相同的转换,像把ViewModel转换为函数同样。