RxSwift介绍及MVVM架构的应用

290 阅读14分钟
函数式编程(FP)

函数式编程就是一种抽象程度很高的编程范式,它将计算机运算看做是数学中函数的计算,而纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用

简单来说: 将函数作为一个单元来处理逻辑,给定一个输入值,就会有对应的一个输出值。函数还可以作为参数和返回值。这就是函数编程。

函数表达式:f(x) —> x = f(x) —> y = f(f(x))

函数编程的特性:

  • 闭包: 一种特殊的函数,绑定了函数内部引用的所有变量,把它引用的东西都放在一个上下文中“包”了起来
  • 高阶函数: 把函数作为参数或者返回值的函数
  • 匿名函数: 在传入函数时,有些时候,不需要显式地定义函数,可以直接传入匿名函数。
  • 柯里化: 把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术

优点

  • 灵活
  • 高复用
  • 简洁
  • 易维护
  • 适应各种需求变化
响应式编程 (RP)

响应式编程是一种从数据流和变化出发的解决问题的模式。 响应式编程,本质上是对数据流或某种变化所作出的反应,但是这个变化什么时候发生是未知的,所以是基于异步、回调的方式在处理问题。

通俗来讲:A赋值给BB发生变化的时候A也会跟着发生变化,这就是响应式编程。

响应式编程的特性

  • 数据流: 一个流就是一串事件发生的时间的值的序列。这里面至少有三要素:数据报错完成.
  • 变化: 信号变化回调
函数响应式编程(FRP)

函数响应式编程:说白了就是函数式和响应式的结合.
函数响应式编程是种编程范式。它是通过构建函数操作数据序列,然后对这些序列做出响应的编程方式。它结合了函数式编程以及响应式编程

RxSwift 介绍

RxSwift: 是 Rx 为 Swift 语言开发的一套函数响应式编程框架,它可以代替iOS系统的 Target Action / 代理 / 闭包 / 通知 / KVO,同时还提供网络、数据绑定、UI事件处理、UI的展示和更新、多线程…… image.png

在RxSwift中,所有异步操作(事件)和数据流均被抽象为可观察序列的概念
RxSwift的主要流程: 创建序列 --> 订阅序列 --> 发送信号 --> 信号接收

为什么要使用 RxSwift ?

  • 复合 - Rx 就是复合的代名词
  • 复用 - 因为它易复合
  • 清晰 - 因为声明都是不可变更的
  • 易用 - 因为它抽象了异步编程,使我们统一了代码风格
  • 稳定 - 因为 Rx 是完全通过单元测试的

一、RxSwift 配置

  1. 使用RxSwift需要导入RxSwiftRxCocoa 这两个库,缺一不可。这两个库的主要作用是:
  • RxSwift:它只是基于 Swift 语言的Rx 标准实现接口库,所以 RxSwift 里不包含任何 Cocoa 或者 UI 方面的类
  • RxCocoa:是基于 RxSwift 针对于 iOS 开发的一个库,它通过 Extension 的方法给原生的比如 UI 控件添加了 Rx 的特性,使得我们更容易订阅和响应这些控件的事件。
  1. 再使用RxSwift的地方import这个两个库就行
import RxSwift
import RxCocoa

二、Observable (创建、订阅、销毁)

1、Observable
  • Observable<T>: 观察序列

1、Observable 这个类就是 Rx 框架的基础,我们可以称它为可观察序列。它的作用就是可以异步地产生一系列的 Event(事件),即一个 Observable 对象会随着时间推移不定期地发出 event(element : T) 这样一个东西。
2、这些 Event 还可以携带数据,它的泛型 就是用来指定这个 Event 携带的数据的类。
3、有了可观察序列,我们还需要有一个 Observer(订阅者)来订阅它,这样这个订阅者才能收到 Observable 不时发出的 Event。

  • Enevt: 事件。
    Observable可以发出三种不同的事件:next、 error 、 completed
public enum Event<Element> {
    /// Next element is produced.
    case next(Element)

    /// Sequence terminated with an error.
    case error(Swift.Error)

