利用Swift实现一个响应式编程库
前言
整个2017年我完全使用Swift进行开发了。使用Swift进行开发是一个很愉快的体验,我已经完全不想再去碰OC了。最近想做一个响应式编程的库,所以就把它拿来分享一下。
在缺乏好的资源的情况下,学习响应式编程成为痛苦。我开始学的时候,做死地找各种教程。结果发现有用的只是极少部分,而且这少部分也只是表面上的东西,对于整个体系结构的理解也起不了多大的作用。
ReactivePrograming
说到响应式编程,ReactiveCocoa和RxSwift可以说是目前iOS开发中最优秀的第三方开源库了。今天咱们不聊ReactiveCocoa和RxSwif,咱们自己来写一个响应式编程库。如果你对观察者模式很熟悉的话,那么响应式编程就很容易理解了。
响应式编程是一种面向数据流和变化传播的编程范式。
比如用户输入、单击事件、变量值等都可以看做一个流,你可以观察这个流,并基于这个流做一些操作。“监听”流的行为叫做订阅。响应式就是基于这种想法。
废话不多说,撸起袖子开干。
我们以一个获取用户信息的网络请求为例:
funcfetchUser(withid:Int,completion:@escaping((User)->Void)){ DispatchQueue.main.asyncAfter(deadline:DispatchTime.now()+2){ letuser=User(name:"jewelz") completion(user) } }
上面是我们通常的做法,在请求方法里传入一个回调函数,在回调里拿到结果。在响应式里面,我们监听请求,当请求完成时,观察者得到更新。
funcfetchUser(withid:Int)->Signal{}
发送网络请求就可以这样:
fetchUser(with:"12345").subscribe({ })
在完成Signal之前,需要定义订阅后返回的数据结构,这里我只关心成功和失败两种状态的数据,所以可以这样写:
enumResult{ casesuccess(Value) caseerror(Error) }
现在可以开始实现我们的Signal了:
finalclassSignal{ fileprivatetypealiasSubscriber=(Result)->Void fileprivatevarsubscribers:[Subscriber]=[] funcsend(_result:Result){ forsubscriberinsubscribers{ subscriber(result) } } funcsubscribe(_subscriber:@escaping(Result)->Void){ subscribers.append(subscriber) } }
写个小例子测试一下:
letsignal=Signal() signal.subscribe{resultin print(result) } signal.send(.success(100)) signal.send(.success(200)) //Print success(100) success(200)
我们的Signal已经可以正常工作了,不过还有很多改进的空间,我们可以使用一个工厂方法来创建一个Signal,同时将send变为私有的:
staticfuncempty()->((Result)->Void,Signal){ letsignal=Signal() return(signal.send,signal) } fileprivatefuncsend(_result:Result){...}
现在我们需要这样使用Signal了:
let(sink,signal)=Signal.empty() signal.subscribe{resultin print(result) } sink(.success(100)) sink(.success(200))
接着我们可以给UITextField绑定一个Signal,只需要在Extension中给UITextField添加一个计算属性:
extensionUITextField{ varsignal:Signal{ let(sink,signal)=Signal.empty() letobserver=KeyValueObserver(object:self,keyPath:#keyPath(text)){strin sink(.success(str)) } signal.objects.append(observer) returnsignal } }
上面代码中的observer是一个局部变量,在signal调用完后,就会被销毁,所以需要在Signal中保存该对象,可以给Signal添加一个数组,用来保存需要延长生命周期的对象。KeyValueObserver是对KVO的简单封装,其实现如下:
finalclassKeyValueObserver:NSObject{ privateletobject:NSObject privateletkeyPath:String privateletcallback:(T)->Void init(object:NSObject,keyPath:String,callback:@escaping(T)->Void){ self.object=object self.keyPath=keyPath self.callback=callback super.init() object.addObserver(self,forKeyPath:keyPath,options:[.new],context:nil) } overridefuncobserveValue(forKeyPathkeyPath:String?,ofobject:Any?,change:[NSKeyValueChangeKey:Any]?,context:UnsafeMutableRawPointer?){ guardletkeyPath=keyPath,keyPath==self.keyPath,letvalue=change?[.newKey]as?Telse{return} callback(value) } deinit{ object.removeObserver(self,forKeyPath:keyPath) } }
现在就可以使用textField.signal.subscribe({})来观察UITextField内容的改变了。
在Playground写个VC测试一下:
classVC{ lettextField=UITextField() varsignal:Signal? funcviewDidLoad(){ signal=textField.signal signal?.subscribe({resultin print(result) }) textField.text="1234567" } deinit{ print("Removingvc") } } varvc:VC?=VC() vc?.viewDidLoad() vc=nil //Print success("1234567") Removingvc
ReferenceCycles
我在上面的Signal中,添加了deinit方法:
deinit{ print("RemovingSignal") }
最后发现Signal的析构方法并没有执行,也就是说上面的代码中出现了循环引用,其实仔细分析上面UITextField的拓展中signal的实现就能发现问题出在哪儿了。
letobserver=KeyValueObserver(object:self,keyPath:#keyPath(text)){strin sink(.success(str)) }
在KeyValueObserver的回调中,调用了sink()方法,而sink方法其实就是signal.send(_:)方法,这里在闭包中捕获了signal变量,于是就形成了循环引用。这里只要使用weak就能解决。修改下的代码是这样的:
staticfuncempty()->((Result)->Void,Signal){ letsignal=Signal() return({[weaksignal]valueinsignal?.send(value)},signal) }
再次运行,Signal的析构方法就能执行了。
上面就实现了一个简单的响应式编程的库了。不过这里还存在很多问题,比如我们应该在适当的时机移除观察者,现在我们的观察者被添加在subscribers数组中,这样就不知道该移除哪一个观察者,所以我们将数字替换成字典,用UUID作为key:
fileprivatetypealiasToken=UUID fileprivatevarsubscribers:[Token:Subscriber]=[:]
我们可以模仿RxSwift中的Disposable用来移除观察者,实现代码如下:
finalclassDisposable{ privateletdispose:()->Void staticfunccreate(_dispose:@escaping()->Void)->Disposable{ returnDisposable(dispose) } init(_dispose:@escaping()->Void){ self.dispose=dispose } deinit{ dispose() } }
原来的subscribe(_:)返回一个Disposable就可以了:
funcsubscribe(_subscriber:@escaping(Result)->Void)->Disposable{ lettoken=UUID() subscribers[token]=subscriber returnDisposable.create{ self.subscribers[token]=nil } }
这样我们只要在适当的时机销毁Disposable就可以移除观察者了。
作为一个响应式编程库都会有map,flatMap,filter,reduce等方法,所以我们的库也不能少,我们可以简单的实现几个。
map
map比较简单,就是将一个返回值为包装值的函数作用于一个包装(Wrapped)值的过程,这里的包装值可以理解为可以包含其他值的一种结构,例如Swift中的数组,可选类型都是包装值。它们都有重载的map,flatMap等函数。以数组为例,我们经常这样使用:
letimages=["1","2","3"].map{UIImage(named:$0)}
现在来实现我们的map函数:
funcmap(_transform:@escaping(Value)->T)->Signal{ let(sink,signal)=Signal.empty() letdispose=subscribe{(result)in sink(result.map(transform)) } signal.objects.append(dispose) returnsignal }
我同时给Result也实现了map函数:
extensionResult{ funcmap(_transform:@escaping(Value)->T)->Result{ switchself{ case.success(letvalue): return.success(transform(value)) case.error(leterror): return.error(error) } } } //Test let(sink,intSignal)=Signal.empty() intSignal .map{String($0)} .subscribe{resultin print(result) } sink(.success(100)) //Printsuccess("100")
flatMap
flatMap和map很相似,但也有一些不同,以可选型为例,Swift是这样定义map和flatMap的:
publicfuncmap(_transform:(Wrapped)throws->U)rethrows->U? publicfuncflatMap(_transform:(Wrapped)throws->U?)rethrows->U?
flatMap和map的不同主要体现在transform函数的返回值不同。map接受的函数返回值类型是U类型,而flatMap接受的函数返回值类型是U?类型。例如对于一个可选值,可以这样调用:
letaString:String?="¥99.9" letprice=aString.flatMap{Float($0)} //Priceisnil
我们这里flatMap和Swift中数组以及可选型中的flatMap保持了一致。
所以我们的flatMap应该是这样定义:flatMap(_transform:@escaping(Value)->Signal)->Signal。
理解了flatMap和map的不同,实现起来也就很简单了:
funcflatMap(_transform:@escaping(Value)->Signal)->Signal{ let(sink,signal)=Signal.empty() var_dispose:Disposable? letdispose=subscribe{(result)in switchresult{ case.success(letvalue): letnew=transform(value) _dispose=new.subscribe({_resultin sink(_result) }) case.error(leterror): sink(.error(error)) } } if_dispose!=nil{ signal.objects.append(_dispose!) } signal.objects.append(dispose) returnsignal }
现在我们可以模拟一个网络请求来测试flatMap:
funcusers()->Signal{ let(sink,signal)=Signal.empty() DispatchQueue.main.asyncAfter(deadline:DispatchTime.now()+2){ letusers=Array(1...10).map{User(id:String(describing:$0))} sink(.success(users)) } returnsignal } funcuserDetail(withid:String)->Signal{ let(sink,signal)=Signal.empty() DispatchQueue.main.asyncAfter(deadline:DispatchTime.now()+2){ sink(.success(User(id:id,name:"jewelz"))) } returnsignal } letdispose=users() .flatMap{returnself.userDetail(with:$0.first!.id)} .subscribe{resultin print(result) } disposes.append(dispose) //Print:success(ReactivePrograming.User(name:Optional("jewelz"),id:"1"))
通过使用flatMap,我们可以很简单的将一个Signal转换为另一个Signal,这在我们处理多个请求嵌套时就会很方便了。
写在最后
上面通过100多行的代码就实现了一个简单的响应式编程库。不过对于一个库来说,以上的内容还远远不够。现在的Signal还不具有原子性,要作为一个实际可用的库,应该是线程安的。还有我们对Disposable的处理也不够优雅,可以模仿RxSwift中DisposeBag的做法。上面这些问题可以留给读者自己去思考了。
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。