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")
}
相关推荐
weixin_382395238 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__8 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.9 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~10 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华10 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子13 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅13 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni13 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学14 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端