★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
➤微信公眾號:山青詠芝(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 版的演示代碼,有興趣的同學可以了解一下。