★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
➤微信公众号:山青咏芝(let_us_code)
➤博主域名:https://www.zengqiang.org
➤GitHub地址:https://github.com/strengthen/LeetCode
➤原文地址:https://www.cnblogs.com/strengthen/p/13581040.html
➤如果链接不是山青咏芝的博客园地址,则可能是爬取作者的文章。
➤原文已修改更新!强烈建议点击原文地址阅读!支持作者!支持原创!
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
MVVM 是 Model-View-ViewModel 的简写。如果你已经对 MVC 非常熟悉了,那么上手 MVVM 也是非常容易的。
MVC
MVC 是 Model-View-Controller 的简写。MVC 主要有三层:
- Model 数据层,读写数据,保存 App 状态
- View 页面层,和用户交互,向用户显示页面,反馈用户行为
- ViewController 逻辑层,更新数据,或者页面,处理业务逻辑
MVC 可以帮助你很好的将数据,页面,逻辑的代码分离开来。使得每一层相对独立。这样你就能够将一些可复用的功能抽离出来,化繁为简。只不过,一旦 App 的交互变复杂,你就会发现 ViewController 将变得十分臃肿。大量代码被添加到控制器中,使得控制器负担过重。此时,你就需要想办法将控制器里面的代码进一步地分离出来,对 APP 进行重新分层。而 MVVM 就是一种进阶的分层方案。
MVVM
MVVM 和 MVC 十分相识。只不过他的分层更加详细:
- Model 数据层,读写数据,保存 App 状态
- View 页面层,提供用户输入行为,并且显示输出状态
- ViewModel 逻辑层,它将用户输入行为,转换成输出状态
- ViewController 主要负责数据绑定
没错,ViewModel 现在是逻辑层,而控制器只需要负责数据绑定。如此一来控制器的负担就减轻了许多。并且 ViewModel 与控制器以及页面相独立。那么,你就可以跨平台使用它。你也可以很容易地测试它。
示例
这里我们将用 MVVM 来重构输入验证。
重构前:
class SimpleValidationViewController : ViewController { ... override func viewDidLoad() { super.viewDidLoad() ... let usernameValid = usernameOutlet.rx.text.orEmpty .map { $0.characters.count >= minimalUsernameLength } .share(replay: 1) let passwordValid = passwordOutlet.rx.text.orEmpty .map { $0.characters.count >= minimalPasswordLength } .share(replay: 1) let everythingValid = Observable.combineLatest( usernameValid, passwordValid ) { $0 && $1 } .share(replay: 1) usernameValid .bind(to: passwordOutlet.rx.isEnabled) .disposed(by: disposeBag) usernameValid .bind(to: usernameValidOutlet.rx.isHidden) .disposed(by: disposeBag) passwordValid .bind(to: passwordValidOutlet.rx.isHidden) .disposed(by: disposeBag) everythingValid .bind(to: doSomethingOutlet.rx.isEnabled) .disposed(by: disposeBag) doSomethingOutlet.rx.tap .subscribe(onNext: { [weak self] in self?.showAlert() }) .disposed(by: disposeBag) } ... }
ViewModel
ViewModel 将用户输入行为,转换成输出的状态:
class SimpleValidationViewModel { // 输出 let usernameValid: Observable<Bool> let passwordValid: Observable<Bool> let everythingValid: Observable<Bool> // 输入 -> 输出 init( username: Observable<String>, password: Observable<String> ) { usernameValid = username .map { $0.characters.count >= minimalUsernameLength } .share(replay: 1) passwordValid = password .map { $0.characters.count >= minimalPasswordLength } .share(replay: 1) everythingValid = Observable.combineLatest(usernameValid, passwordValid) { $0 && $1 } .share(replay: 1) } }
输入:
- username 输入的用户名
- password 输入的密码
输出:
- usernameValid 用户名是否有效
- passwordValid 密码是否有效
- everythingValid 所有输入是否有效
在 init
方法内部,将输入转换为输出。
ViewController
ViewController 主要负责数据绑定:
class SimpleValidationViewController : ViewController { ... private var viewModel: SimpleValidationViewModel! override func viewDidLoad() { super.viewDidLoad() ... viewModel = SimpleValidationViewModel( username: usernameOutlet.rx.text.orEmpty.asObservable(), password: passwordOutlet.rx.text.orEmpty.asObservable() ) viewModel.usernameValid .bind(to: passwordOutlet.rx.isEnabled) .disposed(by: disposeBag) viewModel.usernameValid .bind(to: usernameValidOutlet.rx.isHidden) .disposed(by: disposeBag) viewModel.passwordValid .bind(to: passwordValidOutlet.rx.isHidden) .disposed(by: disposeBag) viewModel.everythingValid .bind(to: doSomethingOutlet.rx.isEnabled) .disposed(by: disposeBag) doSomethingOutlet.rx.tap .subscribe(onNext: { [weak self] in self?.showAlert() }) .disposed(by: disposeBag) } ... }
输入:
- username 将输入的用户名传入 ViewModel
- password 将输入的密码传入 ViewModel
输出:
- usernameValid 用用户名是否有效,来控制提示语是否隐藏,密码输入框是否可用
- passwordValid 用密码是否有效,来控制提示语是否隐藏
- everythingValid 用两者是否同时有效,来控制按钮是否可点击
当 App 的交互变复杂时,你仍然可以保持控制器结构清晰。这样可以大大的提升代码可读性。将来代码维护起来也就会容易许多了。
示例
本节将用 Github Signup 来演示如何使用 MVVM。
注意⚠️:这里介绍的 MVVM 并不是严格意义上的 MVVM。但我们通常都管它叫 MVVM,而且它配合 RxSwift 使用起来非常方便。如需了解什么是严格意义上的 MVVM,请参考微软的 The MVVM Pattern。
Github Signup
这是一个模拟用户注册的程序,你可以在这里下载这个例子。
简介
这个 App 主要有这样几个交互:
- 当用户输入户名时,验证用户名是否有效,是否已被占用,将验证结果显示出来。
- 当用户输入密码时,验证密码是否有效,将验证结果显示出来。
- 当用户输入重复密码时,验证重复密码是否相同,将验证结果显示出来。
- 当所有验证都有效时,注册按钮才可点击。
- 当点击注册按钮后发起注册请求(模拟),然后将结果显示出来。
Service
// GitHub 网络服务 protocol GitHubAPI { func usernameAvailable(_ username: String) -> Observable<Bool> func signup(_ username: String, password: String) -> Observable<Bool> } // 输入验证服务 protocol GitHubValidationService { func validateUsername(_ username: String) -> Observable<ValidationResult> func validatePassword(_ password: String) -> ValidationResult func validateRepeatedPassword(_ password: String, repeatedPassword: String) -> ValidationResult } // 弹框服务 protocol Wireframe { func open(url: URL) func promptFor<Action: CustomStringConvertible>(_ message: String, cancelAction: Action, actions: [Action]) -> Observable<Action> }
这里需要集成三个服务:
- GitHubAPI 提供 GitHub 网络服务
- GitHubValidationService 提供输入验证服务
- Wireframe 提供弹框服务
这个例子目前只提供了这三个服务,实际上这一层还可以包含其他的一些服务,例如:数据库,定位,蓝牙...
ViewModel
ViewModel 需要集成这些服务,并且将用户输入,转换为状态输出:
class GithubSignupViewModel1 { // 输出 let validatedUsername: Observable<ValidationResult> let validatedPassword: Observable<ValidationResult> let validatedPasswordRepeated: Observable<ValidationResult> let signupEnabled: Observable<Bool> let signedIn: Observable<Bool> let signingIn: Observable<Bool> // 输入 -> 输出 init(input: ( // 输入 username: Observable<String>, password: Observable<String>, repeatedPassword: Observable<String>, loginTaps: Observable<Void> ), dependency: ( // 服务 API: GitHubAPI, validationService: GitHubValidationService, wireframe: Wireframe ) ) { ... validatedUsername = ... validatedPassword = ... validatedPasswordRepeated = ... ... self.signingIn = ... ... signedIn = ... signupEnabled = ... } }
集成服务:
- API GitHub 网络服务
- validationService 输入验证服务
- wireframe 弹框服务
输入:
- username 输入的用户名
- password 输入的密码
- repeatedPassword 重复输入的密码
- loginTaps 点击登录按钮
输出:
- validatedUsername 用户名校验结果
- validatedPassword 密码校验结果
- validatedPasswordRepeated 重复密码校验结果
- signupEnabled 是否允许登录
- signedIn 登录结果
- signingIn 是否正在登录
在 init
方法内部,将输入转换为输出。
ViewController
ViewController 主要负责数据绑定:
...
class GitHubSignupViewController1 : ViewController { @IBOutlet weak var usernameOutlet: UITextField! @IBOutlet weak var usernameValidationOutlet: UILabel! @IBOutlet weak var passwordOutlet: UITextField! @IBOutlet weak var passwordValidationOutlet: UILabel! @IBOutlet weak var repeatedPasswordOutlet: UITextField! @IBOutlet weak var repeatedPasswordValidationOutlet: UILabel! @IBOutlet weak var signupOutlet: UIButton! @IBOutlet weak var signingUpOulet: UIActivityIndicatorView! override func viewDidLoad() { super.viewDidLoad() let viewModel = GithubSignupViewModel1( input: ( username: usernameOutlet.rx.text.orEmpty.asObservable(), password: passwordOutlet.rx.text.orEmpty.asObservable(), repeatedPassword: repeatedPasswordOutlet.rx.text.orEmpty.asObservable(), loginTaps: signupOutlet.rx.tap.asObservable() ), dependency: ( API: GitHubDefaultAPI.sharedAPI, validationService: GitHubDefaultValidationService.sharedValidationService, wireframe: DefaultWireframe.shared ) ) // bind results to { viewModel.signupEnabled .subscribe(onNext: { [weak self] valid in self?.signupOutlet.isEnabled = valid self?.signupOutlet.alpha = valid ? 1.0 : 0.5 }) .disposed(by: disposeBag) viewModel.validatedUsername .bind(to: usernameValidationOutlet.rx.validationResult) .disposed(by: disposeBag) viewModel.validatedPassword .bind(to: passwordValidationOutlet.rx.validationResult) .disposed(by: disposeBag) viewModel.validatedPasswordRepeated .bind(to: repeatedPasswordValidationOutlet.rx.validationResult) .disposed(by: disposeBag) viewModel.signingIn .bind(to: signingUpOulet.rx.isAnimating) .disposed(by: disposeBag) viewModel.signedIn .subscribe(onNext: { signedIn in print("User signed in \(signedIn)") }) .disposed(by: disposeBag) //} let tapBackground = UITapGestureRecognizer() tapBackground.rx.event .subscribe(onNext: { [weak self] _ in self?.view.endEditing(true) }) .disposed(by: disposeBag) view.addGestureRecognizer(tapBackground) } }
将用户行为传入给 ViewModel:
- username 将用户名输入框的当前文本传入
- password 将密码输入框的当前文本传入
- ...
将 ViewModel 的输出状态显示出来:
- validatedUsername 用对应的
label
将用户名验证结果显示出来 - validatedPassword 用对应的
label
将密码验证结果显示出来 - ...
整体结构
以下是全部的核心代码:
// ViewModel class GithubSignupViewModel1 { // outputs { let validatedUsername: Observable<ValidationResult> let validatedPassword: Observable<ValidationResult> let validatedPasswordRepeated: Observable<ValidationResult> // Is signup button enabled let signupEnabled: Observable<Bool> // Has user signed in let signedIn: Observable<Bool> // Is signing process in progress let signingIn: Observable<Bool> // } init(input: ( username: Observable<String>, password: Observable<String>, repeatedPassword: Observable<String>, loginTaps: Observable<Void> ), dependency: ( API: GitHubAPI, validationService: GitHubValidationService, wireframe: Wireframe ) ) { let API = dependency.API let validationService = dependency.validationService let wireframe = dependency.wireframe /** Notice how no subscribe call is being made. Everything is just a definition. Pure transformation of input sequences to output sequences. */ validatedUsername = input.username .flatMapLatest { username in return validationService.validateUsername(username) .observeOn(MainScheduler.instance) .catchErrorJustReturn(.failed(message: "Error contacting server")) } .share(replay: 1) validatedPassword = input.password .map { password in return validationService.validatePassword(password) } .share(replay: 1) validatedPasswordRepeated = Observable.combineLatest(input.password, input.repeatedPassword, resultSelector: validationService.validateRepeatedPassword) .share(replay: 1) let signingIn = ActivityIndicator() self.signingIn = signingIn.asObservable() let usernameAndPassword = Observable.combineLatest(input.username, input.password) { ($0, $1) } signedIn = input.loginTaps.withLatestFrom(usernameAndPassword) .flatMapLatest { (username, password) in return API.signup(username, password: password) .observeOn(MainScheduler.instance) .catchErrorJustReturn(false) .trackActivity(signingIn) } .flatMapLatest { loggedIn -> Observable<Bool> in let message = loggedIn ? "Mock: Signed in to GitHub." : "Mock: Sign in to GitHub failed" return wireframe.promptFor(message, cancelAction: "OK", actions: []) // propagate original value .map { _ in loggedIn } } .share(replay: 1) signupEnabled = Observable.combineLatest( validatedUsername, validatedPassword, validatedPasswordRepeated, signingIn.asObservable() ) { username, password, repeatPassword, signingIn in username.isValid && password.isValid && repeatPassword.isValid && !signingIn } .distinctUntilChanged() .share(replay: 1) } } // ViewController class GitHubSignupViewController1 : ViewController { @IBOutlet weak var usernameOutlet: UITextField! @IBOutlet weak var usernameValidationOutlet: UILabel! @IBOutlet weak var passwordOutlet: UITextField! @IBOutlet weak var passwordValidationOutlet: UILabel! @IBOutlet weak var repeatedPasswordOutlet: UITextField! @IBOutlet weak var repeatedPasswordValidationOutlet: UILabel! @IBOutlet weak var signupOutlet: UIButton! @IBOutlet weak var signingUpOulet: UIActivityIndicatorView! override func viewDidLoad() { super.viewDidLoad() let viewModel = GithubSignupViewModel1( input: ( username: usernameOutlet.rx.text.orEmpty.asObservable(), password: passwordOutlet.rx.text.orEmpty.asObservable(), repeatedPassword: repeatedPasswordOutlet.rx.text.orEmpty.asObservable(), loginTaps: signupOutlet.rx.tap.asObservable() ), dependency: ( API: GitHubDefaultAPI.sharedAPI, validationService: GitHubDefaultValidationService.sharedValidationService, wireframe: DefaultWireframe.shared ) ) // bind results to { viewModel.signupEnabled .subscribe(onNext: { [weak self] valid in self?.signupOutlet.isEnabled = valid self?.signupOutlet.alpha = valid ? 1.0 : 0.5 }) .disposed(by: disposeBag) viewModel.validatedUsername .bind(to: usernameValidationOutlet.rx.validationResult) .disposed(by: disposeBag) viewModel.validatedPassword .bind(to: passwordValidationOutlet.rx.validationResult) .disposed(by: disposeBag) viewModel.validatedPasswordRepeated .bind(to: repeatedPasswordValidationOutlet.rx.validationResult) .disposed(by: disposeBag) viewModel.signingIn .bind(to: signingUpOulet.rx.isAnimating) .disposed(by: disposeBag) viewModel.signedIn .subscribe(onNext: { signedIn in print("User signed in \(signedIn)") }) .disposed(by: disposeBag) //} let tapBackground = UITapGestureRecognizer() tapBackground.rx.event .subscribe(onNext: { [weak self] _ in self?.view.endEditing(true) }) .disposed(by: disposeBag) view.addGestureRecognizer(tapBackground) } }
这里还有一个 Driver 版的演示代码,有兴趣的同学可以了解一下。