RxSwift学习记录

概述

  • RxSwift顾名思义是Swift的一种框架,您或许曾经听说过「响应式编程」(Reactive Programming) 一词,甚至很可能研究过 RxSwift 的相关内容。但是如果您没有在日常开发中使用响应式编程的话,那么您就真的落后于时代了!RxSwift就是对于Swift而言进行响应式编程而言的框架,由Github的ReactiveX组织开发,维护。最近看了很多关于这方面的文档案例,这里也做个简单记录,方便自我学习。

概念

  • Rx的核心就是Observable Sequence(可被观察的序列),在Rx中,基本上所有的东西要么是一个可观察的序列对象,要么就是需要和可观察的对象进项协同工作的。因此序列对象将会按需推出其中的内容,供订阅者订阅可观察序列。

  • Rx能做什么,在传统的业务逻辑中,经常会涉及到代理,方法,通知,KVO等,这些都可以采用Rx来实现,而且更为简便。
    Rx操作符执行各种任务,是基于事件的,一般是以异步的方式执行的

Rx模式

  • Rx常见的两种模式
    1.首先是观察者模式(Obsercer),它是管理一系列从属单元的对象,其中包括了观察者和订阅者,一旦发生变化就会发送通知。
    2.迭代模式,这样集合或者序列中的值就可以进=行遍历了

可观察序列对象的生命周期 (7:25)

这个箭头表示可观察序列对象随时间变化的情况,当某个值或者一系列值被放到序列对象当中的时候,它就会给它的观察者发送一个「下一步 (Next)」事件,而这个事件当中将会包含这些新增的元素。再次强调一遍,这个过程称之为发送 (Emitting),而那些值将变成元素 (Element)。不管这里的这些值是什么,比如说触摸事件、点击事件、TouchInside 事件还是什么,它们的工作方式都是相同的。(顺便提一点,这被称之为 Marble 图。)


11513231269_.pic.jpg

如果遇到了错误,那么序列对象将会发送一个错误事件 (Error Event),这其中将包含有错误类型实例,这样您就可以对这个事件做出回应,以便执行错误处理,问询该错误,以便查看哪里出了问题。当错误发生之后,也就是这条时间线上的 X 表示的地方,它同样会立刻终止这个序列,当序列中止之后,它就没办法再发送更多的事件了,因此一旦您接取到了错误事件,就意味着这个序列已经死掉了。

序列也可以正常终止,而当序列正常终止之后,它将会发送一个完成事件 (Completed Event),也就是时间线上的竖线表示的地方。

Rx做了什么?

RxSwift把我们程序中每一个操作都看成一个事件,比如一个TextField中的文本改变,一个按钮被点击,或者一个网络请求结束等,每一个事件源就可以看成一个管道,也就是sequence,比如TextField,当我们改变里面的文本的时候,这个TextField就会不断的发出事件,从他的这个sequence中不断的流出,我们只需要监听这个sequence,每流出一个事件就做相应的处理。同理,Button也是一个sequence,每点击一次就流出一个事件。也就是我们把每一步都想成是一个事件就好去理解RxSwift了。

Observable 和 Observer

比如一个宝宝在睡觉,爸爸妈妈不可能时时刻刻待在那看着吧?那样子太累
了。他们该做啥做啥,只要听到宝宝哭声的时候,他们给宝宝喂奶就行了。这就是一个简单的观察者模式。宝宝是被观察者,爸爸妈妈是观察者也称作订阅者,只要被观察者发出了某一个事件,比如宝宝哭声,叫声都是一个事件,订阅者就会做出相应地响应。

理解了观察者模式这两个概念就很好理解了,Observable就是可被观察的,也就是我们说的宝宝,他也是事件源。而Observer就是我们的观察者,也就是当收到事件的时候去做某些处理的爸爸妈妈。观察者需要去订阅(subscribe)被观察者,才能收到Observable的事件通知消息。

observeOn() 和 subscribeOn()

subscribeOn()设置起点在哪个线程,observeOn()设置了后续工作在哪个线程

someObservable
.doOneThing() // 1
.observeOn(MainRouteScheduler.instance) // 2
.subscribeOn(OtherScheduler.instance) // 3
.subscribeNext { // 4
......
}
.addDisposableTo(disposeBag)

