利用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的做法。上面这些问题可以留给读者自己去思考了。
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。