写在前面
这一系列笔记用于对泊学网RxSwift-step by step课程进行总结和摘选, 仅用于个人学习记忆.
笔记
01-异步事件是以时间为索引的常量队列
以时间为索引的常量队列的方式 这也是引入RxSwift的目的
02-安装RxSwift的三种不同的方式
学习使用playground
来编写文档
安装方式
1.mac环境下使用cocoapods安装
2.非mac环境下可以使用 SPM
安装
在编辑SPM自动生成的package.swift文件, 修改
然后退出编辑 执行swift build (会自动下载RxSwift 然后 自动执行build)
3.手动添加
将RxSwift(只是一个project)下载到当前目录下(这个指令值得深究)
03-RxSwift中的那些"术语"到底在说什么?
我们创建的Observable
, 表达的是异步操作, Observable
中的每一个元素, 都可以理解为一个异步发生的事件.因此,当我们对Observable
调用map和filter方法时,只表示我们要对事件序列中的元素进行处理的逻辑,而并不会立即对Observable
中的元素进行处理.
operator对Observable
的加工是在订阅的时候发生的.这种只有在订阅的时候才emit事件的Observable
,有一个专有的名字,叫做Cold Observable
.
言外之意,就是也有一种Observable
是只要创建了就会自动emit事件的,它们叫做Hot Observable
.在后面的内容中,我们会看到这类事件队列的用法.
对于有限观察序列, 执行到头时 资源就自动释放, 但是有时有些观察序列并非有限 如 timer序列. 所以就需要我们对其进行手动释放,
当然, RxSwfit也提供另一种方式, DisposeBag实例.
调用disposed(by: ) 将Observable
与 一个 DisposeBag绑定起来. 然后当这个bag实例销毁时, 所有其绑定的订阅者都将取消订阅 并 回收资源.
04 理解create和debug operator
理解 create函数中的subcribe参数的意义: 并非订阅, 而是 描述如何向订阅者发送订阅的消息
订阅
操作 do 旁路特性
debug(). 为Observable
执行debug()操作, 会将所以订阅到的消息打印出来,
05 四种Subject的基本用法
上节末尾,我们提到了Subject.既然它可以同时作为Observable
和 Observer
,我们就直奔主题,从一个叫做PublishSubject
的对象开始,感受下Subject
的用法:
PublishSubject
let subject = PublishSubject<String>()
subject.onNext("Episode1 updated")
// 第三,当我们把subject当作Observable的时候,订阅它的代码和订阅普通的Observable完全一样:
let sub1 = subject.subscribe(onNext: {
print("Sub1 - what happened: \($0)")
})
BehaviorSubject
如果你希望Subject从会员制
变成试用制
,就需要使用BehaviorSubject
.
ReplaySubject
ReplaySubject
的行为和BehaviorSubject
类似,都会给订阅者发送历史消息.不同地方有两点:
ReplaySubject
没有默认消息,订阅空的ReplaySubject
不会收到任何消息;ReplaySubject
自带一个缓冲区,当有订阅者订阅的时候,它会向订阅者发送缓冲区内的所有消息;
// ReplaySubject缓冲区的大小,是在创建的时候确定的:
let subject = ReplaySubject<String>.create(bufferSize: 2)
Variable
除了事件序列之外,在平时的编程中我们还经常需遇到一类场景,就是需要某个值是有"响应式"特性的.为了方便这个操作,RxSwift还提供了一个特殊的subject
,叫做Variable
.
// 我们可以像定义一个普通变量一样定义一个Variable:
let stringVariable = Variable("Episode1")
// 当我们要订阅一个Variable对象的时候,要先明确使用asObservable()方法.而不像其他subject一样直接订阅:
let stringVariable = Variable("Episode1")
let sub1 = stringVariable
.asObservable()
.subscribe {
print("sub1: \($0)")
}
// 而当我们要给一个Variable设置新值的时候,要明确访问它的value属性,而不是使用onNext方法:
stringVariable.value = "Episode2"
最后要说明的一点是,Variable
只用来表达一个"响应式"值的语义,因此,它有以下两点性质:
- 绝不会发生
.error
事件; - 无需手动给它发送
.complete
事件表示完成;
07 TodoDemo2
资源被正常回收了么?
此时,尽管已经可以正常添加Todo了,但是如果你足够细心就可以发现,控制台并没有打印Finsih adding a new todo.的提示.也就是说,在dismiss了TodoDetailViewController之后,todoSubject并没有释放,我们应该在某些地方导致了资源泄漏.
为了进一步确认这个问题,在Podfile中添加下面的内容:
post_install do |installer|
installer.pods_project.targets.each do |target|
if target.name == 'RxSwift'
target.build_configurations.each do |config|
if config.name == 'Debug'
config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['-D', 'TRACE_RESOURCES']
end
end
end
end
end
简单来说,就是找到项目中的RxSwift target
,在它的Debug
配置中,添加-D TRACE_RESOURCES
编译参数,并在Termianl
中重新执行pod install
更新下RxSwift
.然后,在TodoDetailViewController
的viewWillAppear
方法中,添加下面的代码:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
todoName.becomeFirstResponder()
todoItem = TodoItem()
print("Resource tracing: \(RxSwift.Resources.total)")
}
但事情至此还没结束,可能你会觉得这样写代码感觉怪怪的,甚至有些危险.因为我们要依赖一个Controller(TodoDetailViewController)中的某个属性(bag)
才能得以工作正常.而常规的开发经验通常告诉我们,如此密切的耦合关系通常是各种问题滋生的温床.这至多,只能算一个"非主流"的办法.
那么,更"主流"的办法是什么呢?
希望你还记得,对于一个Observable
来说,除了所有订阅者都取消订阅会导致其被回收之外,Observable
自然结束(onCompleted)
或发生错误结束(onError)
也会自动让所有订阅者取消订阅,并导致Observable
占用的资源被回收.
因此,当TodoDetailViewController dismiss
之后,实际上我们也不会再使用它添加新的Todo
了,这时,我们应该给todoSubject
发送onCompeleted
事件,明确告知RxSwift
,这个事件序列结束了.
08 ToDoDemo3
可以看到,是否同步成功是通过调用completionHandler
通知的,仿照之前的思路,我们可以让syncTodoToCloud
返回一个Observable<URL>
,其中的URL
是iCloud
保存在本地的路径:
func syncTodoToCloud() -> Observable<URL> {
// ...
return Observable.create({ observer in
plist.save(to: cloudUrl, for: .forOverwriting,
completionHandler: { (success: Bool) -> Void in
if success {
observer.onNext(cloudUrl)
observer.onCompleted()
} else {
observer.onError(SaveTodoError.cannotCreateFileOnCloud)
}
})
return Disposables.create()
})
}
要特别强调的是:onCompleted
对于自定义Observable
非常重要,通常我们要在onNext
之后,自动跟一个onCompleted
,以确保Observable
资源可以正确回收.
09 常用的忽略事件操作符
事例代码
example("ignoreElements") {
let tasks = PublishSubject<String>()
let bag = DisposeBag()
tasks.subscribe { print($0) }
.addDisposableTo(bag)
tasks.onNext("T1");
tasks.onNext("T2");
tasks.onNext("T3");
tasks.onCompleted();
}
/*
正常输出:
next(T1)
next(T2)
next(T3)
completed
*/
Ignore elementss
ignoreElements
会忽略序列中所有的.next
事件.
将事例代码中的订阅部分替换成以下:
tasks.ignoreElements()
.subscribe { print($0) }
.addDisposableTo(bag)
/*
输出:
completed
*/
用序列图描述, 就是这样的:
skip
skip可以用来忽略事件序列中特定个数的.next
.
tasks.skip(2)
.subscribe {
print($0)
}
.addDisposableTo(bag)
/*
输出:
next(T3)
completed
*/
用序列图描述, 就是这样的:
skipWhile / skipUntil
skipWhile
除了可以忽略指定个数的事件外,我们还可以通过一个closure
自定义忽略的条件,这个operator
叫做skipWhile
.但它和我们想象中有些不同的是,它不会"遍历"事件序列上的所有事件,而是当遇到第一个不满足条件的事件之后,就不再忽略
任何事件了.
使用如下代码替换订阅部分代码:
tasks.skipWhile {
$0 != "T2"
}
.subscribe {
print($0)
}
.addDisposableTo(bag)
输出:
next(T2) // 注意还是会订阅到T2的事件
next(T3)
completed
skipUntil
另外一个和skipWhile
类似的operator
是skipUntil
,它不用一个closure
指定忽略的条件,而是使用另外一个事件序列中的事件.例如,我们先把代码改成这样:
let tasks = PublishSubject<String>()
let bossIsAngry = PublishSubject<Void>()
let bag = DisposeBag()
tasks.skipUntil(bossIsAngry)
.subscribe {
print($0)
}
.addDisposableTo(bag)
tasks.onNext("T1");
tasks.onNext("T2");
tasks.onNext("T3");
tasks.onCompleted();
执行一下就会看到,我们不会订阅到任何事件.这就是skipUntil
的效果,它会一直忽略tasks
中的事件,直到bossIsAngry
中发生事件为止.
在T2与T3中插入如下代码
tasks.onNext("T1");
tasks.onNext("T2");
bossIsAngry.onNext();
tasks.onNext("T3");
输出:
next(T3)
completed
用序列图描述出来, 就是这样的:
distinctUntilChanged
distinctUntilChanged
可以用来忽略序列中连续 重复 的事件. 例如:
example("ignoreElements") {
let tasks = PublishSubject<String>()
let bag = DisposeBag()
tasks.distinctUntilChanged()
.subscribe {
print($0)
}
.addDisposableTo(bag)
tasks.onNext("T1")
tasks.onNext("T2")
tasks.onNext("T2")
tasks.onNext("T3")
tasks.onNext("T3")
tasks.onNext("T4")
tasks.onCompleted()
}
由于T2和T3都存在连续重复的事件, 因此我们只能订阅到下面这样的结果
next(T1)
next(T2)
next(T3)
next(T4)
completed
它的序列图是这样的:
但是,如果把T2放到两个T3中间,此时就没有任何连续重复的事件了,我们就会订阅到所有任务.
10 常用的获取事件操作符
elementAt
tasks.elementAt(1)
.subscribe {
print($0)
}
.addDisposableTo(bag)
/*
输出
next(T2)
completed
*/
filter
tasks.filter { $0 == "T2" }
.subscribe {
print($0)
}
.addDisposableTo(bag)
/*
输出:
next(T2)
completed
*/
take
除了选择订阅单一事件之外,我们也可以选择一次性订阅多个事件,例如,选择序列中的前两个事件:
tasks.take(2)
.subscribe {
print($0)
}
.addDisposableTo(bag)
takeWhile/takeWhileWithIndex
我们也可以用一个closure来指定"只要条件为true就一直订阅下去"这样的概念.例如,只要任务不是T3就一直订阅下去:
tasks.takeWhile {
$0 != "T3"
}
.subscribe {
print($0)
}
.addDisposableTo(bag)
控制台输出:
next(T1)
next(T2)
completed
takeWhileWithIndex
的closure
有两个参数,第一个是事件的值,第二个是事件在序列中的索引。它的语义和takeWhile
是完全一样的,需要注意的仍旧是,在closure
里写的,是读取事件的条件,而不是终止读取的条件.
takeUntil
example("ignoreElements") {
let tasks = PublishSubject<String>()
let bossHasGone = PublishSubject<Void>()
let bag = DisposeBag()
tasks.subscribe.takeUntil(bossHasGone) {
print($0)
}
.addDisposableTo(bag)
tasks.onNext("T1")
tasks.onNext("T2")
tasks.onNext("T3")
tasks.onCompleted()
}
tasks.onNext("T1")
tasks.onNext("T2")
bossHasGone.onNext()
tasks.onNext("T3")
tasks.onCompleted()
输出:
next(T1)
next(T2)
completed