1.所有动作都发生在当前的默认线程
2.observeOn()转换线程到主线程,下面所有的操作都在主线程
3.subscribeOn()规定动作一开始不是发生在默认线程,而是在OtherScheduler了。
4.如果我们之前没有调用observeOn(),那么这边会在OtherScheduler发生,但是我们前面调用了observeOn(),所以这个动作会在主线程中调用。

总结一下:subscribeOn()只是影响事件链开始默认的线程,而observeOn()规定了下一步动作发生在哪个线程中。

.share(replay: )

共享监听 保证只执行一次 ,对于一个序列的多次订阅以及绑定的时候防止多次执行
信号处理的顺序

Observable有个隐式的约定,那就是在一个信号处理完成之前,不会发送下一个信号,不管发送信号的线程是并发的or串行的。

比如

someObservable
.subscribe { (e: Event<Element>) in
print("Event processing started")
// processing
print("Event processing ended")
}

只会出现

Event processing started
Event processing ended
Event processing started
Event processing ended
Event processing started
Event processing ended

不会出现

Event processing started
Event processing started
Event processing ended
Event processing ended

  • 第一个例子

我们监听textfield的文字变化,然后,Log出text,当button点击的时候,取消这次监听

class ObservableAndCancelController : UIViewController{
var subscription:Disposable?

@IBOutlet weak var textfield: UITextField!
@IBAction func cancelObserve(sender: AnyObject) {
    subscription?.dispose()
}
override func viewDidLoad() {
    super.viewDidLoad()
    subscription = textfield.rx_text.subscribeNext { (text) in
        print(text)
    }
}

}

RxSwift用extensiton的方式,为UITextfield,UIlabel等控件添加了很多可监听的属性,这里的textfield.rx_text就是一个

效果:随着文字输入,实时Log出textfield的文字,当点击button之后,再输入,则不会Log

操作符(Operators)

在上文的第一个例子里面,你看到了监听信号,并且log出值。事实上,这样直接处理信号的时候是很少的,很多时候,我们需要对信号进行映射,过滤,这时候我们就要用到操作符了。在这个文档里,你可以找到所有的操作符。

关于操作符效果,你可以参见http://rxmarbles.com/的可视化效果,这会给你一个更好的理解

  • 例子二,map,filter,combineLatest

map 对信号(Element)进行映射处理。比如输入是String,影射到Bool
filter 对信号(Element)进行过滤处理。返回信号,和输入的信号是同一种类型
combineLatest 对两种信号的值进行结合。可以返回不同种类的信号。
例如

let firstObserverable = firstTextfield.rx_text.map({"first" + $0})
let secondObserverable = secondTextfield.rx_text.filter({$0.characters.count > 3})
_ = Observable.combineLatest(firstObserverable, secondObserverable, resultSelector:{ ($0 + $1,$0.characters.count + $1.characters.count)}).subscribeNext { (element) in
print("combineLatest:(element)")
}

对于,每一个fistTextfield的信号,在字符串开始处增加”first”;对secondTextfield的信号进行过滤,当长度大于3的时候,才会继续传递。对两个信号进行结合,取truple类型,然后打印出来。

所以,当我在fistTextfield中,输入1234,然后secondTextfield中依次输入abcdefg的时候

combineLatest:("first1234abcd", 13)
combineLatest:("first1234abcd3", 14)
combineLatest:("first1234abcd", 13)
combineLatest:("first1234abcde", 14)
combineLatest:("first1234abcdef", 15)
combineLatest:("first1234abcdefg", 16)

  • 例子三,创建一个Observable

Observerable可以用来处理任务,并且异步返回Event信号(Next,Error,Completion)

比如,这样一个方法

//Observable就是处理输入,并且把description发送出去

func createObserveable(object:AnyObject?)->Observable<String?>{
return Observable.create({ observer in
observer.onNext(object?.description)
observer.onCompleted()
return NopDisposable.instance
})
}

这样调用

_ = createObserveable(test).subscribe({ (event) in
switch event{
case .Next(let value):
print(value)
case .Completed:
print("Completed")
case .Error(let error):
print(error)
}
})

