此部分对于对于ReactiveX就不单独介绍了,详情请转至:ReactiveX website或者RxSwift GitHub repo

从RxRealm入手

我们可以通过CocoaPods简单的安装使用RxRealm这个extension:pod ‘RxRealm’,或者自行安装,详情参考文档source code from GitHub
这个库是封装好的Realm信号量集合。我们可以通过简单的订阅Results对象来刷新表视图,这仅仅只需要asObservable()操作。

1
2
3
4
5
6
let realm = try! Realm()
realm.objects(Lap)
.asObservable()
.subscribeNext {[weak self] laps in
self?.tableView.reloadData()
}

asObservable()在集合有改变的时候会发出信号,返回为Observable<Self>,收到信号的同时会得到最终改变过的集合,任由你处理。
如果你只是想拿到变动的集合,只需要使用asObservableChangeset()即可,返回形式为Observable<(Self, RealmChangeset?)>,代码如下:

1
2
3
4
5
6
7
8
9
10
11
realm.objects(Lap)
.asObservableChangeset()
.subscribeNext {result, changes in
if let changes = changes {
//it's an update
print("deleted: \(changes.deleted) inserted: \(changes.inserted) updated: \(changes.updated)")
} else {
//it's the initial data
print(result)
}
}

如果你仅仅是想处理一些中间情况或者筛选等等之类的操作,asObservableArray()最适合不过来:

1
2
3
4
5
6
7
8
realm.objects(Lap)
.asObservableArray()
.map {array in
return array.prefix(3) //slice off the first 3 items
}
.subscribeNext { text in
...
}

看起来是不是So easy???好,让我们来做个简单的小应用,包你分分钟上手RxRealm,从此以后爷爷奶奶都不用你担心你获取Realm操作的想哭了。

GitHub Search应用

这里我们手把手新建一个Github搜索应用,通过GitHub’s JSON API来做查询,让应用可以模糊搜索匹配Repository,是时候展现Realm + Rx真正的实力了。

懒魔福利:源码

做这个简单的小应用可以自行搭建工程,自行采用适合的架构即可,不一定非要MVVM,只需要能搜索到如下三种语言结果即可:

接口交互

稍微提醒下,以下几部分代码都是基于你对RxSwift和RxCocoa有所了解,如果还有不了解的请转至文首部分。

我们首先要做的工作就是把输入条件转化为接口查询条件,需要做的就是把输入条件和选择条件合并为一个信号条件,最后用作接口调用的参数即可:

1
2
3
4
let input = Observable.combineLatest(
query.rx_text.filter {$0.utf8.count > 2}, language.rx_selectedTitle)
{term, language in (term, language!)}
.shareReplay(1)

我们将输入的最新文本(长度至少3个字符,节省网络请求)以及segment中的最后一个选定标题。并将它们组合成元组,以供订阅。

接下来让我们订阅输入并对GitHub进行网络调用:

1
2
3
4
5
input.throttle(0.5, scheduler: MainScheduler.instance)
.map(NSURL.gitHubSearch)
.flatMapLatest { url in
return NSURLSession.sharedSession().rx_JSON(url)
}

通过调节输入频率来节约网络请求,并生成合适的URL来做网络请求,利用rx_JSON做数据转换。
最后只需要做简单的数据解析,转换成realm对象保存即可。

1
2
3
4
5
6
7
8
... // Code from above
.observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
.map {json -> [Repo] in
guard let json = json as? [String: AnyObject],
items = json["items"] as? [AnyObject] else {return []}

return items.map {Repo(value: $0)}
}

我们切换到后台调度队列,然后将JSON映射做数据解析即可。

注意:如果JSON与预期属性不匹配,会crash,此代码仅用于测试用,生产中要处理解析错误的情况。

最后,我们可以将Repo对象存储到Realm中。此时,代码已经在后台线程上运行(并且不再更改线程,Realm对象是线程安全的),最后订阅一盘就可以用来存储对象了:

1
2
3
4
5
6
7
8
... // Code from above
.subscribeNext {repos in
let realm = try! Realm()
try! realm.write {
realm.add(repos, update: true)
}
}
.addDisposableTo(bag)

