Combine 框架学习笔记
一、Combine 是什么
Combine 是苹果推出的响应式编程框架,用统一的方式处理"会随时间变化"的异步数据------网络请求、用户输入、通知、定时器等。
核心三件套:
markdown
Publisher(发布者)──发出数据──> Subscriber(订阅者)
↓
Operator(操作符,中间加工)
- Publisher :负责产生数据流,有两个关联类型
Output(发什么值)和Failure(可能报什么错,Never表示不会失败) - Subscriber :负责接收并处理数据,最常用
sink和assign - Subscription:连接两者的"管道",可以取消
二、常用 Publisher 类型
| 类型 | 用途 |
|---|---|
Just |
发出一个值然后结束 |
Future |
异步产生一个值(类似 Promise) |
PassthroughSubject |
手动触发事件,不保存当前值 |
CurrentValueSubject |
手动触发事件,保存并能读取当前值 |
@Published |
给属性自动生成 Publisher,SwiftUI 中常用 |
[1,2,3].publisher |
把数组转成 Publisher,依次发出每个元素 |
Empty |
不输出任何值,立即完成 |
swift
let justPublisher = Just ( "Hello, Combine!" )
justPublisher.sink { value in
print (value) // 输出:Hello, Combine!
}
swift
let emptyPublisher = Empty < String , Never >()
emptyPublisher.sink(
receiveCompletion: { completion in
print ( "Completed" )
},
receiveValue: { value in
print (value)
}
) // 输出:Completed
[1, 2, 3, 4, 5].publisher
把一个普通数组转换成 Publisher,依次同步发出每个元素,发完后再发一个"完成"信号。
swift
[1, 2, 3, 4, 5].publisher
.sink { value in print(value) }
// 依次输出: 1 2 3 4 5,然后 finished
类型是 Publishers.Sequence<[Int], Never>------Failure = Never 表示这种 Publisher 永远不会失败(遍历现成数组没有出错的可能),所以 sink 可以只写一个闭包,不用处理错误分支。
常用于快速测试操作符链 ,不用真的发网络请求就能验证 map/filter 逻辑对不对。
三、@Published
@Published 是一个属性包装器,让普通属性自动具备"广播变化"的能力。
swift
class UserViewModel: ObservableObject {
@Published var username: String = ""
}
加上它之后,实际上多了两样东西:
| 写法 | 含义 |
|---|---|
username |
读取/设置当前值 |
$username |
它的 Publisher,可以拿去订阅 |
swift
let vm = UserViewModel()
let sub = vm.$username.sink { print("变成了: \($0)") }
vm.username = "Alice" // 自动触发: 变成了: Alice
关键细节 :@Published 底层是 CurrentValueSubject,一订阅就立刻收到一次当前值,不用等下次变化。
使用限制:
- 只能用在
class里,不能用在struct - 必须是
var,不能是let - 只能感知"整个属性被重新赋值",无法感知"复杂对象内部属性的变化"
和 SwiftUI 的关系 :ObservableObject 协议自带一个 objectWillChange Publisher。只要类里有一个属性标了 @Published,它变化前就会自动触发 objectWillChange.send(),SwiftUI 监听到后自动重新渲染界面------不需要手写 objectWillChange.send()。
四、sink 和 Cancellable
sink
数据流的终点,负责真正"消费"这个值。
swift
// 写法一:只关心值
publisher.sink { value in print(value) }
// 写法二:同时关心完成状态和值(用于可能失败的 Publisher)
publisher.sink(
receiveCompletion: { completion in
switch completion {
case .finished: print("正常结束")
case .failure(let err): print("出错: \(err)")
}
},
receiveValue: { value in print("值: \(value)") }
)
AnyCancellable:为什么必须"留住"它
类比订阅杂志:调用 .sink 相当于"订阅",返回的 AnyCancellable 相当于"订阅凭证"。只要留着凭证,杂志就一直寄来;凭证被释放,订阅自动取消。
swift
// ❌ 错误:没保存返回值,订阅瞬间失效
subject.sink { print($0) }
subject.send("hello") // 什么都不会打印
// ✅ 正确:保存返回值
let cancellable = subject.sink { print($0) }
subject.send("hello") // 正常打印
AnyCancellable 遵循 Cancellable 协议:
swift
protocol Cancellable {
func cancel()
}
可以手动调用 .cancel() 提前终止订阅;它 deinit 时也会自动调用一次 cancel()。
用 Set 统一管理多个订阅
swift
class SearchViewModel: ObservableObject {
@Published var searchText: String = ""
var cancellables = Set<AnyCancellable>() // 收纳订阅凭证的"文件夹"
init() {
$searchText
.sink { text in print(text) }
.store(in: &cancellables) // 把凭证放进文件夹
}
}
AnyCancellable(单数)= 一张订阅凭证Set<AnyCancellable>(复数)= 装这些凭证的文件夹,本身不是订阅关系,而是订阅关系的容器- ViewModel 销毁时,集合被销毁,里面所有订阅自动统一取消,不用逐个手动 cancel
.store(in:)
Cancellable 协议提供的便捷方法,把订阅凭证丢进集合,交给集合管理生命周期。
swift
func store(in collection: inout Set<AnyCancellable>)
inout 意味着直接修改传入的集合,所以调用时要写 &cancellables。
swift
// 这两种写法等价:
let c = publisher.sink { print($0) }
cancellables.insert(c)
// 链式写法,更常用:
publisher
.sink { print($0) }
.store(in: &cancellables)
也可以存进数组 [AnyCancellable],区别仅在于 Set 不允许重复、无序,Array 允许重复、有序。
五、常用操作符(Operator)
swift
[1, 2, 3, 4, 5].publisher
.map { $0 * 2 } // 转换: 2,4,6,8,10
.filter { $0 > 4 } // 过滤: 6,8,10
.sink { print($0) }
| 操作符 | 作用 |
|---|---|
map |
转换值 |
filter |
过滤值 |
debounce |
防抖(如搜索框延迟触发) |
combineLatest |
合并多个 Publisher 的最新值 |
merge |
合并多个同类型 Publisher |
removeDuplicates |
去重 |
flatMap |
把值映射成新 Publisher 并展开(常用于串联请求) |
assign(to:on:) |
自动把新值赋给某对象的某个属性 |
.map(.属性名) ------ KeyPath 简写
\.data 是 KeyPath 语法,表示"指向某个属性的一条路径"。
swift
// 这三种写法完全等价:
.map { output in return output.data }
.map { $0.data }
.map(\.data)
典型场景:dataTaskPublisher 发出的是元组 (data: Data, response: URLResponse),但 decode 只需要 Data,所以用 .map(\.data) 提取出需要的字段,做类型桥接。
也可以连续取多层:
swift
.map(\.address.city)
防抖搜索实战
swift
$searchText
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.sink { text in performSearch(text) }
多属性联动校验(登录表单)
swift
class LoginForm: ObservableObject {
@Published var username: String = ""
@Published var password: String = ""
}
form.$username
.combineLatest(form.$password)
.map { username, password in
!username.isEmpty && password.count >= 6
}
.sink { isValid in print("能否提交: \(isValid)") }
六、错误处理
swift
enum MyError: Error { case somethingWrong }
let publisher = Future<Int, MyError> { promise in
promise(.failure(.somethingWrong))
}
publisher
.catch { error -> Just<Int> in
print("出错了: \(error)")
return Just(0) // 提供默认值
}
.sink { print("最终值: \($0)") }
七、网络请求实战(含 .map(.data))
swift
struct User: Decodable {
let id: Int
let name: String
}
func fetchUser(id: Int) -> AnyPublisher<User, Error> {
let url = URL(string: "https://api.example.com/users/\(id)")!
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data) // 元组 -> Data
.decode(type: User.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
let cancellable = fetchUser(id: 1)
.sink(
receiveCompletion: { print($0) },
receiveValue: { user in print("用户: \(user.name)") }
)
assign 是 sink 的"近亲"------它也是订阅数据流的一种方式,但用途更专一:只做一件事,就是把新值直接赋给某个对象的某个属性,不需要你手写赋值逻辑。
swift
// 只是赋值,用 assign 更简洁
publisher.assign(to: &$title)
// 赋值之外还要做点别的事,用 sink
publisher.sink { [weak self] newValue in
self?.title = newValue
print("标题更新了: \(newValue)")
self?.logEvent("title_changed")
}