由浅入深,从基本概念到源码解析,带你全面掌握 Swift 响应式编程
一、什么是响应式编程? 1.1 从命令式到声明式 传统的命令式编程中,我们通过直接修改状态和调用方法来驱动程序执行:
1 2 3 4 5 6 7 8 9 textField.addTarget(self , action: #selector (textDidChange), for: .editingChanged) func textDidChange () { let text = textField.text ?? "" if text.count >= 3 { searchUsers(text) } }
响应式编程则将数据流 视为核心概念,用声明式 的方式描述「当数据变化时该做什么」:
1 2 3 4 5 6 7 textField.reactive.continuousTextValues .filter { ($0 ? .count ?? 0 ) >= 3 } .debounce(0.3 , on: QueueScheduler .main) .observeValues { [weak self ] text in self ? .searchUsers(text ?? "" ) }
1.2 ReactiveCocoa 与 ReactiveSwift
ReactiveSwift :纯 Swift 实现的响应式核心库,提供 Signal、SignalProducer 等基础类型
ReactiveCocoa :基于 ReactiveSwift,为 Cocoa/Cocoa Touch 提供 UI 绑定、扩展和便捷 API
两者关系可以理解为:ReactiveSwift 是引擎,ReactiveCocoa 是上层封装。
1.3 为什么选择 RAC?
统一抽象 :将 delegate、回调、通知、KVO、Target-Action 等统一为「事件流」
可组合 :通过 map、filter、combineLatest 等操作符组合数据流
减少状态 :用数据流替代分散的中间变量
声明式 :代码更贴近「业务意图」,易于阅读和维护
二、核心概念 2.1 事件 (Event) Event 是事件流中的最小传输单元 ,类似一次性的直播流中的一帧:
1 2 3 4 5 6 public enum Event <Value , Error : Swift .Error > { case value(Value ) case failed(Error ) case completed case interrupted }
一个典型的流:若干个 .value,最后以 .completed 或 .failed 结束;.interrupted 表示订阅被取消。
2.2 观察者 (Observer) Observer 负责向事件流发送 事件:
1 2 3 4 5 6 let (signal, observer) = Signal <String , Never >.pipe()observer.send(value: "Hello" ) observer.send(value: "World" ) observer.sendCompleted()
2.3 可销毁对象 (Disposable) 订阅信号会返回 Disposable,用于取消订阅 、释放资源:
1 2 3 4 5 6 let disposable = signalProducer.start { value in print (value) } disposable.dispose()
2.4 核心类型总览
类型
描述
类比
Signal
热信号,单向事件流,所有者控制发送
直播画面
SignalProducer
冷信号,延迟执行,每次 start 创建新流
点播视频
Property
可观察的「有值」盒子,永不失败
播放进度条
Action
串行执行、可启用/禁用的操作
自动售货机
Lifetime
观察的生命周期,用于自动取消
观看时间段
三、Signal 与 SignalProducer 3.1 Signal:热信号
热信号 :无论有没有观察者,事件都会持续发送
所有者完全控制何时发送、发送什么
观察者只能订阅,不能影响流的产生
1 2 3 4 5 6 7 8 9 10 let (signal, observer) = Signal <String , Never >.pipe()signal.observeValues { value in print ("收到: \(value) " ) } observer.send(value: "A" ) observer.send(value: "B" ) observer.sendCompleted()
3.2 SignalProducer:冷信号
冷信号 :只有在 start 时才真正执行
每次 start 都会创建新的 Signal,重新跑一遍逻辑
适合网络请求、文件读取等「按需执行」的场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 let producer = SignalProducer <String , Never > { observer, _ in print ("开始执行" ) observer.send(value: "Hello" ) observer.send(value: "World" ) observer.sendCompleted() } let d1 = producer.startWithValues { print ("观察者1: \($0 ) " ) }let d2 = producer.startWithValues { print ("观察者2: \($0 ) " ) }
3.3 热信号 vs 冷信号
特性
Signal (热)
SignalProducer (冷)
执行时机
由发送者决定
start 时执行
多订阅
共享同一流
每个订阅独立执行
典型场景
按钮点击、通知
网络请求、文件读取
四、Property 与 Action 4.1 Property:可观察的盒子 Property 表示「始终有一个当前值」且「永不失败」的流,适合表示 UI 状态、配置等:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 let username = MutableProperty <String >("" )print (username.value)username.signal.observeValues { newValue in print ("用户名变为: \(newValue) " ) } username.value = "张三" username.modify { $0 = "李四" }
4.2 Action:串行操作 Action 将「输入 → 输出」封装为可复用、可启用/禁用的操作,且保证串行执行 (同一时间只处理一个请求):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 let searchAction = Action <String , [User ], NSError > { keyword in return searchAPI(keyword) } let enabledSearch = Action (state: viewModel.keyword, enabledIf: { $0 .count > 0 }) { _ , keyword in return searchAPI(keyword) } searchAction.apply("Swift" ).startWithResult { result in switch result { case .success(let users): self .users = users case .failure(let error): self .showError(error) } }
五、常用操作符 5.1 转换类 map :转换每个值
1 2 3 signal .map { $0 .uppercased() } .observeValues { print ($0 ) }
filter :过滤值
1 2 3 signal .filter { $0 .count >= 3 } .observeValues { print ($0 ) }
reduce :聚合为单个值(在 completed 时发出)
1 2 3 signal .reduce(0 ) { $0 + $1 } .observeValues { print ("总和: \($0 ) " ) }
5.2 组合类 combineLatest :合并多个流的最新值
1 2 3 4 let combined = Signal .combineLatest(usernameSignal, passwordSignal)combined.observeValues { (user, pwd) in loginButton.isEnabled = user.count > 0 && pwd.count >= 6 }
zip :按顺序一一配对
1 2 let zipped = Signal .zip(numbersSignal, lettersSignal)
5.3 扁平化 (flatten) merge :内层多个流的值按到达顺序全部输出
concat :按顺序消费内层流,前一个完成后才订阅下一个
latest :只保留「最新」的内层流,常用于搜索联想(新关键词来时取消旧请求)
1 2 3 4 5 6 7 searchKeywordSignal .flatMap(.latest) { keyword in searchAPI(keyword) } .observeValues { users in self .updateUI(users) }
5.4 错误处理 1 2 3 4 producer .flatMapError { _ in SignalProducer (value: []) } .retry(upTo: 3 ) .mapError { CustomError .wrapped($0 ) }
六、源码与实现原理 6.1 Signal 的核心结构 Signal 通过闭包保存「如何向观察者推送事件」的逻辑,内部维护观察者列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 public init (_ generator : (Observer ) -> Disposable ?)public static func pipe () -> (Signal , Signal .Observer ) { var observer: Signal .Observer ! let signal = Signal { innerObserver in observer = innerObserver return nil } return (signal, observer) }
当 observer.send(value:) 被调用时,所有已注册的观察者都会收到该值。
6.2 SignalProducer 的延迟执行 SignalProducer 保存的是「如何创建 Signal」的闭包,而不是 Signal 本身:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public func start (_ observer : Observer ) -> Disposable { let (signal, pipeObserver) = Signal .pipe() let disposable = CompositeDisposable () disposable += signal.observe(observer) disposable += self .startWithSignal { signal, innerDisposable in pipeObserver.observe(signal) disposable += innerDisposable } return disposable }
因此每次 start 都会触发一次完整的「创建 + 执行」过程。
6.3 操作符的链式调用 map、filter 等操作符本质是创建新的 Signal/SignalProducer,内部订阅上游并转换后传给下游:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 extension Signal { public func map <U >(_ transform : @escaping (Value ) -> U ) -> Signal <U , Error > { return Signal { observer in return self .observe { event in switch event { case let .value(value): observer.send(value: transform(value)) case .completed: observer.sendCompleted() case let .failed(error): observer.send(error: error) case .interrupted: observer.sendInterrupted() } } } } }
七、实战示例 7.1 登录表单校验 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class LoginViewModel { let username = MutableProperty ("" ) let password = MutableProperty ("" ) var canLogin: Property <Bool > { return Property ( initial: false , then: Signal .combineLatest(username.signal, password.signal) .map { user, pwd in user.count >= 3 && pwd.count >= 6 } ) } let loginAction: Action <(String , String ), User , Error > init () { loginAction = Action (enabledIf: canLogin) { [weak self ] _ , input in return loginAPI(username: input.0 , password: input.1 ) } } } viewModel.canLogin.signal .observeValues { [weak loginButton] canLogin in loginButton? .isEnabled = canLogin } loginButton.reactive.pressed = CocoaAction (viewModel.loginAction) { _ in (viewModel.username.value, viewModel.password.value) }
7.2 搜索联想(防抖 + 取消旧请求) 1 2 3 4 5 6 7 8 9 10 11 12 13 searchTextField.reactive.continuousTextValues .filter { ($0 ?? "" ).count >= 2 } .debounce(0.3 , on: QueueScheduler .main) .flatMap(.latest) { [weak self ] keyword -> SignalProducer <[User ], Error > in guard let keyword = keyword, ! keyword.isEmpty else { return .init (value: []) } return self ? .searchAPI(keyword) ?? .empty } .observe(on: UIScheduler ()) .startWithValues { [weak self ] users in self ? .updateSearchResults(users) }
7.3 多数据源合并展示 1 2 3 4 5 6 7 8 9 10 11 12 13 let localUsers = loadLocalUsers() let remoteUsers = fetchRemoteUsers() SignalProducer .combineLatest( localUsers.flatMapError { _ in .init (value: []) }, remoteUsers.flatMapError { _ in .init (value: []) } ) .map { local, remote in mergeAndDeduplicate(local: local, remote: remote) } .startWithValues { [weak self ] users in self ? .tableView.reload(with: users) }
八、ReactiveCocoa 的 Cocoa 扩展 ReactiveCocoa 为 UIKit 提供了 reactive 命名空间下的扩展:
1 2 3 4 5 6 7 8 9 10 11 12 13 button.reactive.controlEvents(.touchUpInside) .observeValues { _ in print ("点击" ) } textField.reactive.continuousTextValues .observeValues { print ($0 ?? "" ) } label.reactive.text <~ viewModel.title button.reactive.pressed = CocoaAction (viewModel.submitAction)
九、最佳实践与注意事项
避免循环引用 :在闭包中使用 [weak self],在适当时机 dispose 订阅
主线程更新 UI :使用 .observe(on: UIScheduler()) 确保 UI 更新在主线程
合理选择热/冷信号 :异步任务、网络请求用 SignalProducer;UI 事件、通知用 Signal
错误类型 :尽量使用 Never 表示不会失败的流,便于组合
利用 Lifetime :在 ViewController/View 销毁时通过 Lifetime 自动取消订阅
十、总结
概念
要点
Event
事件流的传输单元:value / failed / completed / interrupted
Signal
热信号,由所有者控制,多订阅共享
SignalProducer
冷信号,延迟执行,每次 start 独立运行
Property
有当前值、不失败的流,适合状态
Action
串行、可启用/禁用的操作封装
操作符
map、filter、combineLatest、flatMap 等组合与转换流
实践
表单校验、搜索联想、多源合并、UI 绑定
掌握这些概念后,你就能用响应式思维重构业务逻辑,写出更清晰、更易维护的 Swift/iOS 代码。
参考资源