在最后一段代码中,我们从当前线程中开一个Realm实例,添加所有的返回对象,并将订阅添加到dispose包中。
现在我们已经订购了驱动整个序列的订阅:从验证和限制用户输入,到制作异步网络调用,解析和转换JSON,最后创建Realm对象并将它们存储在本地。
接下来我们看下如何使用RxRealm并使用Realm的内置通知。

利用RxRealm驱动UI

接下来,我们将对用户输入进行另一次订阅,过滤存储在Realm中的对象,并在视图控制器的表视图中显示结果。

这里我们把表叔图刷新的逻辑抽出来,减少代码冗余:

1
2
3
4
5
input
.subscribeNext {[weak self] in
self?.bindTableView($0, language: $1)
}
.addDisposableTo(bag)

现在,任何时候用户输入一个有效的搜索词,就会调用bindTableView(_, language: _)
但是,当用户删除搜索词时会发生什么?我们再为这种情况添加另一个订阅:

1
2
3
4
5
query.rx_text.filter {$0.utf8.count <= 2}
.subscribeNext {[weak self] _ in
self?.bindTableView(nil)
}
.addDisposableTo(bag)

如果搜索项短于3个字符,则会bindTableView(_, language: _)用nil参数调用。

接着是bindTableView逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
private var repos: Results<Repo>?

func bindTableView(term: String?, language: String? = nil) {
resultsBag = DisposeBag()

guard let term = term, let language = language else {
repos = nil
tableView.reloadData()
return
}

...

如果给bindTableView传递nil参数,代码将置空repos并刷新表视图(隐藏任何以前的结果可见)。

接下来让我们找到用户感兴趣的结果:

1
2
3
...
repos = realm.objects(Repo).filter("full_name CONTAINS[c] %@ AND language = %@", term, language)
...

然后我们订阅变化的存储对象用于显示:

1
2
3
4
5
6
7
8
9
...
repos!.asObservableChangeset()
.subscribeNext {[weak self] repos, changes in
guard let tableView = self?.tableView else { return }

... // Do some table view magic here
}
.addDisposableTo(resultsBag)
...

现在,当用户输入搜索词时,会发生什么情况:

  • 该应用将向GitHub发起网络请求
  • 该应用将显示搜索词的本地搜索结果
  • 当JSON解析完成后,会被存储到本地
  • 结果的更改将通过上述订阅完成,因此也能更新表格视图

Nice,该应用具有本地缓存​​,因此它可以脱机工作以及在线工作。所以,它会显示即时结果,如果本地没有任何缓存,它也会处理来自网络的响应!

最后我们把搜索到服务端返回的结果,保存并显示出来:

1
2
3
4
5
6
7
8
9
// Table view magic
if let changes = changes {
tableView.beginUpdates()
tableView.insertRowsAtIndexPaths(changes.inserted.map { NSIndexPath(forRow: $0, inSection: 0) },
withRowAnimation: .Automatic)
tableView.endUpdates()
} else {
tableView.reloadData()
}

为了尽快展示本地搜索结果,我们调用tableView.reloadData()。相反,为了使通过网络返回的新结果更加明显,我们使用动画进行更新。

注意:因为在这个演示中我们只添加到本地缓存中,所以我们只关心changes.inserted。如果你还期望对象更新或删除,你应该处理changes.updated和changes.deleted。有关更多如何使用变更通知,请查看此帖

测试

让我们运行应用并搜索“animation”并尝试“Swift”和“Objective-C”之间切换来观察结果:

接着我们来测试本地和网络同时筛选的情况,根据第一条的关键字Easy来搜索:

正如看到的,本地结果会立刻展示,网络请求得到的结果会通过动画的形式展现出来。

最后测试删除结果,并清空列表:

结尾

这里就展示完了通过Observable.changeset(from: objects)来处理变更通知的基本操作,对于changes.updated和changes.deleted可以自行尝试,当然这个扩展库远不止这些,还有比如批量添加Realm.rx.add()和批量删除Realm.rx.delete()这种骚操作也是很容易信手拈来的。