    /// Sequence completed successfully.
    case completed
}

1、nextnext 事件就是那个可以携带数据 <T> 的事件
2、error: error事件表示一个错误,它可以携带具体的错误内容,一旦 Observable 发出了 error event,则这个 Observable 就等于终止了,以后它再也不会发出 event 事件了
3、completedcompleted 事件表示 Observable 发出的事件正常地结束了,一旦 Observable 发出了completed event,则这个 Observable 就等于终止了,以后它再也不会发出 event 事件了

2、Observable 序列的创建

Observable有好多种序列的创建方法,我们这里就解说一种比较常用的create() 序列创建。
create 序列

  • 该方法接受一个闭包形式的参数,任务是对每一个过来的订阅进行处理。
  • 下面是一个简单的样例。为方便演示,这里增加了订阅相关代码
  • 这也是序列创建的一般方式,应用非常之多
// 1、创建序列
_ = Observable<String>.create({ (observable) -> Disposable in
            //3、发送信号
            observable.onNext("RxSwift  你好!")
//            observable.onError()  //发送错误信号
//            observable.onCompleted() //发送完成信号
            
            return Disposables.create() // 销毁
           // 2、订阅序列
        }).subscribe(onNext: { (next) in
            NSLog("订阅到--\(next)")
        }, onError: { (error) in
            NSLog("错误--\(error)")
        }, onCompleted: {
            NSLog("完成回调")
        }, onDisposed: {
            NSLog("订释放回调")
        })
3、Observable 订阅

一般我们创建完Observable 序列以后,我们会用subscribe()对其进行订阅,用来接收Observable发出的事件Event .
关于subscribe的订阅有两种方式:

  • subscribe()方法:该方法的 block 的回调参数就是被发出的 event 事件,我们将其直接打印出来。
let observable = Observable<String>.of("Json","XML")

observable
    .subscribe { (event) in
       Log("---of----\(event)"
    )}.disposed(by: self.disposeBag)

// 打印值
//ofQueue():73->---of----next(Json)
//ofQueue():73->---of----next(XML)
//ofQueue():73->---of----completed

1、初始化 Observable 序列时设置的默认值都按顺序通过 .next事件发送出来
2、next事件送完以后,还会自动发送一个completed事件
3、我们要向获取接收到的元素可以用event.element 去获取

  • subscribe的另一种订阅方法:该方法可以把 event 进行分类,通过不同的 block 回调处理不同类型的 event,同时会把 event 携带的数据直接解包出来作为参数,方便我们使用。
    let observable = Observable<String>.of("Json","XML")
              
     observable.subscribe { (element) in
          print("---of----\(element)")
     } onError: { (error) in
          print(error)
     } onCompleted: {
          print("completed")
     } onDisposed: {
          print("disposed")
     }
     
    // 打印
    //---of----Json
    //---of----XML
    //completed
    //disposed

1、我们通过打印发现这次直接打印的是element,不需要event.element处理了
2、每个回调都独立出来了,我们可以做一些不同的业务需求

4、监听事件的生命周期

关于事件的生命周期,我们可以用doOn来监听,关于doOn:

  • doOn 方法的调用不会影响subscribe的调用
  • doOn 方法会在每一次事件发送前被调用
  • 同时它和 subscribe 一样,可以通过不同的 block 回调处理不同类型的 event

doOn的每个回调都发生在subscribe之前
do(onNext:)方法在 subscribe(onNext:) 前调用
do(onCompleted:) 方法在 subscribe(onCompleted:) 前面调用 do(onDisposed)销毁方法却是在subscribe(onDisposed)后面调用的

let observable = Observable<String>.of("Json","XML")
observable
    .do(onNext: { element in
        print("---do---\(element)" )
    }, onError: { error in
        print("do Error:", error)
    }, onCompleted: {
        print("do Completed")
    }, onDispose: {
        print("do Disposed")
    })
    .subscribe { (element) in
        print("---of---\(element)")
    } onError: { (error) in
        print(error)
    } onCompleted: {
        print("completed")
    } onDisposed: {
        print("disposed")
    }
    
