Combine 框架学习笔记

93 阅读3分钟

Combine 框架学习笔记

一、Combine 是什么

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

核心三件套:

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不输出任何值,立即完成
let justPublisher =  Just ( "Hello, Combine!" ) 

justPublisher.sink { value in 
    print (value)   // 输出:Hello, Combine!
 }
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,依次同步发出每个元素,发完后再发一个"完成"信号。

[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 是一个属性包装器,让普通属性自动具备"广播变化"的能力。

class UserViewModel: ObservableObject {
    @Published var username: String = ""
}

加上它之后,实际上多了两样东西:

写法含义
username读取/设置当前值
$username它的 Publisher,可以拿去订阅
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

数据流的终点,负责真正"消费"这个值。

// 写法一:只关心值
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 相当于"订阅凭证"。只要留着凭证,杂志就一直寄来;凭证被释放,订阅自动取消。

// ❌ 错误:没保存返回值,订阅瞬间失效
subject.sink { print($0) }
subject.send("hello")  // 什么都不会打印

// ✅ 正确:保存返回值
let cancellable = subject.sink { print($0) }
subject.send("hello")  // 正常打印

AnyCancellable 遵循 Cancellable 协议:

protocol Cancellable {
    func cancel()
}

可以手动调用 .cancel() 提前终止订阅;它 deinit 时也会自动调用一次 cancel()

用 Set 统一管理多个订阅

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 协议提供的便捷方法,把订阅凭证丢进集合,交给集合管理生命周期。

func store(in collection: inout Set<AnyCancellable>)

inout 意味着直接修改传入的集合,所以调用时要写 &cancellables

// 这两种写法等价:
let c = publisher.sink { print($0) }
cancellables.insert(c)

// 链式写法,更常用:
publisher
    .sink { print($0) }
    .store(in: &cancellables)

也可以存进数组 [AnyCancellable],区别仅在于 Set 不允许重复、无序,Array 允许重复、有序。


五、常用操作符(Operator)

[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 语法,表示"指向某个属性的一条路径"。

// 这三种写法完全等价:
.map { output in return output.data }
.map { $0.data }
.map(\.data)

典型场景:dataTaskPublisher 发出的是元组 (data: Data, response: URLResponse),但 decode 只需要 Data,所以用 .map(\.data) 提取出需要的字段,做类型桥接。

也可以连续取多层:

.map(\.address.city)

防抖搜索实战

$searchText
    .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
    .removeDuplicates()
    .sink { text in performSearch(text) }

多属性联动校验(登录表单)

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)") }

六、错误处理

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))

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

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

// 赋值之外还要做点别的事,用 sink
publisher.sink { [weak self] newValue in
    self?.title = newValue
    print("标题更新了: \(newValue)")
    self?.logEvent("title_changed")
}