然后,Log如下

Optional("{\n a = b;\n 1 = 2;\n}")
Completed

可以看到,创建一个Observable相当容易,调用Observable.create,在必要的时候发送onNext,onError,onCompleted信号。然后返回一个Disposable用来取消信号

throttle/retry/distinctUntilChanged/flatMapLatest

  • throttle 忽略上一个信号的一段时间的变化,也就是说一段时间没有新的信号输入,才会向下发送

  • distinctUntilChanged 直到信号改变了再发送

  • retry 如果失败,重新尝试的次数(Drive序列是不允许发出error所以也就没有尝试的操作符)

  • flatMapLatest 仅仅执行最新的信号,当有新的信号来的时候,取消上一次未执行完的整个序列
    最直接的例子就是搜索,通常我们想要

  • 用户用一段时间没有输入的时候,在进进行网络请求,不然网络请求太频繁,对客户端和服务器都是负担
    当新的请求来的时候,如果上一个未完成,则取消上一个
    如果网络失败,能重新请求几次就更好了
    这时候,用RxSwift你得代码会变的非常简单

let searchResults = searchBar.rx_text
.throttle(0.3, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.flatMapLatest { query -> Observable<[Repository]> in
if query.isEmpty {
return Observable.just([])
}
return doSearchAPI(query).retry(3)
.catchErrorJustReturn([])
}
.observeOn(MainScheduler.instance)

这里简单讲解下作用

throttle(0.3, scheduler: MainScheduler.instance) 保证用户没有输入0.3秒后再进行下一步
distinctUntilChanged() 假如0.3秒之前输入是ab,0.3秒后还是ab,则不会进行下一步,只有改变了才会进行下一步
flatMapLatest 保证只搜索最新的,如果之前的没有完成,会被自动取消
doSearchAPI(query).retry(3) 保证,如果发生错误,自动重试3次

Schedulers

Schedulers 抽象化了线程,线程池,GCD中操作队列,Runloop等概念。可以理解为,Schedulers就是一个执行任务的线程。

有一点要注意:默认一个Observerable在其创建的线程上执行

与Schedulers相关的操作符有两个

observeOn(scheduler) 在一个scheduler上执行任务,使用场景较多
subscribeOn(scheduler) 在一个scheduler进行监听
比如

sequence1
.observeOn(backgroundScheduler)
.map { n in
print("This is performed on the background scheduler")
}
.observeOn(MainScheduler.instance)
.map { n in
print("This is performed on the main scheduler")
}.subscribeOn(backgroundScheduler)
.subscribeNext{ n in
print("This is performed on the background scheduler")
}

  • 默认一个subscribeNext或者subscribe在其调用的线程上执行

Serial/Concurrent Schedulers 串行或并行

和GCD的队列很相似,并行Schedulers的任务可以并发之行,串行Schedulers只能依次之行。不过RxSwift有内部机制,保证上文提到的信号处理的顺序

RxSwift内置的Scheduler

通常,使用内置的Scheduler足矣。

CurrentThreadScheduler(串行) 当前线程Scheduler,默认使用的
MainScheduler(串行) 主线程
SerialDispatchQueueScheduler 封装了GCD的串行队列
ConcurrentDispatchQueueScheduler 封装了GCD的并行队列,这个在有任务要在后台执行的时候很有用
OperationQueueScheduler 封装了NSOperationQueue
例子四,在后台Scheduler之行任务,然后在主线程上更新UI

Variable

Variable表示一个可监听的数据结构。使用Variable,你可以监听数据的变化,也可以把其他值绑定到它身上。

当Variable被释放的时候,它会向监听者发送onCompleted

例子五,Variable进行监听

class VariableController: UIViewController {

@IBOutlet weak var label: UILabel!
var timer:NSTimer?
var count = Variable(0)
override func viewDidLoad() {
    super.viewDidLoad()
    timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector:#selector(VariableController.updateValue) , userInfo: nil, repeats: true)
    _ = count.asObservable().subscribeNext { (num) in
        self.label?.text = "VariableValue:\(num)"
    }
}
func updateValue(){
    count.value = count.value + 1
}
override func viewDidDisappear(animated: Bool) {
    super.viewDidDisappear(animated)
    timer?.invalidate()
} }

数据绑定

数据绑定是开发的时候很常见的,比如根据文本的输入动态调整textfield的背景色,动态调整按钮的enable。亦或者根据textfield的输入变化,动态的去反馈到model层。如果你听过MVVM,那你肯定知道,MVVM的难点就是ViewModel与View的数据绑定问题。

不过,使用RxSwift,数据绑定变的十分容易,你甚至可以把数据绑定到tableview和collectionView上去。

  • 例子六,bindTo
    很简单,随着Switch的开关,view进行显示/隐藏

只需要一行代码

_ = mySwitch.rx_value.bindTo(testView.rx_hidden)

  • 例子七,根据输入,进行View状态绑定

我们想要实现这样的状态

用户名至少6位,小于6位,则背景色是灰色,合法则透明
密码至少位8位,小于8位,则背景色是灰色,合法则透明
当用户名和密码都合法的时候,注册按钮enable,并且背景色变红

信号的处理方式如下,

  >  let nameObserable = nameTextfield.rx_text.shareReplay(1).map({$0.characters.count >= 6}) 
    let pwdObserable = passwordTextfield.rx_text.shareReplay(1).map({$0.characters.count >= 8})

    _ = nameObserable.subscribeNext({ (valid) in
        self.nameTextfield.backgroundColor = valid ? UIColor.clearColor():UIColor.lightGrayColor()
    }).addDisposableTo(disposeBag)

    _ = pwdObserable.subscribeNext({ (valid) in
        self.passwordTextfield.backgroundColor = valid ? UIColor.clearColor(): UIColor.lightGrayColor()
    }).addDisposableTo(disposeBag)//addDisposableTo(disposeBag)是为了自动释放

    _ = Observable.combineLatest(nameObserable, pwdObserable) {$0 && $1}.subscribeNext({valid in
            if valid{
                self.registerButton.enabled = true
                self.registerButton.backgroundColor = UIColor.redColor()
            }else{
                self.registerButton.enabled = false
                self.registerButton.backgroundColor = UIColor.darkGrayColor()
            }
        }).addDisposableTo(disposeBag)
    _ = registerButton.rx_tap.shareReplay(1).subscribeNext {
        print("Button tapped")
    }

共享监听Sharing subscription-shareReplay

这个是很常用的,比如一个Obserable用做网络请求,通常,当你这样调用的时候,会创建两个序列,也就是会进行两次网络请求,这是不需要的

let network = networkWithText(text)
let subscription1 = network
.subscribeNext { n in
//创建第一个序列 }
let subscription2 = network
.subscribeNext { n in
//创建第二个序列
}

为了共享一个序列,你只需要这这样调用

let network = networkWithText(text).shareReplay(1)
1
就只会进行一次网络请求,两个subscription共享结果,也就是shareReplay的意思

自定义可绑定属性

上文,textfield和button的状态绑定是手动的,这无疑是不方便的。RxSwift为我们提供了一种方式,来自定义可绑定属性

创建两个exetnsion

//设置绑定属性

extension BCShop {
var ex_shopState:AnyObserver<[BCSection]>{
return Binder(self) { homeVC, state in
self.shopTab.reloadData()
}.asObserver()
}
}

extension UIButton {
var ex_State:AnyObserver<Bool>{
return Binder(self) { button, state in
button.isEnabled = state
button.backgroundColor = state ? UIColor.purple : UIColor.gray
button.titleLabel?.textColor = state ? UIColor.white : UIColor.orange
}.asObserver()
}
}

然后,上文的代码,就可以简化成三行了,So easy

    _ = nameObserable.bindTo(nameTextfield.ex_validState).addDisposableTo(disposeBag)
    _ = pwdObserable.bindTo(passwordTextfield.ex_validState).addDisposableTo(disposeBag)

    _ = Observable.combineLatest(nameObserable, pwdObserable) {$0 && $1}.bindTo(registerButton.ex_validState).addDisposableTo(disposeBag)

Driver(老司机)

Driver是RxSwift精心制作的,专门提供给UI层的一个接口。
利用Driver你可以

利用CoreData的模型来驱动UI
利用UI的状态来绑定其他UI的状态
Driver能够保证,在主线程上监听,因为UIKit不是需要在主线程上操作

Tips: RxSwift中做数据绑定有三种

  • 利用BindTo方法
  • 利用Driver(强烈建议使用这个,)
    -利用KVO来手动绑定(很少用到)
    回到Driver上来,上文提到了,对于搜索,我们可以这么做,

let results = query.rx_text
.throttle(0.3, scheduler: MainScheduler.instance) //延迟0.3秒
.flatMapLatest { query in //永远只执行最新的
searchWithText(query)
}

results
.map { "($0.count)" }
.bindTo(resultCount.rx_text)//绑定label
.disposed(by: disposeBag)

results
.bindTo(resultsTableView.rx_itemsWithCellIdentifier("Cell")) { (_, result, cell) in //绑定tableview
cell.textLabel?.text = "(result)"
}
.disposed(by: disposeBag)

那么,这有什么缺陷呢?

  • 假如searchWithText失败了,那么整个序列就断掉了,后面的绑定不会有任何作用
    假如searchWithText是在后台线程执行的,那么后续绑定是在后台线程上进行的,会崩溃
    绑定了两次,意味着会执行两次
    于是,我们需要进行额外的操作,来避免上述缺陷。

let results = query.rx_text
.throttle(0.3, scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
.observeOn(MainScheduler.instance) // 保证在主线程(解决缺陷1)
.catchErrorJustReturn([]) // 发生错误,返回空数组(解决缺陷2)
}
.shareReplay(1) // 共享监听,保证只执行一次(解决缺陷3)
results
.map { "($0.count)" }
.bindTo(resultCount.rx_text)
.disposed(by: disposeBag)

results
.bindTo(resultTableView.rx_itemsWithCellIdentifier("Cell")) { (_, result, cell) in
cell.textLabel?.text = "(result)"
}
.disposed(by: disposeBag)

利用Driver我们可以将上述过程简化

let results = query.rx_text.asDriver() // 转换成Driver序列
.throttle(0.3, scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
.asDriver(onErrorJustReturn: []) // 告诉Driver发生错误怎么办
}

results
.map { "($0.count)" }
.drive(resultCount.rx_text) // 用Driver绑定,不需要切换到主线程
.addDisposableTo(disposeBag)
results
.drive(resultTableView.rx_itemsWithCellIdentifier("Cell")) { (_, result, cell) in
cell.textLabel?.text = "(result)"
}
.addDisposableTo(disposeBag)

  • 任何满足以下三个条件的Observer序列都可以转换为Driver

1.不会因为错误就序列断掉(比如,有错误,但是没有调用onError来发送错误)
2.在主线程傻姑娘监听
3.共享 side effects
对于,使用者只需要调用asDriver(onErrorJustReturn: [])就能保证上述三点都实现了

KVO

通常的KVO,你需要在这个函数里来处理

-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context

而使用RxSwift,KVO变成了这样

_ = self.view.rx.observe(CGRect.self, "frame")
.subscribe { (frame) in
print(frame.element!!)
}

或者这样

_ = self.view.rx.observeWeakly(CGRect.self, "frame")
.subscribe(onNext: { (frame) in
print(frame!)
})

二者的区别是

在rx_observe可以使用地方都可以使用rx_observeWeakly。rx_observeWeakly的执行效率要低一点,因为要处理对象的dealloc关系。除此之外,rx_observeWeakly还可以用在weak属性上。
在使用view.rx_observe的时候,有几点要注意

由于KVO是建立在NSObject子类的基础上的,你可以通过如下方法,来让Structs支持KVO

Notification

使用RxSwift,Notification变的十分简洁

NSNotificationCenter.defaultCenter()
.rx_notification(UITextViewTextDidBeginEditingNotification, object: myTextView)
.map { /do something with data/ }
....

学习资料

后续在还会持续更新在项目中结合Moya+HandyJSON+Rx的实战实例,欢迎指导,共同学习,共同进步,与君共勉!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容