MVVM 和 响应式编程入门

MVVM 和 响应式编程入门

架构的思考

简要归纳一下 App 作的事?
架构是干什么的?git

App 的本质是反馈回路

一个项目,最重要的两个角色:Model 和 View。github

Model 决定是什么(数据),View 决定如何展现,其余的内容,基本都是处理二者的交互关系,以及提供这二者的服务。
若是要将 MV(X),其实基本讨论都逃不过二者间的交互:算法

反馈回路:编程

App 的任务

基于上图呢,咱们的大部分的工做其实能够拆分到下面 5 项中的一项:swift

  1. 构建:谁负责构建 model 和 view,以及将二者链接起来?
  2. 更新 model:如何处理 view action?
  3. 改变 View:如何将 model 的数据应用到 view 上去?
  4. view state:如何处理导航和其余一些 model state(按钮状态、switch)
  5. 测试:为了达到必定程度的测试覆盖,要采起怎样的测试策略

对于上面5个问题的回答,构成了 App 设计模式的基础要件设计模式

view state 想一想页面跳转的逻辑,按钮是否可点击的状态网络

回顾 MVC

MVC 其实在上面的反馈回路中间插入了 Controller,使得每条线都通过了 Controller。多线程

MVC 模块职责

  1. 构建:谁负责构建 model 和 view,以及将二者链接起来?
  2. 更新 model:如何处理 view action?
  3. 改变 View:如何将 model 的数据应用到 view 上去?(单向数据流)

单向数据流:好比更改了用户姓名,其实应该是只负责更新 model 也就是 name 字段,而后经过 kvo,让响应的nameLabel 改变显示闭包

  1. view state:如何处理导航和其余一些 model state
  2. 测试:为了达到必定程度的测试覆盖,要采起怎样的测试策略(集成测试)

MVC 总结

最简单的模式,适用于绝大部分状况。

两个地方不太尽人意:

  1. 观察者模式失效

举个例子:这段代码有什么潜在的问题?

func changeName(name : String) {
    person.name = name
    namelabel.text = name
}
  1. 肥大的 View Controller

责任重大,因此代码量很是容易就动辄几千行

对于这两个问题,其实也有不少的解决办法,第一个好比单向数据流,严格执行观察者模式;第二个办法很是很是多,好比 catogory,代理,代码拆分。(objc中国、唐巧)

MVC 是万能的吗?

为何还要学习下其余的架构?

  • 借鉴思想弥补 MVC
  • 定义好框架严格执行
  • 锻炼设计、抽象的能力
  • 拓宽思惟
    。。。

MVVM - C

Model - View - ViewModel + 协调器(Coordinator)

从 MVC 到 MVVM

用过 MVVM的话 以为这张图有什么问题吗?

注意几点:

  1. 必须建立 View-model。
  2. 必须创建起 View-model 和 View 之间的绑定。
  3. Model 由 View-model 拥有,而不是由 controller 拥有。

学习一下代码,看是怎么跑起来的

先演示一下 Demo,并看一下 MVVM - C 个模块职责

协调器:

  1. 负责将 Model 的初始值,赋值给 rootViewController
  2. 全部和页面跳转的相关逻辑,(同时提供了新页面所须要的数据)

ViewModel

  1. 持有 model
  2. 拥有一系列可观察的信号量(序列)
  3. 提供数据的更新方法
  4. 私有辅助方法

ViewController & View

  1. 绑定 ViewModel的序列,和 View 的某个字段
  2. 将一些 ViewAction 调用的方法指向 ViewModel 中的数据更新方法
  3. 保存 View State
  4. 维护 View 的层级(demo中是 Storyboard)
  5. View 如何展现 (如何)

返回回路,已经基本被摘出去了

Model

仍是那个 model

