[RxSwift]7.1、RxSwift 常用架構:MVVM


★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
➤微信公眾號:山青詠芝(let_us_code)
➤博主域名:https://www.zengqiang.org
➤GitHub地址:https://github.com/strengthen/LeetCode
➤原文地址:https://www.cnblogs.com/strengthen/p/13581040.html
➤如果鏈接不是山青詠芝的博客園地址,則可能是爬取作者的文章。
➤原文已修改更新!強烈建議點擊原文地址閱讀!支持作者!支持原創!
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★

熱烈歡迎,請直接點擊!!!

進入博主App Store主頁,下載使用各個作品!!!

注:博主將堅持每月上線一個新app!!!

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 版的演示代碼,有興趣的同學可以了解一下。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM