[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
➤如果链接不是山青咏芝的博客园地址,则可能是爬取作者的文章。
➤原文已修改更新!强烈建议点击原文地址阅读!支持作者!支持原创!
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
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 版的演示代码,有兴趣的同学可以了解一下。