[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