一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第27天,点击查看活动详情。
- 本文主要介绍
RxSwift中mvvm的使用,这里以登录为例,界面如下:
这里我们的viewmodel的思路就是输入和输出的操作,我们的逻辑操作都在ViewModel中
protocol ViewModelType {
associatedtype Input
associatedtype Output
func transform(input:Input) -> Output
}
class ViewModel: NSObject {
let disposeBag = DisposeBag()
}
我们定义一个协议,这里关联一个Input和Output类,以及一个transform方法,和我们RXSwift基类一样,明确基类的作用。
1. UI界面元素操作绑定
input我们看下我们的输入,对于上面的界面主要是4个输入
/// 输入信号
struct Input {
let iphoneNumber: Driver<String>
let codeTextNumber: Driver<String>
let codeSenderTap: Driver<Void>
let loginButtonTap: Driver<Void>
}
Output输入的话,我们同样划分为4个
struct Output {
/// 发送按钮状态
let codeSenderEnable: Driver<Bool>
/// 登录按钮状态
let loginButtonEnable: Driver<Bool>
/// 登录完成跳转
let loginButtonComplete: Driver<Bool>
/// 接口状态
let signingIn: Driver<Bool>
}
我们看下把我们的UI事件转换为我们的Driver序列
guard let viewModel = self.viewModel as? LoginViewModel else { return }
let input = LoginViewModel.Input(
iphoneNumber: iphoneTextField.rx.text.orEmpty.asDriver(),
codeTextNumber: codeTextField.rx.text.orEmpty.asDriver(),
codeSenderTap: codeSenderButton.rx.tap.asDriver(),
loginButtonTap: loginButton.rx.tap.asDriver())
这里我们通过rx把我们的输入框和按钮转换为我们的driver序列,Driver我们之前说过主要是对于UI的事件转换为可观察序列
lazy var phoneSignal: Observable<Bool> = {
iphoneTextField.rx
.text
.orEmpty
.map { Validator.phone($0).isValid }
}()
我们也可以自己进行转换判断输入的手机号是否可用,继续看下转换操作
2. 逻辑处理
- 输入框转换
let codeSenderEnable = input.iphoneNumber.map { num -> Bool in
return num.count >= 11
}
把我们的输入string ,判断数字是否大于等于11.转换为bool值
- 验证码处理
let iphoneNumberEnable = input.codeTextNumber.map { num -> Bool in
return num.count >= 6
}
同样的验证码输入框进行6位数的判断
- 验证码点击处理
input.codeSenderTap.withLatestFrom(input.iphoneNumber)
.drive { [weak self] num in
guard let self = self else { return }
self.getCode(iphone: num)
} .disposed(by: disposeBag)
这里把我们的输入框序列源和验证码点击事件合并到一起,merges合并。通过使用第二个序列中最新的元素,将两个可观察序列合并为一个可观察序列,当我们订阅的时候会拿到最新的元素也就是我们的手机号。这里drive类似我们的订阅获取验证码
func getCode(iphone: String) {
request(LoginApi.sms(phone: iphone), Bool.self)
.subscribe { isOk in
NSLog("发送成功\(isOk)")
}.disposed(by: disposeBag)
}
- 登录按钮的处理
let loginButtonEnable = Driver.combineLatest(codeSenderEnable, iphoneNumberEnable).map { num in
return num.0 && num.1
}
通过我们的combineLatest,只会保存后发送的信号,并且组合信号中,只有都发送了才会到共同订阅的.因此只有我们的验证码和输入框都符合我的要求才可以点击。
之后获取我们手机输入框和验证码输入框的值
let codeAndIphone = Driver.combineLatest(input.iphoneNumber, input.codeTextNumber) {($0,$1)}
处理我们登录按钮的事件
let activity = ActivityIndicator()
let signingIn = activity.asDriver()
let loginButtonComplete = input.loginButtonTap
.withLatestFrom(codeAndIphone)
.flatMap { [weak self] num -> Driver<Bool> in
guard let self = self else { return Driver.just(false) }
return self.logon(iphone: num.0, code: num.1, activity: activity)
ActivityIndicator是类似我们loading的加载,我们把我们点击和我们的输入框的值合并,之后进行转换
flatMap,因为我们的codeAndIphone合并的是一个元祖类型的元素,我们通过flatMap转换为一个可观察序列,相当于把2个序列源转换为一个。
- 登录请求
func logon(iphone: String, code: String, activity: ActivityIndicator) -> Driver<Bool> {
return request(LoginApi.smsVerify(phone:iphone, code:code ), Token.self)
.trackActivity(activity)
.map { token -> Bool in
AuthManager.setToken(token: token)
return true }
.asDriver(onErrorJustReturn: false)
}
我们的请求是队moya进行封装,你也可以使用RxAlamofire
private func rawRequest<T: Decodable>(_ target: TargetType, _ type: T.Type) -> Observable<Pandora<T>>{
NetworkManager.provider.rx
.request(MultiTarget(target))
.subscribeOn(CurrentThreadScheduler.instance)
.asObservable()
.map(Pandora<T>.self ,using: CleanJSONDecoder())
.catchError { error in
if let moyaError = error as? MoyaError {
throw ApiError(statusCode: moyaError.response?.statusCode ?? -1, code: nil, message: moyaError.errorDescription ?? "")
}
throw ApiError(statusCode: -1, code: .unknwon, message: nil)
}
.share()
.observeOn(MainScheduler.instance)
.filter { pandora in
if pandora.isInvalidToken {
NotificationCenter.default.post(Notification(name: .InvalidToken)) // token 失效
return false
}
return true
}
}
这里 就是把登录请求结果转换为bool值,中间会保存token等操作。
3. 输出的绑定
我们通过transform处理了我们的逻辑,接下来就是把我们的输出的信号绑定到UI上。
- 验证码按钮
output.codeSenderEnable
.drive(codeSenderButton.rx.isEnabled)
.disposed(by: disposeBag)
我们的验证码按钮是否可点击绑定我们的验证码按钮。
- 登录按钮
output.loginButtonEnable
.drive(loginButton.rx.isEnabled)
.disposed(by: disposeBag)
登录按钮的绑定是否可用
- 登录结果
output.loginButtonComplete.filter { $0 }
.drive { [weak self ] _ in
guard let self = self else { return }
self.gotoTabbarViewController()
}.disposed(by: disposeBag)
我们登录的结果拿到结果后进行调整等操作。
- 正在登录
output.signingIn
.drive { self.view.isLoading(isStop:$0) }
.disposed(by: disposeBag)
我们视图展示为我们基类的loading,是否展示。
func isLoading(isStop: Bool) {
isStop ? startLoading() : stopLoading()
}
以上就是我们输入和输出,界面的绑定,可以发现我们vc的代码少了很多,就是我们进行展示我们的绑定关系
4. 总结
把我们的界面的UI元素转换为可观察序列,viewmodel的Input。之后通过我们的逻辑处理viewmodel输出我们的Output。通过我们的序列进行连接我们UI操作作为输入源element,通过订阅对我们的UI进行展示和操作。