使用 RxSwift ,代码可读性很好
声明式编程,把要做的,一行行写下
看代码,像看爽文
定义都是静态的,不可变的
关注的是数据的变化。
需要良好的设计。
平常写 OOP, 哪里改,写那里。增补一番
写 FRP,指定位置,统一前置了
下面的例子是,一个搜索列表的功能
代码简练,不多
let API = DefaultWikipediaAPI.sharedAPI
let results = searchBar.rx.text.orEmpty // 搜索框的文本
.asDriver()
.throttle(.milliseconds(300)) // 300 毫秒内的事件,只取一个
.distinctUntilChanged() // 不变化,就不用管
.flatMapLatest { query in // 文本,映射成网络查询
API.getSearchResults(query) // 去查询
.retry(3)
// 失败了,重试三次
.retryOnBecomesReachable([], reachabilityService: Dependencies.sharedDependencies.reachabilityService)
// 有网,才重试
.startWith([]) // 先清空,再出结果
.asDriver(onErrorJustReturn: [])
// 出错,返回 []
}
.map { results in
results.map(SearchResultViewModel.init)
// Json 转 Model
}
results
.drive(resultsTableView.rx.items(cellIdentifier: "WikipediaSearchCell", cellType: WikipediaSearchCell.self)) { (_, viewModel, cell) in
// Model 出界面
cell.viewModel = viewModel
}
.disposed(by: disposeBag)
// 界面退出,disposeBag 销毁, 订阅销毁
RxSwift 给了开发者很好的基础设施
- 可以用同步的方式,写异步的代码
AF.request("https://httpbin.org/get").response { response in
debugPrint(response)
}
Alamofire 默认采样内联闭包的方式,处理异步的网络请求
RxSwift 的语法,更加优雅
func getSearchResults(_ query: String) -> Observable<[WikipediaSearchResult]>
- 响应式编程, 可以做数据绑定
observable 把数据和事件,包裹在一起。
建立响应,需要数据和事件, 数据是流动的,是最新状态的数据
事件,就是通知来事情了,告诉订阅者有消息
进入套路写法
1,要处理异步任务的函数,
// input -> output
// Value -> Observable<T>
func reachedBottom(offset: CGFloat = 500.0) -> ControlEvent<Bool>
函数,有输入,有输出,输出是异步的,
异步编程,事件尚未发生,先把流程整理清楚
处理异步,不是通过添加参数,熟悉的内联闭包
2,要处理异步任务的类,一般就是 ViewModel
ViewModel 也是有输入,有输出
Swift 的类,有协议支持,可以针对输入、输出作出行为规范
与上面的函数,继续区别,
类可以有状态,可以有几个属性,记录下当前的状态,
函数是无状态的,没有属性来记录
例子是 Papr 的首页
2.1 ,找出所有的输入,给 viewModel
上图中有 3 个,下拉刷新 ( 顶上的刷新控件 ),上拉更多,右上角按钮切换最新/最热
class HomeViewController{
func bindViewModel() {
let inputs = viewModel.inputs
let rightBarButtonItemTap = rightBarButtonItem.rx.tap.share()
// 连输入
rightBarButtonItemTap
.scan(into: OrderBy.latest) { result, _ in
result = (result == .latest) ? .popular : .latest
}
.bind(to: inputs.orderByProperty)
.disposed(by: disposeBag)
rightBarButtonItemTap
.merge(with: refreshControl.rx.controlEvent(.valueChanged).asObservable())
.map(to: true)
.bind(to: inputs.refreshProperty)
.disposed(by: disposeBag)
collectionView.rx.reachedBottom()
.bind(to: inputs.loadMoreProperty)
.disposed(by: disposeBag)
// ...
}
}
将三种原始输入关联到,关心的三种事件
protocol HomeViewModelInput {
// 刷新第一页
var refreshProperty: BehaviorSubject<Bool> { get }
// 刷新更多页
var loadMoreProperty: BehaviorSubject<Bool> { get }
// 更改排序逻辑,最新,还是最热
var orderByProperty: BehaviorSubject<OrderBy> { get }
}
protocol HomeViewModelType {
var inputs: HomeViewModelInput { get }
var outputs: HomeViewModelOutput { get }
}
final class HomeViewModel: HomeViewModelType, HomeViewModelInput, HomeViewModelOutput {
var inputs: HomeViewModelInput { return self }
var outputs: HomeViewModelOutput { return self }
let refreshProperty = BehaviorSubject<Bool>(value: true)
let loadMoreProperty = BehaviorSubject<Bool>(value: false)
let orderByProperty = BehaviorSubject<OrderBy>(value: .latest)
协议有点绕,有输入协议,有输出协议,有输入输出协议。
好处是,代码语意明确
viewModel.inputs
, 就是 viewModel
,
安全点
viewModel.inputs
, 只能访问 viewModel
输入的相关属性
3. BehaviorSubject 的使用
Subject 既是事件流 Observable, 能够接受订阅,
也是事件的观察者 Observer ,可以产生事件
观察者对 BehaviorSubject 进行订阅时,BehaviorSubject 会将源 Observable 中最新的元素发送出来(如果不存在最新的元素,就发默认元素 ) , 接着发送随后产生的元素
这个例子的场景下,就是用户刚进来,用户没点击,没产生事件流 observable,
也要有内容看啊,这里使用了 BehaviorSubject 发送默认的特性。
简单理解, viewDidLoad
里面有个网络请求界面初始化,简化为 BehaviorSubject
- 不需要初始状态,可采用 PublishSubject,PublishSubject 只发送事件给当前的订阅者
来一个事件,处理一个
4, 继续说 ViewModel
上图中,用户的行为,构成了 ViewModel 的输入,
要展示给用户的状态,构成了 ViewModel 的输出
上图中,要管理的输出状态,有五个,
- 点击右上角按钮,切换最新/最热的图片
- 是否在请求第一页,控制刷新控件的旋转
- 请求第一页的时候,在刷新。和底部上拉刷新更多的时候,右上角按钮,不可点击
- 请求第一页的时候,刷新控件旋转。请求第一页完成,刷新控件隐藏。
- 网络请求到的数据,列表展示出来
class HomeViewController{
func bindViewModel() {
let outputs = viewModel.outputs
// ...
// 用输出
outputs.isOrderBy
.map { $0 == .popular ? Papr.Appearance.Icon.flame : Papr.Appearance.Icon.arrowUpRight }
.bind(to: rightBarButtonItem.rx.image)
.disposed(by: disposeBag)
outputs.isFirstPageRequested
.negate()
.bind(to: inputs.refreshProperty)
.disposed(by: disposeBag)
outputs.isRefreshing
.merge(with: outputs.isLoadingMore)
.negate()
.bind(to: rightBarButtonItem.rx.isEnabled)
.disposed(by: disposeBag)
outputs.isRefreshing
.bind(to: refreshControl.rx.isRefreshing(in: collectionView))
.disposed(by: disposeBag)
outputs.homeViewCellModelTypes
.map {
[HomeSectionModel(model: "", items: $0)]
}
.bind(to: collectionView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
}
MVVM 不难。找出输入,转为 Observable, 交给 ViewModel .
要管理的状态,就是 ViewModel 输出的 observable,Subscribe 掉。
bind ,即把 observable , 交给一个可以 Subscribe 的方法,同样消耗掉, 给 fire 了
ViewModel 只是一个容器,集中化管理,数据流非常的清晰。
他做的事情,就是把输入变成输出,通过各种 operator
RxSwift 提供了强大的基础设施,各种好用的 operator
ViewModel 里面的代码同样使用声明式编程,很好读,没什么废话。
这里就省去了
5, RxSwift 可以省去中间状态,Less stateful
上图右上角的按钮,来回切,选择最新/ 最热
常规的 OOP , 会有一个记录属性,记录上一步的状态
这里采用了 operator 扫描 scan
scan
跟 operator reduce
类似,
scan
多了一个中间状态,可以把上一步的状态,带过来。正适合这里的场景。
Observable.of(10, 100, 1000)
.scan(1) { lastValue, newValue in
lastValue + newValue
}
.subscribe(onNext: { print($0) })
.disposed(by: bag)
一般 scan
这么调用,提供一个初始值,闭包中拿上一步的值和新的值,来计算
11
111
1111
这里的
class HomeViewController{
func bindViewModel() {
rightBarButtonItemTap
.scan(into: OrderBy.latest) { result, _ in
result = (result == .latest) ? .popular : .latest
}
.bind(to: inputs.orderByProperty)
.disposed(by: disposeBag)
}
}
这里 scan
的初始值,与 ViewModel 的初始值,保持一致。
都是最新 OrderBy.latest
final class HomeViewModel{
let orderByProperty = BehaviorSubject<OrderBy>(value: .latest)
}
scan
里面的闭包,就是简单的采用了上一个状态,做了一个 toggle,
没有采用新值计算
5.1,数据是流动的,一般处理最新状态的数据,不是处理静态的数据
专心于数据的变化,不是很关心事件的来源
可观察好处: 省很多事...
如果不可观察,就需要很多胶水代码,保持同步。
模型变了,同步 UI
UI 改了,同步模型
双向绑定,就是可观察,
A 变,需要 B C D 跟着变。
用 observable ,自动处理。
使用 OOP, 手动处理。改完 A ,还要手动改 B C D .
- 如果需要知道事件的来源,可以采用 Enum ,
Swift 的 Enum, 可以有关联值, Associated Values ,这样统一类型来源和 Value,
- 更复杂的状态追踪,可以用结构体
5.2, ViewModel 也可以做控制器的路由跳转
代码见 jdisho/Papr