iOS 项目从 ReactiveSwift 迁移到 openCombine

为什么是 Combine?

1. 官方支持

苹果于 2019 年 6 月对外发布了 Combine 框架,至今已经过去快两年时间,做为 SwiftUI 的御用数据流管理框架,基本不太可能在未来被抛弃。网上对它的实践经验也有不少,所以使用时机已基本成熟

2. UI 框架的结合

无论是 Reactive 还是 Rx,它们的设计出发点都是针对 UIKit 的,而 Combine 是为了 SwiftUI 而生,在声明式 UI 开发时更有先天优势

3. 性能优势

虽然 Reactive 和 Rx 都被极尽优化了,但 Combine 的官方背景,使它在性能方面完全碾轧所有第三方竞品,可以参考这篇文章

4. 自身发展

移动互联网发展迅速,6 年前 Swift 才刚发布,6 年后 OC 已然被苹果边缘化,OC基本没有很大的更新, YouTube 上几乎看不到 OC 的新教程。很难想象几年后 UIKit 是否会被 SwiftUI 革命,尽早适应没有坏处

关于 OpenCombine 框架

因为苹果的 Combine 框架需要最低 iOS 13 系统,所以我们可以使用 OpenCombine 替代

OpenCombine 是一个开源的 Combine 接口兼容框架,旨在提供一个旧版本系统和跨平台的 Combine API 解决方案

它包含了三个公开的子框架:

  1. OpenCombine: 核心框架,对应于苹果官方的 Combine 框架
  2. OpenCombineFoundation: 将 Foundation 框架中的一些事件,封装为 Combine 的 Publisher,比如 NotificationCenter, URLSession 等
  3. OpenCombineDispatch: 将 Dispatch 框架中的一些事件,封装为 Combine 的 Scheduler

虽然 OpenCombine 的目标是复刻 Combine 的所有 API,但目前还在进行中

所有未实现的方法,都在项目根目录的 RemainingCombineInterface.swiftRemainingFoundationInterface.swift 文件中进行了列举

迁移改动点

1. 手势绑定

手势绑定现在是对 UIView 扩展出新方法

extension UIView {    
  /// 绑定手势
  public func onGesture<T>(_ gesture: Gesture<T>, _ callback: @escaping (T) -> Void) -> T where T : UIGestureRecognizer    
/// 取消绑定手势
    public func cancelGesture<T>(_ gesture: Gesture<T>) where T : UIGestureRecognizer
}

其中手势类型封装为一个枚举类型,调用的返回值是手势对象

// ReactiveCocoa
view.addGestureRecognizer(UITapGestureRecognizer().then {
    $0.reactive.stateChanged.observeValues { _ in
        print("You tapped")
    }
})
 
// OpenCombine & Combine
view.onGesture(.tap) { _ in
    print("You tapped")
}

2. UIControl 事件响应

UIControl 封装了统一的事件响应方法

extension ControlEventSubscribable where Self : UIControl {
            /// 监听 events 事件时的回调
            public func onControlEvents(for events: UIControl.Event, _ action: @escaping (_ sender: Self) -> Void) -> AnyCancellable
        }

方法的返回值是可取消对象

                // ReactiveCocoa
        button.reactive.controlEvents(.touchUpInside).observeValues { _ in
            print("You touched up inside")
        }
        // OpenCombine & Combine
        button.onControlEvent(for: .touchUpInside) { _ in
            print("You touched up inside")
        }

3. 成员变量的订阅

这部分改动较 ReactiveSwift 区别较大,也简洁了很多,直接上代码对比一下吧

class Person {
            /// 名字
            var name = MutableProperty<String?>(nil)
            /// 更新名字
            func updateName() {
                name.value = "Bruce"
                // 需要对 name 的 value 赋值
            }
        }
        // 订阅者进行订阅操作(需要取 name 的 signal 进行订阅)
        person.name.signal.observeValues { name in
            print("name: \\(name)")
        }
class Person {
    /// 名字
    @OpenCombine.Published  // 若要迁移至 Combine,删除 OpenCombine. 即可
    var name: String?
 
    /// 更新名字
    func updateName() {
        name = "Bruce"  // 直接对 name 赋值
    }
}
 
