• [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 版的演示代码,有兴趣的同学可以了解一下。

     

  • 相关阅读:
    Android(java)学习笔记6:实现Runnable接口创建线程 和 使用Callable和Future创建线程
    Android(java)学习笔记5:线程的生命周期
    Android(java)学习笔记4:线程的控制
    Android(java)学习笔记3:线程的优先级
    Android(java)学习笔记2:继承Thread类创建线程类
    凑算式
    三洋献瑞
    三洋献瑞
    三洋献瑞
    移动距离
  • 原文地址:https://www.cnblogs.com/strengthen/p/13581040.html
Copyright © 2020-2023  润新知