打印
// ---do---Json
// ---of---Json
// ---do---XML
// ---of---XML
// do Completed
// completed
//disposed
// do Disposed
 
5、Observable 的销毁(Dispose)

当一个Observable被观察订阅后,就会产生一个Disposable实例,通过这个实例,我们就能进行资源的释放了。
对于RxSwift中资源的释放,也就是解除绑定、释放空间,有两种方法,分别是显式释放隐式释放

  • 显式释放: 也就是dispose(), 可以让我们在代码中直接调用释放方法进行资源的释放
let dispose = self.TestButton.rx.tap
                   .subscribe({ _ in
                       NSLog("点击")
                   })
 dispose.dispose()
 
  • 隐式释放
    隐式释放则通过DisposeBag来进行,它类似于Objective-C ARC中的自动释放池机制.
    DisposeBag对于RxSwift就像自动释放池一样,我们把资源添加到DisposeBag中,让资源随着DisposeBag一起释放。

DisposeBag就像个垃圾袋一样,我们把创建的序列放在DisposeBag这个垃圾袋中,他会在合适的时候帮我们释放资源。

// 可以写成全局的
static var disposeBag = DisposeBag()
let observable = Observable<String>.of("Json","XML")
observable
    .subscribe { (event) in
        Log("---of----\(event)")
    }.disposed(by: self.disposeBag)

RxSwift在MVVM架构的应用

原先常用的架构:MVC

image.png MVC 是 Model-View-Controller 的简写。MVC 主要有三层:

  • Model 数据层,读写数据,保存 App 状态
  • View 页面层,和用户交互,向用户显示页面,反馈用户行为
  • ViewController 逻辑层,更新数据,或者页面,处理业务逻辑

缺点:

  • ViewController 既扮演了 View 的角色,又扮演了 ViewController 的角色
  • 而 Model 在 VIewController 中又可以直接与 View 进行交互
  • 当 App 交互复杂的时候,就会发现 ViewController 将变得十分臃肿,大量代码被添加到控制器中,使得控制器负担过重。
MVVM

image.png

MVVM 和 MVC 十分相识。只不过他的分层更加详细:

  • Model 数据层,读写数据,保存 App 状态
  • View 页面层,提供用户输入行为,并且显示输出状态
  • ViewModel 逻辑层,它将用户输入行为,转换成输出状态
  • ViewController 主要负责数据绑定

没错,ViewModel 现在是逻辑层,而控制器只需要负责数据绑定。如此一来控制器的负担就减轻了许多。并且 ViewModel 与控制器以及页面相独立。那么,你就可以跨平台使用它。你也可以很容易地测试它。

优点:

  • 可以对 ViewController 进行瘦身
  • 实现逻辑视图的复用。比如一个 ViewModel 可以绑定到不同的 View 上,让多个 View 重用相同的视图逻辑。
  • 而且使用 MVVM 可以大大降低代码的耦合性,方便进行单元测试以及维护,也方便多人协作开发(比如一个人负责逻辑实现,一个人负责 UI 实现)。

缺点:

  • 相较于 MVC,使用 MVVM 会轻微的增加代码量,但是总体上减少了代码的复杂性。
  • 还有就是有一定的学习成本(如何数据绑定等)。

MVVM目录结构

image.png
上图是项目中的一个模块,使用MVVM架构后的文件结构,Model被我集中的定义在一个公共的文件夹里了,接下来我会详细介绍。

ViewModel

查阅了许多资料,不同人对ViewModel的实现有很多种,我这里总结了一下多数人也是我比较赞同的一种实现方法

image.png

将ViewModel理解为一个简单的黑盒子,它接受输入以产生输出,这里的输入和输出都是一个个序列。这样就能实现MVVM的最大的好处,使业务逻辑可测试。ViewModel里面主要进行网络请求、业务处理等操作。网络请求的框架我们用的是Moya,因为它可以使我们的请求得到一个序列,然后我们才可以进行数据绑定。 一般的ViewModel大概是长这样的:

class ViewModel {
    // 输入转化输出,这里是真正的业务逻辑代码了
    func transform(input: Input) -> Output {
    }
}
extension ViewModel {
    // 输入,类型是Driver,因为跟UI控件有关
    struct Input {
    }
    // 输出,类型也是Driver
    struct Output {
    }
}

Model

对于Model,它主要是定义一些数据模型,当然你也可以封装一些数据转换等公共的业务方法。

ViewController和View

ViewController的主要作用是管理视图的生命周期,绑定数据和View的关系,数据绑定的实现主要是通过RxDataSources+RxSwift来实现的,所以说你的项目中要引入这两个库。RxCocoa给UI框架提供了Rx支持,让我们能够使用按钮点击序列,这样我们就可以给ViewModel提供输入了,而RxDataSources能够帮助你简化书写 TabelView或 CollectionView的数据源这一过程,并且提供了通过序列更新TableView的方法,这时候我们只要把ViewModel的数据输出序列绑定到TableView的数据源序列就可以了。

Navigator

Navigator是从ViewController剥离出来用来控制视图跳转

上代码

下图是上述目录结构中一个页面

291549013399_.pic.jpg

先分析下界面上的输入和输出

输入:进入页面时的请求,重命名按钮点击,删除按钮点击,新建分组按钮点击
输出:TableView数据源,页面Loading状态

ViewModel核心代码:

class MenuSubGroupViewModel {
    func transform(input: Input) -> Output {
        let loadingTracker = ActivityIndicator()
        let createNewGroup = input.createNewGroup
            .flatMapLatest { _ in
                self.navigator.toMenuEditGroupVC()
                    .saveData
                    .asDriverOnErrorJustComplete()
            }
        let renameGroup = input.cellRenameButtonTap
            .flatMapLatest...
        let getMenusInfo = Driver.merge(createNewGroup, input.viewDidLoad, renameGroup)
            .flatMapLatest...
        let deleteSubGroups = input.cellDeleteButtonTap
            .flatMapLatest...
        let dataSource = Driver.merge(getMenusInfo, deleteSubGroups)
        let loading = loadingTracker.asDriver()
        return Output(dataSource: dataSource, loading: loading)
    }
}
extension MenuSubGroupViewModel {
    struct Input {
        let createNewGroup: Driver<Void>
        let viewDidLoad: Driver<Void>
        let cellDeleteButtonTap: Driver<IndexPath>
        let cellRenameButtonTap: Driver<IndexPath>
    }
    struct Output {
        let dataSource: Driver<[MenuSubGroupViewController.CellSectionModel]>
        let loading: Driver<Bool>
    }
}

这里可能会有人疑问为什么会保存页面的数据呢,我们的数据不是直接通过网络请求生成一个序列绑定到TableView了吗?因为在某些业务场景下我们需要保存它,比如在网络请求错误的时候,我希望页面还会继续显示之前有数据的状态,这时候我们就可以在网络请求错误的序列中塞入我们之前保存的数据,这样页面还是显示原样,还有你注意没有这个属性是private的。 ActivityIndicator:可以监听网络请求的状态从而改变loading的状态,具体实现在下面代码中已经贴出。

createNewGroup :当点击页面上的新建分组按钮会发送一个序列作为ViewModel输入,通过flatMapLatest转换操作进入到下一页完成新建分组的操作,并将结果以序列的形式传回来。这里的saveData是一个PublishSubject类型,可接收也可发送序列,因为Driver只能接收而不能发送。如果成功就去刷新页面。

viewDidLoad:当ViewController调用viewDidLoad的方法的时候会发送一个序列作为ViewModel输入,通过transform转化dataSource输出去更新TableView。

cellDeleteButtonTap和cellRenameButtonTap: 点击cell中的按钮,会发出一个序列作为ViewModel输入,然后执行相应的业务代码,最后产生输出。

dataSource:TableView数据源序列,发生改变会去刷新TableView。

loading:控制页面loading状态的序列

ActivityIndicator核心代码