这样咱们有个一个完整的管道

  1. 协调器协调了跳转以及为每一个要跳转的 ViewController 的 ViewModel 设置初始的 model。
  2. ViewModel 使用 model提供直接可以使用的可观察序列。(什么是观察序列?
  3. 为了能直接使用,须要干两间事:合并序列以及数据变形。(什么叫能直接使用?
  4. Controller 使用 bind 将准备好的值绑定到各个 View 上去。

来看反馈回路:

  1. TableView 发送 Action
tableView.rx.modelDeleted(Item.self)
            .subscribe(onNext: { [unowned self] in self.viewModel.deleteItem($0) }).disposed(by: disposeBag)
  1. Controller 调用 ViewModel 的方法,来删除数据
func deleteItem(_ item: Item) {
    folder.value.remove(item)
}
  1. 调用持久化层的 save 更改数据,产生一个通知
NotificationCenter.default.post(name: Store.changedNotification, object: notifying, userInfo: userInfo)
  1. 通知早已经做为一个序列,已经被其余序列合并,形成多个序列的更新事件。
var folderContents: Observable<[AnimatableSectionModel<Int, Item>]> {
    return folderUntilDeleted.map { folder in
        guard let f = folder else {
            return [AnimatableSectionModel(model: 0, items: [])]
        }
        return [AnimatableSectionModel(model: 0, items: f.contents)]
    }
}

注意,和通知序列相关的好多序列都会收到更新,从而更新各类 View 的显示或者状态。

  1. 经过以前的 bind 更新 view 的显示
viewModel.folderContents.bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: disposeBag)

怎么就绑定了?

先来看下函数响应式编程

函数响应式编程

先来看个例子

一个拥有用户名和密码输入框的登陆界面:

产品经理说了需求:4句话:

  1. 用户名不足 5 个字符的时候,给出红色提示语1;
  2. 用户名不足 5 个字符的时候,没法输入密码,>=5时,能够输入
  3. 密码不足 5 个时候,也显示红色提示语2;
  4. 用户名和密码有一个不符合要求时,底部绿色按钮不可点击,只有当用户名和密码同时有效时按钮才能够点击。

通常的思路:

监听 Username 输入框,根据字符个数要考虑下面3件事:

  1. 提示语1是否显示
  2. Password 是否可输入
  3. 结合 Password 的情况,判断按钮是否能够点击

监听 Password 输入框,根据字符个数考虑下面2件事:

  1. 提示语2是否显示
  2. 结合 UserName 的情况,判断按钮是否能够点击

因此开发过程:

有什么问题?

  1. 须要翻译这个过程
  2. 不少变化须要结合到一块儿考虑,若是变化因素更多了,很容易出 bug。

其实这个翻译过程,咱们本身把一些有联系的因素给放到一块儿处理了,好比按钮是均可点击,须要同时监听两个文本框的状态。

咱们可否只罗列条件,而后把这些条件扔给一个条件处理的机制,这个机制就能帮咱们正确的处理这些关系?

函数响应式编程来了:

咱们作两件事情:

  1. 将条件做为对象(序列)
  2. 将条件和结果进行绑定

开发过程变成了:

来看看代码:

let usernameValid = usernameOutlet.rx.text.orEmpty
    .map { $0.characters.count >= minimalUsernameLength }
    .share(replay: 1) // without this map would be executed once for each binding, rx is stateless by default

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)

无需翻译,只须要罗列条件,接下来就是见证奇迹的时刻

直观的来看代码清晰不少,而后不怎么用动脑子,咱们下面来看看什么是函数响应式编程再来讲明他有哪些优缺点。

函数式编程

函数式编程是种编程范式,它须要咱们将函数做为参数传递,或者做为返回值返还。咱们能够经过组合不一样的函数来获得想要的结果。

经过函数这个“管道”,数据从一头通过“管道”到另外一头,就获得了想要的数据。

编程范式?(命令式、声明式、函数式)http://www.javashuo.com/article/p-driicrqp-gb.html

函数响应式编程

函数式编程 + 响应

经过函数构建数据序列,最后经过适当的方式来响应这个序列,就是函数响应式编程。

