利用Swift实现一个响应式编程库

 更新时间:2020年6月30日 23:37  点击:1702

前言

整个2017年我完全使用 Swift 进行开发了。使用 Swift 进行开发是一个很愉快的体验,我已经完全不想再去碰 OC 了。最近想做一个响应式编程的库,所以就把它拿来分享一下。

在缺乏好的资源的情况下,学习响应式编程成为痛苦。我开始学的时候,做死地找各种教程。结果发现有用的只是极少部分,而且这少部分也只是表面上的东西,对于整个体系结构的理解也起不了多大的作用。

Reactive Programing

说到响应式编程,ReactiveCocoa 和 RxSwift 可以说是目前 iOS 开发中最优秀的第三方开源库了。今天咱们不聊 ReactiveCocoa 和 RxSwif,咱们自己来写一个响应式编程库。如果你对观察者模式很熟悉的话,那么响应式编程就很容易理解了。

响应式编程是一种面向数据流和变化传播的编程范式。

比如用户输入、单击事件、变量值等都可以看做一个流,你可以观察这个流,并基于这个流做一些操作。“监听”流的行为叫做订阅。响应式就是基于这种想法。

废话不多说,撸起袖子开干。

我们以一个获取用户信息的网络请求为例:

func fetchUser(with id: Int, completion: @escaping ((User) -> Void)) {
 DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+2) {
  let user = User(name: "jewelz")
  completion(user)
 }
}

上面是我们通常的做法,在请求方法里传入一个回调函数,在回调里拿到结果。在响应式里面,我们监听请求,当请求完成时,观察者得到更新。

func fetchUser(with id: Int) -> Signal {}

发送网络请求就可以这样:

fetchUser(with: "12345").subscribe({ 
})

在完成 Signal 之前, 需要定义订阅后返回的数据结构,这里我只关心成功和失败两种状态的数据,所以可以这样写:

enum Result {
 case success(Value)
 case error(Error)
}

现在可以开始实现我们的 Signal 了:

final class Signal {
 fileprivate typealias Subscriber = (Result) -> Void
 fileprivate var subscribers: [Subscriber] = [] 
 func send(_ result: Result) {
 for subscriber in subscribers {
  subscriber(result)
 }
 } 
 func subscribe(_ subscriber: @escaping (Result) -> Void) {
 subscribers.append(subscriber)
 }
}

写个小例子测试一下:

let signal = Signal()
signal.subscribe { result in
 print(result)
}
signal.send(.success(100))
signal.send(.success(200))
// Print
success(100)
success(200)

我们的 Signal 已经可以正常工作了,不过还有很多改进的空间,我们可以使用一个工厂方法来创建一个 Signal, 同时将 send变为私有的:

static func empty() -> ((Result) -> Void, Signal) {
 let signal = Signal()
 return (signal.send, signal)
}
fileprivate func send(_ result: Result) { ... }

现在我们需要这样使用 Signal 了:

let (sink, signal) = Signal.empty()
signal.subscribe { result in
 print(result)
}
sink(.success(100))
sink(.success(200))

接着我们可以给 UITextField 绑定一个 Signal,只需要在 Extension 中给 UITextField添加一个计算属性 :

extension UITextField {
 var signal: Signal {
 let (sink, signal) = Signal.empty()
 let observer = KeyValueObserver(object: self, keyPath: #keyPath(text)) { str in
  sink(.success(str))
 }
 signal.objects.append(observer)
 return signal
 }
}

上面代码中的 observer 是一个局部变量,在 signal调用完后,就会被销毁,所以需要在 Signal 中保存该对象,可以给 Signal 添加一个数组,用来保存需要延长生命周期的对象。 KeyValueObserver 是对 KVO 的简单封装,其实现如下:

final class KeyValueObserver: NSObject {
 
 private let object: NSObject
 private let keyPath: String
 private let callback: (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)
 } 
 override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
 guard let keyPath = keyPath, keyPath == self.keyPath, let value = change?[.newKey] as? T else { return } 
 callback(value)
 } 
 deinit {
 object.removeObserver(self, forKeyPath: keyPath)
 }
}

现在就可以使用textField.signal.subscribe({}) 来观察 UITextField 内容的改变了。

在 Playground 写个 VC 测试一下:

class VC {
 let textField = UITextField()
 var signal: Signal? 
 func viewDidLoad() {
 signal = textField.signal
 signal?.subscribe({ result in
  print(result)
 })
 textField.text = "1234567"
 } 
 deinit {
 print("Removing vc")
 }
}
var vc: VC? = VC()
vc?.viewDidLoad()
vc = nil
// Print
success("1234567")
Removing vc

Reference Cycles

我在上面的 Signal 中,添加了 deinit方法:

deinit {
 print("Removing Signal")
}

最后发现 Signal 的析构方法并没有执行,也就是说上面的代码中出现了循环引用,其实仔细分析上面 UITextField 的拓展中 signal的实现就能发现问题出在哪儿了。