// 订阅者进行订阅操作(直接在 name 前加上 $ 符号即可订阅)
person.$name.sink { name in
    print("name: \\(name)")
}.store(to: self)

通过对比可以发现:

ReactiveSwift 需要显示的定义一个 MutableProperty 对值进行包装,更新值的时候需要访问 value,订阅的时候访问 signal,侵入性较大

OpenCombine 只需要声明一个名为 @OpenCombine.Published 的属性包装器,内部使用无其他变化,外部订阅时只需要在属性名前加上 $ 即可

4. Foundation

Foundation 的封装是在 OpenCombineFoundation 中实现的,可以把一些事件回调封装到 Publisher 中,以我们工程中用到的一处为例

// ReactiveCocoa
NotificationCenter.default.reactive.notifications(forName: .CTRadioAccessTechnologyDidChange)
 
// OpenCombine
NotificationCenter.default.ocombine.publisher(for: .CTRadioAccessTechnologyDidChange)
 
// Combine
NotificationCenter.default.publisher(for: .CTRadioAccessTechnologyDidChange)

关键点

三大核心角色

  • Publisher 数据的提供者,它提供了最原始的数据,不管这个数据是从什么地方获取的。如果把 pipline 想象成一条包子生产线,那么 Publisher 就表示食材
  • Subscriber 数据的接收者,它要求接收的数据必须是处理好的,同样把 pipline 想象成一条包子生产线,则 Subscriber 就是成品包子,而不是中间产物(菜馅等)
  • Operator 中间处理过程,它上下联通 Publisher 和 Subscriber,对 Publisher 输出地数据进行处理,然后返回成品数据给 Subscriber

关于订阅

无论你的 Publisher 从何而来,需要获取其中的数据时,都使用 sink 方法,而 ReactiveSwift 这点并未统一,有些地方使用 observeValues,有些是 observe

sink 方法会返回一个 Cancellable 对象,可以用来取消订阅,如果该对象被释放,订阅自动被取消,所以对于引用对象,我们可以将它存到 OC 关联对象中,ARC 会自动将它绑定到自身到生命周期中

我在 DJFoundationSwift 中,对 AnyCancellable 进行了扩展,增加了一个 store(to:) 方法,可以很方便的将 Cancellable 对象保存到一个 ARC 管理的 Set 中

extension AnyCancellable {
    /// 存储到自动管理的 Cancellable 池中
    public func store(to object: NSObject) {
        store(in: &object.autoCancellation.cancellables)
    }
}

Subject

在 ReactiveObjC 中有一个 RACSubject,它既可以订阅信号也可以发送信号

到了 ReactiveSwift 这个概念被去除掉了,取而代之的是将输入&输出两个信号做一个 pipe 操作

而 Combine 中也有一个 Subject,含义与 ReactiveObjC 类似,既可以订阅信号又可以发送信号,不过它是一个 protocol

Subject 有两个实现类:

  • PassthroughSubject:类似一个门铃,你可以向它发送信号,但它没有状态,只是传递了信号
  • CurrentValueSubject:更像一个开关,传递信号的同时,它会记录当前的一个状态值

理解了上述区别后,什么场景选用哪个应该也不难了

弹珠图

在响应式开发框架中,都会提供大量的 Operator 对信号进行转换,但遇到一些不常用的 Operator 时,看函数声明或注释总归不直观,这时候我们就可以参考弹珠图

比如 zip 操作的弹珠图:

截屏2021-06-15 下午3.04.15

上面两行数输入,下面就是 zip 后的输出示意图,看起来非常直观

更多 Operator 的弹珠图可以在这里找到,你可以在操作的上面对弹珠进行拖动,观察输出的变化

eraseToAnyPublisher()

调用这个方法可以对类型进行擦除

为什么要类型擦除?

因为我们有时候需要对 API 的可访问边界进行控制,低层方法对数据进行一系列转换后,高层不需要关心数据是怎么转换的

参考

[1] Combine: https://developer.apple.com/documentation/combine

[2] Using Combine: https://heckj.github.io/swiftui-notes/

[3] OpenCombine: https://github.com/OpenCombine/OpenCombine

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

推荐阅读更多精彩内容