在 Swift 中,咱们是用 RxSwift 来实现函数响应式编程!

RxSwift 核心

核心角色有如下5个

  • Observable - 可被监听的序列 - 产生事件
  • Observer - 观察者 - 响应事件
  • Operator - 操做符 - 建立变化组合事件
  • Disposable - 可被清楚的资源 - 管理绑定(订阅)的生命周期
  • Schedulers - 调度器 - 线程队列调配

以下图所示:

Observable 可被监听的序列(下面都简称序列)

一个序列,随着时间的流逝,这个队列将陆续产生一些能够被观察的值。

你能够将温度看做是一个序列,而后监测这个序列产生的值,最后对这个值作出响应。例如:当室温高于 33 度时,打开空调降温。

函数响应式编程里最重要的就是构造序列。在函数响应式编程中,一切均可以看做是序列。

  1. 一次点击事件
  2. 一个属性的变化
  3. 一个网络请求回调
  4. 。。。。

其实对于值的变化的队列,好理解,那么一次操做,或者一次网络请求任务也看作是队列,这个怎么实现的?

大多数序列能够产生3种事件:

public enum Event<Element> {
    case next(Element)
    case error(Swift.Error)
    case completed
}
  • next - 序列产生了一个新的元素
  • error - 建立序列时产生了一个错误,致使序列终止
  • completed - 序列的全部元素都已经成功产生,整个序列已经完成

因此当你想任何东西封装成一个序列,只要 create 一个序列,而后在原有逻辑基础上在适当的时机调用这些事件便可,而且 RxSwift 已经帮咱们建立了大量的序列:

button 的点击
textField 的当前文本
switch 的开关状态
Notification 队列

若是本身建立,就须要调用上面说的事件了,好比咱们手动建立一个序列:

let numbers: Observable<Int> = Observable.create { observer -> Disposable in

    observer.onNext(0)
    observer.onNext(1)
    observer.onNext(2)
    observer.onNext(3)
    observer.onNext(4)
    observer.onNext(5)
    observer.onNext(6)
    observer.onNext(7)
    observer.onNext(8)
    observer.onNext(9)
    observer.onCompleted() // 结束

    return Disposables.create()
}

除了普通的队列,框架还提供了好多其余的队列提供了不一样的特性。

  • Single:它要么只能发出一个元素,要么产生一个 error 事件。
  • Completable :它要么只能产生一个 completed 事件,要么产生一个 error 事件。
  • Maybe:它介于 Single 和 Completable 之间,它要么只能发出一个元素,要么产生一个 completed 事件,要么产生一个 error 事件。
  • 还有 Driver、ControlEvent

一个序列,其实就是一个被观察者

观察者

观察者是:观察序列的,响应序列事件的角色

建立一个观察者:

tap.subscribe(onNext: { [weak self] in
    self?.showAlert()
}, onError: { error in
    print("发生错误: \(error.localizedDescription)")
}, onCompleted: {
    print("任务完成")
})

建立观察者最直接的方法就是在 Observable 的 subscribe 方法后面描述,事件发生时,须要如何作出响应。而观察者就是由后面的 onNext,onError,onCompleted的这些闭包构建出来的。

一样框架为咱们提供了不少观察者,几乎每一个类的全部属性(包括自定义类),class.rx.xx 均可以做为观察者。

viewModel.navigationTitle.bind(to: rx.title).disposed(by: disposeBag)
viewModel.noRecording.bind(to: activeItemElements.rx.isHidden).disposed(by: disposeBag)
viewModel.hasRecording.bind(to: noRecordingLabel.rx.isHidden).disposed(by: disposeBag)
viewModel.timeLabelText.bind(to: progressLabel.rx.text).disposed(by: disposeBag)
viewModel.durationLabelText.bind(to: durationLabel.rx.text).disposed(by: disposeBag)
viewModel.sliderDuration.bind(to: progressSlider.rx.maximumValue).disposed(by: disposeBag)
viewModel.sliderProgress.bind(to: progressSlider.rx.value).disposed(by: disposeBag)
viewModel.playButtonTitle.bind(to: playButton.rx.title(for: .normal)).disposed(by: disposeBag)
viewModel.nameText.bind(to: nameTextField.rx.text).disposed(by: disposeBag)

