RxSwift学习-24-RxSwift中MVVM的使用

1,996 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第27天,点击查看活动详情

  • 本文主要介绍RxSwiftmvvm的使用,这里以登录为例,界面如下:

image.png

这里我们的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的事件转换为可观察序列

image.png



    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的代码少了很多,就是我们进行展示我们的绑定关系

image.png

4. 总结

把我们的界面的UI元素转换为可观察序列,viewmodelInput。之后通过我们的逻辑处理viewmodel输出我们的Output。通过我们的序列进行连接我们UI操作作为输入源element,通过订阅对我们的UI进行展示和操作