Combine 框架学习笔记

Combine 框架学习笔记

一、Combine 是什么

Combine 是苹果推出的响应式编程框架,用统一的方式处理"会随时间变化"的异步数据------网络请求、用户输入、通知、定时器等。

核心三件套:

markdown 复制代码
Publisher(发布者)──发出数据──> Subscriber(订阅者)
                ↓
            Operator(操作符,中间加工)
  • Publisher :负责产生数据流,有两个关联类型 Output(发什么值)和 Failure(可能报什么错,Never 表示不会失败)
  • Subscriber :负责接收并处理数据,最常用 sinkassign
  • 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)") }
    )

assignsink 的"近亲"------它也是订阅数据流的一种方式,但用途更专一:只做一件事,就是把新值直接赋给某个对象的某个属性,不需要你手写赋值逻辑。

swift 复制代码
// 只是赋值,用 assign 更简洁
publisher.assign(to: &$title)

// 赋值之外还要做点别的事,用 sink
publisher.sink { [weak self] newValue in
    self?.title = newValue
    print("标题更新了: \(newValue)")
    self?.logEvent("title_changed")
}
相关推荐
runnerdancer1 小时前
Agent如何加载执行Skill的脚本
前端·agent
yingyima2 小时前
VS Code 正则替换技巧:从凌晨3点的服务器报警开始
前端
默_笙2 小时前
🛬 我让 AI 帮我写了一个打飞机游戏,结果 Canvas 把我整不会了
前端·javascript
梯度不陡2 小时前
AI 到底能不能从零写软件?ProgramBench 和 RepoZero 给出了两种答案
前端·javascript·面试
冬奇Lab2 小时前
每日一个开源项目(第137篇):Penpot - 真正开源的设计协作工具,SVG 原生格式消灭设计-开发鸿沟
前端·开源·设计
nuIl2 小时前
实现一个 Coding Agent(7):Skills
前端·agent·cursor
nuIl3 小时前
实现一个 Coding Agent(8):会话持久化与多会话
前端·agent·cursor
jt君424264 小时前
React Native JSI 深入剖析 — 第 5 部分中文技术整理:用 HostObject 把 C++ 类暴露给 JavaScript
前端·react native
胡萝卜术4 小时前
滑动窗口最大值:从暴力到单调队列,层层优化全解析
前端·javascript·面试