Binder

观察者有两种:咱们此次只讲下 Binder

  • AnyObserver
  • Binder

Binder 主要有如下两个特征:

  • 不会处理错误事件
  • 确保绑定都是在给定 Scheduler 上执行(默认 MainScheduler)

因此不少UI 观察者都使用 Binder 去实现,只处理 Next ,而且在 主线程响应。

usernameValidOutlet.rx.isHidden 的由来

因为页面是否隐藏是一个经常使用的观察者,因此应该让全部的 UIView 都提供这种观察者:

extension Reactive where Base: UIView {
  public var isHidden: Binder<Bool> {
      return Binder(self.base) { view, hidden in
          view.isHidden = hidden
      }
  }
}
usernameValid
    .bind(to: usernameValidOutlet.rx.isHidden)
    .disposed(by: disposeBag)

这样你没必要为每一个 UI 控件单首创建该观察者。这就是 usernameValidOutlet.rx.isHidden 的由来,许多 UI 观察者 都是这样建立的。

操做符

有了序列(被观察者 Observable)和观察者 (Observer),还差一点什么?

我有了一个时间戳的序列,怎么和观察者(birthdayLabel.rx.text)绑定?
我有了 Username 和 Password 是否有效的序列,怎么和 Button 是否能够点击的观察者(doSomethingOutlet.rx.isEnabled)绑定?

序列须要变形、合并、相互影响!

操做符能够帮助你们建立新的序列,或者变化组合原有的序列,从而生成一个新的序列。

https://beeth0ven.github.io/RxSwift-Chinese-Documentation/content/decision_tree.html

这里只介绍一种最简单经常使用的操做符,map,知道操做符的含义便可。

let usernameValid = usernameOutlet.rx.text.orEmpty
    .map { $0.characters.count >= minimalUsernameLength }
    .share(replay: 1)

Disposable

既然之后绑定和订阅,确定有取消绑定和订阅,怎么取消呢,就用到了 Disposable。

最经常使用的是清除包(DisposeBag) 或者 takeUntil 操做符 来管理订阅的生命周期。

var disposeBag = DisposeBag() // 来自父类 ViewController

override func viewDidLoad() {
    super.viewDidLoad()
    
    ...
    
    usernameValid
        .bind(to: passwordOutlet.rx.isEnabled)
        .disposed(by: disposeBag)
}

这个例子中 disposeBag 和 ViewController 具备相同的生命周期。当退出页面时, ViewController 就被释放,disposeBag 也跟着被释放了,那么这里的绑定(订阅)也就被取消了。这正是咱们所须要的。

调度器

Schedulers 是 Rx 实现多线程的核心模块,它主要用于控制任务在哪一个线程或队列运行。

let rxData: Observable<Data> = ...

rxData
    // 序列的构建函数在后台运行
    .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated))
    // 主线程监听和处理结果
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: { [weak self] data in
        self?.data = data
    })
    .disposed(by: disposeBag)

函数响应式编程总结

我的感悟:

序列、操做符、观察者分别被封装成,交互过程当中的3个对象,将这3者进行了解耦。能够灵活的组合,对序列进行变形、合并生成新的可直接使用的序列。提供一种更为高效的编程方法,从数据变形的角度看是一种降维,因此这方面的 Bug 会少一些。同时,学习曲线过于陡峭,用很差极可能写出很反人类的代码,让别人根本无法读,另外使用绑定机制,出现一些 Bug 确实不易调试。