let observer = KeyValueObserver(object: self, keyPath: #keyPath(text)) { str in
 sink(.success(str))
}

在 KeyValueObserver 的回调中,调用了 sink()方法,而 sink 方法其实就是 signal.send(_:)方法,这里在闭包中捕获了signal 变量,于是就形成了循环引用。这里只要使用 weak 就能解决。修改下的代码是这样的:

static func empty() -> ((Result) -> Void, Signal) {
 let signal = Signal()
 return ({[weak signal] value in signal?.send(value)}, signal)
}

再次运行, Signal 的析构方法就能执行了。

上面就实现了一个简单的响应式编程的库了。不过这里还存在很多问题,比如我们应该在适当的时机移除观察者,现在我们的观察者被添加在 subscribers 数组中,这样就不知道该移除哪一个观察者,所以我们将数字替换成字典,用 UUID 作为 key :

fileprivate typealias Token = UUID
fileprivate var subscribers: [Token: Subscriber] = [:]

我们可以模仿 RxSwift 中的 Disposable 用来移除观察者,实现代码如下:

final class Disposable {
 private let dispose: () -> Void
 
 static func create(_ dispose: @escaping () -> Void) -> Disposable {
 return Disposable(dispose)
 } 
 init(_ dispose: @escaping () -> Void) {
 self.dispose = dispose
 } 
 deinit {
 dispose()
 }
}

原来的 subscribe(_:) 返回一个 Disposable 就可以了:

func subscribe(_ subscriber: @escaping (Result) -> Void) -> Disposable {
 let token = UUID()
 subscribers[token] = subscriber
 return Disposable.create {
  self.subscribers[token] = nil
 } 
 }

这样我们只要在适当的时机销毁 Disposable 就可以移除观察者了。

作为一个响应式编程库都会有 map, flatMap, filter, reduce 等方法,所以我们的库也不能少,我们可以简单的实现几个。

map

map 比较简单,就是将一个 返回值为包装值的函数 作用于一个包装(Wrapped)值的过程, 这里的包装值可以理解为可以包含其他值的一种结构,例如 Swift 中的数组,可选类型都是包装值。它们都有重载的 map, flatMap等函数。以数组为例,我们经常这样使用:

let images = ["1", "2", "3"].map{ UIImage(named: $0) }

现在来实现我们的 map 函数:

func map(_ transform: @escaping (Value) -> T) -> Signal {
 let (sink, signal) = Signal.empty()
 let dispose = subscribe { (result) in
  sink(result.map(transform))
 }
 signal.objects.append(dispose)
 return signal
}

我同时给 Result 也实现了 map 函数:

extension Result {
 func map(_ transform: @escaping (Value) -> T) -> Result {
 switch self {
 case .success(let value):
  return .success(transform(value))
 case .error(let error):
  return .error(error)
 }
 }
}
// Test
let (sink, intSignal) = Signal.empty()
intSignal
 .map{ String($0)}
 .subscribe { result in
 print(result)
}
sink(.success(100))
// Print success("100")

flatMap

flatMap 和 map 很相似,但也有一些不同,以可选型为例,Swif t是这样定义 map 和 flatMap 的:

public func map(_ transform: (Wrapped) throws -> U) rethrows -> U?
public func flatMap(_ transform: (Wrapped) throws -> U?) rethrows -> U?

flatMap 和 map 的不同主要体现在 transform 函数的返回值不同。map 接受的函数返回值类型是 U类型,而 flatMap 接受的函数返回值类型是 U?类型。例如对于一个可选值,可以这样调用:

let aString: String? = "¥99.9"
let price = aString.flatMap{ Float($0)}
// Price is nil

我们这里 flatMap 和 Swift 中数组以及可选型中的 flatMap 保持了一致。

所以我们的 flatMap 应该是这样定义:flatMap(_ transform: @escaping (Value) -> Signal) -> Signal。

理解了 flatMap 和 map 的不同,实现起来也就很简单了:

func flatMap(_ transform: @escaping (Value) -> Signal) -> Signal {
 let (sink, signal) = Signal.empty()
 var _dispose: Disposable?
 let dispose = subscribe { (result) in
  switch result {
  case .success(let value):
  let new = transform(value)
  _dispose = new.subscribe({ _result in
   sink(_result)
  })
  case .error(let error):
  sink(.error(error))
  }
 }
 if _dispose != nil {
 signal.objects.append(_dispose!)
 }
 signal.objects.append(dispose)
 return signal
}

现在我们可以模拟一个网络请求来测试 flatMap:

func users() -> Signal {
 let (sink, signal) = Signal.empty()
 DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+2) {
  let users = Array(1...10).map{ User(id: String(describing: $0)) }
  sink(.success(users))
 }
 return signal
 } 