public class ActivityIndicator: SharedSequenceConvertibleType {
    fileprivate func trackActivityOfObservable<O: ObservableConvertibleType>(_ source: O) -> Observable<O.E> {
        return Observable.using({ () -> ActivityToken<O.E> in
            self.increment()
            return ActivityToken(source: source.asObservable(), disposeAction: self.decrement)
        }) { activity in
            return activity.asObservable()
        }
    }
    private func increment() {
        lock.lock()
        value += 1
        subject.onNext(value)
        lock.unlock()
    }
    private func decrement() {
        lock.lock()
        value -= 1
        subject.onNext(value)
        lock.unlock()
    }
}

ViewController中的核心代码

import UIKit
class MenuSubGroupViewController: UIViewController {
    private let cellDeleteButtonTap = PublishSubject<IndexPath>()  // 删除分组序列,cell中删除按钮点击时调用onNext方法发送序列
    private let cellRenameButtonTap = PublishSubject<IndexPath>() // 分组重命名序列,cell中重命名按钮点击时调用onNext方法发送序列

    // 初始化ViewModel的输入序列并进行ViewModel的输出序列绑定到View
    func bindViewModel() {
        let viewDidLoad = Driver<Void>.just(())
        let input = MenuSubGroupViewModel.Input(createNewGroup: createGroupButton.rx.tap.asDriver(),
                                                viewDidLoad: viewDidLoad,
                                                cellDeleteButtonTap: cellDeleteButtonTap.asDriverOnErrorJustComplete(),
                                                cellRenameButtonTap: cellRenameButtonTap.asDriverOnErrorJustComplete())
        
        let output = viewModel.transform(input: input)
        output.loading..
        output.dataSource
            .drive(tableView.rx.items(dataSource: dataSource))
            .disposed(by: disposeBag)
    }
  
    private lazy var dataSource: RxTableViewSectionedReloadDataSource<CellSectionModel> = {
        return RxTableViewSectionedReloadDataSource<CellSectionModel>(configureCell: { [weak self](_, tableView, indexPath, item) -> UITableViewCell in
            let cell: LabelButtonCell = tableView.dequeueReusableCell(LabelButtonCell.self)
            ...
            cell.rightButton1.rx.tap
                .subscribe(onNext: { [weak self] (_) in
                    self?.cellDeleteButtonTap.onNext(indexPath)
                })
                .disposed(by: cell.disposeBag)
            cell.rightButton2.rx.tap...
            return cell
        })
    }()
}

在这里RxDataSources的使用方法我就不再详细叙述了,所以说我们主要关注bindViewModel的方法,里面定义了页面的各种输入,并通过transform方法等得到输出的序列,再对TableView的数据源进行绑定。RxCocoa为我们提供了很多系统基础控件的Rx调用,可以很方便的进行数据绑定。

Navigator中的核心代码

class MenuSubGroupNavigator: BaseNavigator {
    func toMenuEditGroupVC(menuUid: String, dishGroupsInfo: DishGroupInfo? = nil) -> MenuEditGroupViewController {
        let navigator = MenuEditGroupNavigator(navigationController: navigationController)
        let viewModel = MenuEditGroupViewModel(navigator: navigator)
        let vc = MenuEditGroupViewController()
        vc.viewModel = viewModel
        navigationController?.pushViewController(vc, animated: true)
        return vc
    }
}

总结

  1. 要搭建一个上述的MVVM项目,RxSwift,RxDataSources,Moya是必不可少的,并且你要会用RxDataSource创建UITableView数据源,对RxSwift要有一定的了解。
  2. 在项目中对cell中的点击事件的处理方式是在ViewController里创建一个PublishSubject的序列,然后在事件回调或监听处主动调用onNext方法。
  3. 对于页面loading,无数据,无网等状态可以分别封装ViewController的Rx属性,然后通过ActivityIndicator可以监听网络请求的状态,发送序列从而改变页面状态。
  4. 上述的MVVM项目的很多操作都是通过序列来完成的,发生错误时可能不好定位。

RxSwift中文文档
RxSwift&MVVM