可是最难得的是这种思想,咱们还能够把函数看作是对象,能够做为参数传递,能够做为返回值,就像一些设计模式,将算法封装成对象,有了策略模式;将命令封装成对象,有了命令模式;将状态封装成对象,有了状态模式,都解决了一些领域里的问题。学习这种思想,让咱们之后在编码上可以已更加宽广的视角去看待问题。

回头来看 MVVM 中的绑定

init(initialFolder: Folder = Store.shared.rootFolder) {
    folder = Variable(initialFolder)
    folderUntilDeleted = folder.asObservable()
        // Every time the folder changes
        .flatMapLatest { currentFolder in
            // Start by emitting the initial value
            Observable.just(currentFolder)
                // Re-emit the folder every time a non-delete change occurs
                .concat(currentFolder.changeObservable.map { _ in currentFolder })
                // Stop when a delete occurs
                .takeUntil(currentFolder.deletedObservable)
                // After a delete, set the current folder back to `nil`
                .concat(Observable.just(nil))
        }.share(replay: 1)
}

folder.asObservable()

对一个属性生成一个序列的方式

flatMapLatest

“在数据源每次发出一个值的时候,它使用该值构建,开始,或者选择一个新的可观察量。不过这个变形可让>咱们基于第一个可观察量发出的状态,来订阅第二个可观察量。”

—— 摘录来自: Chris Eidhof. “App 架构。” iBooks.

just

concat

让两个或者多个 Observables 按顺序串联起来

concat 操做符将多个 Observables 按顺序串联起来,当前一个 Observable 元素发送完毕后,后一个 `Observable 才能够开始发出元素。

concat 将等待前一个 Observable 产生完成事件后,才对后一个 Observable 进行订阅。若是后一个是“热” Observable ,在它前一个 Observable 产生完成事件前,所产生的元素将不会被发送出来。

currentFolder.changeObservable

var changeObservable: Observable<()> {
    return NotificationCenter.default.rx.notification(Store.changedNotification).filter { [weak self] (note) -> Bool in
        guard let s = self else { return false }
        if let item = note.object as? Item, item == s, !(note.userInfo?[Item.changeReasonKey] as? String == Item.removed) {
            return true
        } else if let userInfo = note.userInfo, userInfo[Item.parentFolderKey] as? Folder == s {
            return true
        }
        return false
    }.map { _ in () }
}

每次收到通知,只有通过 filter 函数检验为 true 的元素,才会被放到序列中做为新事件。

takeUntil

currentFolder.deletedObservable

同 currentFolder.changeObservable,这不过这个是和删除有关的通知,才会放到序列。

concat(Observable.just(nil))

nil 将会在 takeUtil 执行后发出,想一想为啥?

share(replay: 1)

屡次绑定只执行一次操做序列

folderUntilDeleted

咱们将 folder 这个可观察量,与其余由 model 驱动的,可能影响咱们 view 的逻辑的可观察量,进行了合并。获得的结果是一个新的可观察量 folderUntilDeleted,它会在底层文件夹对象发生变化时正确更新,而且在底层文件夹对象被从 store 中删除时将本身设置为 nil。

MVVM-C 总结

  1. 很大程度上解决了 MVC 的痛点
  2. 响应式编程双刃剑,学习成本陡峭,可是用起来会很是爽,总体代码甚至会减小,bug也会减小,调试变得困难。

较少响应式编程的 MVVM

  1. tableview的 代理
  2. 使用 Notification 和 KVO 代替响应式编程

经验和教训

即便咱们不使用 MVVM 他的一些思想咱们仍是能够借鉴的。

  1. 引入中间层
  2. 协调器,解耦多个 Controller
  3. 数据变形是单独能够提出来的

引用:

RxSwift 中文文档:https://beeth0ven.github.io/RxSwift-Chinese-Documentation/
《App 架构》
Interactive diagrams of Rx Observables : http://rxmarbles.com
菜鸟教程 swift 教程: https://www.runoob.com/swift/swift-tutorial.html

相关文章
相关标签/搜索