func userDetail(with id: String) -> Signal {
 let (sink, signal) = Signal.empty()
 DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+2) {
 sink(.success(User(id: id, name: "jewelz")))
 }
 return signal
}
let dispose = users()
 .flatMap { return self.userDetail(with: $0.first!.id) }
 .subscribe { result in
 print(result)
}
disposes.append(dispose)
// Print: success(ReactivePrograming.User(name: Optional("jewelz"), id: "1"))

通过使用 flatMap ,我们可以很简单的将一个 Signal 转换为另一个 Signal , 这在我们处理多个请求嵌套时就会很方便了。

写在最后

上面通过100 多行的代码就实现了一个简单的响应式编程库。不过对于一个库来说,以上的内容还远远不够。现在的 Signal 还不具有原子性,要作为一个实际可用的库,应该是线程安的。还有我们对 Disposable 的处理也不够优雅,可以模仿 RxSwift 中 DisposeBag 的做法。上面这些问题可以留给读者自己去思考了。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对猪先飞的支持。

[!--infotagslink--]

相关文章

  • swift中利用runtime交换方法的实现示例

    这篇文章主要给大家介绍了关于swift中利用runtime交换方法的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。...2020-06-30
  • Swift设置UILabel内边距的实例代码

    有时候,我们需要一个显示文字,又想这些文字与边界之间有自定义的边距,所以下面这篇文章主要给大家介绍了关于Swift设置UILabel内边距的相关资料,需要的朋友可以参考下...2021-10-14
  • swift中的@UIApplicationMain示例详解

    这篇文章主要给大家介绍了关于swift中@UIApplicationMain的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。...2020-06-30
  • 详解swift中xcworkspace多项目管理

    给大家详细讲解了IOS开发中swift语言xcworkspace多项目管理的方法和介绍,一起参考一下。...2020-06-30
  • Swift 中如何使用 Option Pattern 改善可选项的 API 设计

    这篇文章主要介绍了Swift 中如何使用 Option Pattern 改善可选项的 API 设计,帮助大家更好的进行ios开发,感兴趣的朋友可以了解下...2020-10-23
  • 如何使用Swift来实现一个命令行工具的方法

    这篇文章主要介绍了如何使用Swift来实现一个命令行工具,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-06-30
  • Swift实现多个TableView侧滑与切换效果

    这篇文章主要为大家详细介绍了Swift实现多个TableView侧滑与切换效果,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-06-30
  • Swift中的命名空间详解

    这篇文章主要给大家介绍了关于Swift中命名空间的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-06-30
  • 浅析Swift中struct与class的区别(汇编角度底层分析)

    这篇文章主要介绍了Swift中struct与class的区别 ,本文从汇编角度分析struct与class的区别,通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2020-06-30
  • Swift缩放并填充图片功能的实现

    最近有一个需求,就是将图片先等比例缩放到指定大小,然后将空余出来空间填充为黑色,返回指定大小的图片。本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧...2021-11-02
  • Swift中优雅处理闭包导致的循环引用详解

    这篇文章主要给大家介绍了关于Swift中优雅的处理闭包导致的循环引用的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Swift具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧...2020-06-30
  • Swift实现倒计时5秒功能

    这篇文章主要为大家详细介绍了Swift实现倒计时5秒功能,在“登录”和“注册”页面也有相似功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-06-30
  • Swift 使用 Observe 监测页面滚动的实现方法

    这篇文章主要介绍了Swift 使用 Observe 监测页面滚动的实现方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2020-06-30
  • Swift在控件中添加点击手势的方法

    这篇文章主要介绍了Swift在控件中添加点击手势的方法,本文讲解如何在tableview的headerview中添加点击手势的方法,需要的朋友可以参考下...2020-06-30
  • Swift如何使用类型擦除及自定义详解

    有很多地方会用到类型擦除,并且它们的作用的各不相同。下面这篇文章主要给大家介绍了关于Swift如何使用类型擦除及自定义的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下...2020-06-30
  • Swift利用CoreData如何存储多种数据类的通讯录

    这篇文章主要给大家介绍了关于Swift利用CoreData如何存储多种数据类的通讯录的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧。...2020-06-30
  • SwiftUI 中创建反弹动画的实现

    这篇文章主要介绍了SwiftUI 中创建反弹动画的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-10-30
  • Swift中动态调用实例方法介绍

    这篇文章主要介绍了Swift中动态调用实例方法介绍,在Swift中有一类很有意思的写法,可以让我们不直接使用实例来调用这个实例上的方法,而是通过类型取出这个类型的某个实例方法的签名,然后再通过传递实例来拿到实际需要调用的方法,需要的朋友可以参考下...2020-06-30
  • iOS Swift UICollectionView横向分页滚动,cell左右排版问题详解

    UICollectionView是iOS中比较常见的一个控件,这篇文章主要给大家介绍了关于iOS Swift UICollectionView横向分页滚动,cell左右排版问题的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考借鉴,下面随小编来一起学习学习吧。...2020-06-30
  • Swift中如何避免循环引用的方法

    本篇文章主要介绍了Swift中如何避免循环引用的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-30