Part 1

Swift 现代并发编程

彻底告别 GCD 回调地狱,拥抱 async/await、Actor、结构化并发。本部分适合拥有 10 年 GCD 经验的 iOS 工程师,每个知识点都提供与 GCD 的对比视角,帮助你建立新的心智模型。

Swift 6 iOS 16+ SE-0296~0423 严格并发

① async/await 基础 Swift 5.5

心智模型:从回调到直线型代码

用了十年 GCD,你对 DispatchQueue.async { ... } 加回调的模式已经非常熟悉。然而一旦需要多个异步操作串行,就会陷入回调地狱(Callback Hell)——缩进层层嵌套,错误处理散落各处,线程安全靠肉眼保证。

async/await 的本质是:编译器把"挂起点"之后的代码自动打包成续体(Continuation),让你可以用同步的写法表达异步逻辑,线程切换由运行时负责。

GCD 写法 vs async/await 写法对比

swift — GCD 回调地狱(旧写法)
// ❌ GCD 回调地狱:三层嵌套,错误处理零散
func loadUserProfile(id: String, completion: @escaping (Result<UserProfile, Error>) -> Void) {
    // 第一步:网络请求 token
    fetchToken { tokenResult in
        switch tokenResult {
        case .failure(let err):
            completion(.failure(err))   // 必须记得每个分支都回调
        case .success(let token):
            // 第二步:用 token 请求用户信息
            fetchUser(id: id, token: token) { userResult in
                switch userResult {
                case .failure(let err):
                    completion(.failure(err))
                case .success(let user):
                    // 第三步:加载头像
                    fetchAvatar(url: user.avatarURL) { avatarResult in
                        switch avatarResult {
                        case .failure(let err):
                            completion(.failure(err))
                        case .success(let avatar):
                            // 终于拿到所有数据…
                            let profile = UserProfile(user: user, avatar: avatar)
                            completion(.success(profile))
                        }
                    }
                }
            }
        }
    }
}
// 调用点 —— 还要切回主线程
loadUserProfile(id: "42") { result in
    DispatchQueue.main.async {   // 每次都要手动 dispatch
        switch result {
        case .success(let p): self.show(p)
        case .failure(let e): self.showError(e)
        }
    }
}
swift — async/await(新写法)
// ✅ async/await:直线型代码,错误用 throw 统一传播
func loadUserProfile(id: String) async throws -> UserProfile {
    let token  = try await fetchToken()           // 挂起点①:等待 token
    let user   = try await fetchUser(id: id, token: token)  // 挂起点②
    let avatar = try await fetchAvatar(url: user.avatarURL) // 挂起点③
    return UserProfile(user: user, avatar: avatar)
    // Swift 编译器把每个 await 后面的代码打包成续体(Continuation)
    // 函数看起来是同步的,但底层完全是非阻塞的
}

// 调用点 —— 在 Task 或其他 async 上下文里调用
Task { @MainActor in        // @MainActor 保证在主线程执行
    do {
        let profile = try await loadUserProfile(id: "42")
        show(profile)       // 直接调用,无需手动 DispatchQueue.main
    } catch {
        showError(error)
    }
}
💡 挂起点 (Suspension Point)
每个 await 表达式都是一个挂起点。Swift 运行时在此暂停当前任务,把线程还给系统去做其他工作,异步操作完成后再恢复执行——整个过程不阻塞任何线程。这正是它比 GCD + 信号量方案高效得多的原因。

async 函数定义规则

swift
// 1. 在函数签名 throws 之前加 async
func fetchData() async throws -> Data { ... }

// 2. async 属性(computed property)
var currentUser: User {
    get async throws { ... }
}

// 3. async 闭包
let closure: () async -> Void = {
    await Task.sleep(nanoseconds: 1_000_000_000)
    print("1 秒后执行")
}

// 4. 协议中声明 async 要求
protocol DataService {
    func fetch(id: String) async throws -> Data
}

// 5. async 函数只能在以下上下文里被 await:
//    a) 另一个 async 函数内部
//    b) Task { } 闭包内部
//    c) @MainActor 标注的函数(它本身也是 async 上下文)
//    d) async let 声明

// ❌ 错误示范:在同步上下文里直接调用 async 函数
// let data = try await fetchData()  // 编译错误!

async/await 与 throws 的组合

swift
// async throws —— 最常见组合
func fetchUser(id: String) async throws -> User {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    let (data, response) = try await URLSession.shared.data(from: url)
    // URLSession.data(from:) 原生支持 async throws(iOS 15+)

    guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
        throw APIError.badStatus
    }
    return try JSONDecoder().decode(User.self, from: data)
}

// async —— 不会抛错(内部自己处理了错误)
func fetchUserSafe(id: String) async -> User? {
    return try? await fetchUser(id: id)
}

// rethrows 在 async 场景同样有效
func withRetry<T>(times: Int, operation: () async throws -> T) async throws -> T {
    var lastError: Error?
    for _ in 0..<times {
        do { return try await operation() }
        catch { lastError = error }
    }
    throw lastError!
}

在 ViewDidLoad 等同步方法中桥接

swift — UIKit 中启动异步任务
class ProfileViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // viewDidLoad 是同步方法,不能直接 await
        // 用 Task 创建一个非结构化任务作为入口
        Task {
            await loadAndDisplayProfile()
        }
        // ⚠️ 注意:Task {} 立即返回,不等待异步任务完成
        // viewDidLoad 会继续执行 Task {} 之后的代码
    }

    private func loadAndDisplayProfile() async {
        do {
            let profile = try await loadUserProfile(id: userID)
            // Task 继承了 viewDidLoad 的 actor 上下文(MainActor)
            // 所以这里直接更新 UI 是安全的
            nameLabel.text = profile.name
            avatarView.image = profile.avatar
        } catch {
            showAlert(error: error)
        }
    }
}

② Task 创建、优先级与取消 SE-0304

Task 是什么?

Task 是 Swift 并发中的基本工作单元,类似 GCD 中的 DispatchWorkItem,但功能更强大:支持优先级、可取消、携带结果、可 await 等待。

概念GCDSwift Concurrency
异步执行单元DispatchWorkItem / 闭包Task
优先级QoS (.userInteractive / .background …)TaskPriority (.high / .medium / .low / .background)
取消DispatchWorkItem.cancel()(弱取消)Task.cancel() + 协作式取消
等待结果DispatchGroup + notify / semaphore(阻塞!)await task.value(非阻塞挂起)
错误传播回调参数传递throws / rethrows 直接传播

创建非结构化任务

swift — Task 的四种创建方式
// ① Task { } —— 继承调用方的 actor 上下文 + 优先级
let t1 = Task {
    // 如果在 @MainActor 方法里创建,这里也在主线程
    await doSomething()
}
// 等待结果(非阻塞)
let result = await t1.value   // value: Void,如果有返回值则等结果

// ② Task(priority:) —— 指定优先级
let t2 = Task(priority: .background) {
    await expensiveComputation()
}

// ③ Task.detached —— 完全独立,不继承任何上下文(actor/优先级)
let t3 = Task.detached(priority: .utility) {
    // 没有继承 actor 上下文,不会自动在主线程
    await processData()
}

// ④ 有返回值的 Task
let t4: Task<String, Error> = Task {
    return try await fetchTitle()
}
let title = try await t4.value   // 等待并获取结果,throws 会传播

// 重要区别:Task {} vs Task.detached {}
// Task {}        继承 actor 上下文(如 @MainActor)
// Task.detached  脱离 actor 上下文,运行在协作线程池上

协作式取消

Swift 并发的取消是协作式的,与 GCD 的"弱取消"类似,但更完善。取消只是设置一个标志,任务本身必须检查并响应。

swift — 取消任务
// 取消任务
let task = Task {
    await longRunningOperation()
}
// 某个时机取消(如用户点击 Cancel 按钮)
task.cancel()   // 设置取消标志,不会强制中断

// ── 在任务内部响应取消 ──────────────────────────────────

// 方式1:try Task.checkCancellation()  —— 抛出 CancellationError
func longRunningOperation() async throws {
    for i in 0..<1000 {
        try Task.checkCancellation()    // 如果已取消,立即抛错
        await processItem(i)
    }
}

// 方式2:Task.isCancelled  —— 检查标志,自己决定怎么处理
func longRunningOperation2() async -> [Result] {
    var results: [Result] = []
    for i in 0..<1000 {
        if Task.isCancelled {           // 检查取消标志
            break                       // 提前退出,返回已完成的部分
        }
        results.append(await processItem(i))
    }
    return results
}

// -----------------------------------------------------------------------
// | ❓方式1、2中,静态方法如何知道检查「哪个 Task」:                             |
// | Task 开始执行时 → 运行时把 task 实例指针存入 TLS(线程局部存储)             |
// | Task.checkCancellation() → 从 TLS 读出当前 task 实例 → 检查标志位        |
// -----------------------------------------------------------------------

// 方式3:withTaskCancellationHandler —— 立即响应取消信号
func streamData() async throws -> Data {
    return try await withTaskCancellationHandler {
        // 正常工作
        return try await fetchLargeData()
    } onCancel: {
        // 任务被取消时立即(同步)调用,可以在这里清理资源
        // ⚠️ 这个闭包可能在任意线程被调用,注意线程安全
        networkSession.invalidateAndCancel()
    }
}

// UIKit 中的典型用法:ViewController 生命周期绑定任务
class SearchViewController: UIViewController {
    private var searchTask: Task<Void, Never>?

    func search(query: String) {
        searchTask?.cancel()            // 取消上一次搜索
        searchTask = Task {
            do {
                let results = try await searchAPI.search(query)
                guard !Task.isCancelled else { return }   // 二次检查
                updateUI(with: results)
            } catch is CancellationError {
                // 被取消,不显示错误
            } catch {
                showError(error)
            }
        }
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        searchTask?.cancel()            // 页面消失时取消任务
    }
}

Task 优先级与调度

swift
// TaskPriority 优先级(从高到低)
// .high        —— 用户正在等待的操作(相当于 GCD .userInitiated)
// .medium      —— 默认优先级(大多数普通任务)
// .low / .utility  —— 用户不直接等待的操作(如后台同步)
// .background  —— 最低,不影响用户体验(如预取、清理缓存)

// 优先级提升(Priority Escalation)
// 当高优先级任务 await 低优先级任务时,Swift 会自动提升被等待任务的优先级
// 这解决了 GCD 的"优先级反转"问题

Task(priority: .high) {
    // 等待一个 .low 任务
    let t = Task(priority: .low) { await compute() }
    await t.value   // Swift 自动把 compute() 的优先级提升到 .high
}

// 读取当前任务优先级
func adaptivework() async {
    let p = Task.currentPriority   // 读取当前任务优先级
    if p >= .high {
        await fastPath()
    } else {
        await slowPath()
    }
}
⚠️ 避免在 Task 中保留强引用循环
非结构化 Task 的生命周期独立于创建它的对象。如果 Task 闭包捕获了 self,记得用 [weak self] 或确保任务在对象销毁时取消,否则会造成内存泄漏。

③ 结构化并发:async let 与 TaskGroup SE-0317

结构化并发(Structured Concurrency)的核心思想:子任务的生命周期不能超过父任务。就像函数调用栈一样,父任务结束时所有子任务必须已完成或已取消。这提供了可预测的生命周期和自动取消传播。

async let —— 并行执行固定数量任务

当你知道需要并行执行几个固定数量的任务时,async let 是最简洁的写法。对比 GCD 的 DispatchGroup,代码量减少了 80%。

swift — GCD DispatchGroup vs async let
// ❌ GCD DispatchGroup:繁琐,还要手动管理共享状态的线程安全
func loadDashboard(completion: @escaping (Dashboard) -> Void) {
    let group = DispatchGroup()
    var user: User?
    var stats: Stats?
    var feeds: [Feed] = []
    let lock = NSLock()         // 手动加锁防止竞态

    group.enter()
    fetchUser { u in
        lock.lock(); user = u; lock.unlock()
        group.leave()
    }
    group.enter()
    fetchStats { s in
        lock.lock(); stats = s; lock.unlock()
        group.leave()
    }
    group.enter()
    fetchFeeds { f in
        lock.lock(); feeds = f; lock.unlock()
        group.leave()
    }
    group.notify(queue: .main) {
        completion(Dashboard(user: user!, stats: stats!, feeds: feeds))
    }
}

// ✅ async let:三行并行,安全简洁
func loadDashboard() async throws -> Dashboard {
    async let user  = fetchUser()    // 立即启动子任务,不等待
    async let stats = fetchStats()   // 同上,三个任务并行运行
    async let feeds = fetchFeeds()   // 同上

    // await 在这里才真正等待所有三个结果
    // 如果任何一个 throws,其他子任务自动被取消
    return Dashboard(
        user:  try await user,
        stats: try await stats,
        feeds: try await feeds
    )
}

// async let 的关键语义:
// 1. async let 声明时立即启动子任务(并行)
// 2. await 时才等待结果(挂起点)
// 3. 如果在 await 之前作用域退出(如 throws),子任务自动取消
// 4. 父任务结束前必须 await 所有 async let(编译器强制)

withTaskGroup —— 动态数量的并行任务

当任务数量是动态的(如遍历数组并发处理每个元素),使用 withTaskGroup。类比 GCD 的 DispatchQueue.concurrentPerform,但更灵活、安全。

swift
// 场景:并发下载多张图片
func downloadImages(urls: [URL]) async throws -> [UIImage] {
    return try await withThrowingTaskGroup(of: (Int, UIImage).self) { group in
        // 为每个 URL 添加一个子任务
        for (index, url) in urls.enumerated() {
            group.addTask {
                let image = try await downloadImage(from: url)
                return (index, image)   // 携带 index 以便排序
            }
        }

        // 收集结果(任务完成顺序不确定,用 index 排序)
        var results: [(Int, UIImage)] = []
        for try await result in group {    // 逐个 await 完成的任务
            results.append(result)
        }

        // 按原始顺序排列
        return results.sorted { $0.0 < $1.0 }.map { $0.1 }
    }
    // withThrowingTaskGroup 作用域结束时,所有子任务必须已完成
}

// 不会抛错的版本
func downloadImagesOptional(urls: [URL]) async -> [UIImage?] {
    await withTaskGroup(of: (Int, UIImage?).self) { group in
        for (i, url) in urls.enumerated() {
            group.addTask {
                let img = try? await downloadImage(from: url)
                return (i, img)
            }
        }
        var dict: [Int: UIImage?] = [:]
        for await (i, img) in group { dict[i] = img }
        return (0..<urls.count).map { dict[$0] ?? nil }
    }
}

// 限制并发数量(类似 OperationQueue.maxConcurrentOperationCount)
func downloadWithConcurrencyLimit(urls: [URL], limit: Int = 5) async throws -> [UIImage] {
    try await withThrowingTaskGroup(of: (Int, UIImage).self) { group in
        var results: [Int: UIImage] = [:]
        var running = 0
        var iterator = urls.enumerated().makeIterator()

        // 先填满并发槽位
        while running < limit, let (i, url) = iterator.next() {
            group.addTask { (i, try await downloadImage(from: url)) }
            running += 1
        }

        // 每完成一个,再补充一个新任务
        for try await (i, img) in group {
            results[i] = img
            if let (ni, nurl) = iterator.next() {
                group.addTask { (ni, try await downloadImage(from: nurl)) }
            }
        }
        return (0..<urls.count).compactMap { results[$0] }
    }
}
✅ 结构化并发的三大优势
1. 自动取消传播:父任务取消时,所有子任务自动收到取消信号。
2. 错误传播:任意子任务抛错,其他子任务被取消,错误向上传播。
3. 生命周期保证:编译器确保父任务作用域结束时所有子任务已处理完毕,杜绝"野任务"。

④ Actor 与 MainActor SE-0306

Actor 解决了什么问题?

GCD 时代,保护共享可变状态靠的是:os_unfair_lockNSLock、串行 DispatchQueue + sync。这些都需要工程师自己记住并正确使用,一旦遗漏就是数据竞争 Bug。
actor 是 Swift 内置的引用类型,编译器强制要求所有对 actor 内部状态的访问都经过隔离(isolation),彻底消除数据竞争。

swift — GCD 串行队列保护 vs Actor
// ❌ GCD 方式:手动串行队列,忘记 sync/async 就出 Bug
class OldCache {
    private var store: [String: Data] = [:]
    private let queue = DispatchQueue(label: "cache.queue")   // 串行队列保护

    func set(_ value: Data, for key: String) {
        queue.async { self.store[key] = value }      // 异步写(不阻塞调用方)
    }
    func get(_ key: String) -> Data? {
        queue.sync { store[key] }                    // 同步读(阻塞调用方线程!)
        // 如果 get 在主线程调用,而 queue 里已有任务,会造成主线程卡顿
    }
}

// ✅ Actor 方式:编译器保证串行访问
actor ImageCache {
    private var store: [URL: UIImage] = [:]
    private var loadingTasks: [URL: Task<UIImage, Error>] = [:]

    // 普通方法默认在 actor 上串行执行(相当于 queue.async)
    func store(_ image: UIImage, for url: URL) {
        store[url] = image
    }

    func image(for url: URL) -> UIImage? {
        store[url]    // 直接访问,无需加锁
    }

    // 防止重复请求同一 URL(double-fetch 问题)
    func loadImage(from url: URL) async throws -> UIImage {
        if let cached = store[url] { return cached }

        if let existing = loadingTasks[url] {
            return try await existing.value    // 复用进行中的任务
        }
        let task = Task { try await downloadImage(from: url) }
        loadingTasks[url] = task

        do {
            let image = try await task.value
            store[url] = image
            loadingTasks.removeValue(forKey: url)
            return image
        } catch {
            loadingTasks.removeValue(forKey: url)
            throw error
        }
    }

    // nonisolated:不访问 actor 状态的方法,可从任意上下文同步调用
    nonisolated func cacheKey(for url: URL) -> String {
        url.absoluteString.replacingOccurrences(of: "/", with: "_")
    }
}

// 调用 actor 的方法必须 await
let cache = ImageCache()
Task {
    let image = try await cache.loadImage(from: imageURL)   // ✅ 跨 actor 边界必须 await
    let key   = cache.cacheKey(for: imageURL)               // ✅ nonisolated,无需 await
}

MainActor —— 主线程 Actor

@MainActor 是全局 actor,保证代码在主线程执行。它完全取代了 DispatchQueue.main.async { } 的用法,而且类型系统会帮你检查是否正确使用。

swift
// 1. 标注整个类在主线程(最常见于 ViewController 和 ViewModel)
@MainActor
class ProfileViewModel: ObservableObject {
    @Published var user: User?
    @Published var isLoading = false
    @Published var errorMessage: String?

    func loadProfile(id: String) async {
        isLoading = true
        defer { isLoading = false }
        do {
            // ✅ 在 actor 隔离的上下文里,可以跨越 await 安全访问属性
            user = try await userService.fetchUser(id: id)
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}

// 2. 标注单个方法在主线程
class DataController {
    @MainActor
    func updateUI(with data: [Item]) {
        tableView.reloadData()
    }

    func fetchData() async {
        let data = try? await api.fetchItems()
        await updateUI(with: data ?? [])    // 跨 actor 边界,需要 await
    }
}

// 3. MainActor.run —— 从非主线程切回主线程(替代 DispatchQueue.main.async)
Task.detached(priority: .background) {
    let result = await heavyComputation()
    await MainActor.run {                   // ✅ 替代 DispatchQueue.main.async
        updateLabel.text = result
    }
}

// 对比旧写法:
// DispatchQueue.main.async { self.label.text = result }  // 旧
// await MainActor.run { label.text = result }            // 新(等价但类型安全)

// 4. 假设推断(Assumed MainActor)
// UIViewController、UIView 的子类在 Xcode 15 / Swift 5.9+ 中隐式 @MainActor
// 所以在 UIViewController 子类里创建 Task,它自动继承 @MainActor
💡 Actor 与 class 的区别
Actor 是引用类型(和 class 一样),但:
① 不支持继承(类似 struct)
② 访问其存储属性/方法必须跨越 actor 边界(加 await
③ 同一时间只有一个任务在 actor 内部执行(串行化保证)
nonisolated 方法可以绕过隔离,适合纯计算或不访问状态的方法
🆚 Swift 5 vs Swift 6 — Actor Isolation 变化
Swift 5(宽松模式):actor isolation 警告为可选,部分跨 actor 访问不报错。
Swift 6(严格模式):所有跨 actor 访问均为编译期错误;新增 nonisolated(unsafe) 修饰符,用于明确声明"我保证安全,编译器不必检查";全局 actor 推断规则更严格(详见 §1-11)。
swift — nonisolated(unsafe)
// Swift 6 新增:nonisolated(unsafe) 用于跨 actor 的不安全共享
actor Config {
    // Swift 5:nonisolated let 只能用于不可变值
    nonisolated let apiVersion: String = "v2"

    // Swift 6 新增:nonisolated(unsafe) 允许跨隔离域访问可变状态
    // 开发者需自行保证线程安全(编译器不检查)
    nonisolated(unsafe) var legacyFlag: Bool = false
}

⑤ Sendable 协议与数据隔离 SE-0302

Sendable 是 Swift 并发的"类型安全护盾"。跨越并发域(actor 边界、Task 边界)传递的值必须是 Sendable,否则编译器会报警。这从根本上阻止了数据竞争。

Sendable类型
// ── 哪些类型自动满足 Sendable ──────────────────────────────
// ✅ 值类型(struct/enum)且所有存储属性也是 Sendable
struct Point: Sendable { var x: Double; var y: Double }
// Double, Int, String, Bool 等基础类型都是 Sendable
// Array<Element>, Dictionary<Key,Value> 在 Element/Key/Value 是 Sendable 时也是

// ✅ 不可变 class(所有属性是 let 且 Sendable)
final class ImmutableConfig: Sendable {
    let baseURL: URL
    let timeout: TimeInterval
    init(baseURL: URL, timeout: TimeInterval) {
        self.baseURL = baseURL; self.timeout = timeout
    }
}

// ✅ Actor(天生 Sendable,因为它序列化了所有访问)
actor DataStore: Sendable { ... }

// ── 需要手动声明 ──────────────────────────────────────────
// 当编译器无法自动推断时,你可以用 @unchecked Sendable 承诺线程安全
// ⚠️ 这把安全责任转回到你自己手上
final class ThreadSafeCounter: @unchecked Sendable {
    private var value = 0
    private let lock = NSLock()

    func increment() { lock.withLock { value += 1 } }
    func get() -> Int { lock.withLock { value } }
}

@Sendable 闭包

// ── @Sendable 闭包 ──────────────────────────────
// @Sendable 闭包的约束:
// 1. 不能捕获非 Sendable 的可变状态(var)
// 2. 捕获的引用类型必须是 Sendable
// 3. 本身可以安全地跨线程/actor 传递

// 普通闭包 —— 不能跨 actor 传递
let image = UIImage(named: "photo")  // UIImage 非 Sendable
let closure = {
    print(image)  // 捕获了非 Sendable 的引用
}
// ✅ @Sendable 闭包 —— 编译器强制检查捕获内容
// UIImage → Data(Data 是 Sendable)
let image = UIImage(named: "photo")!
let imageData = image.pngData()!  // ✅ Data 是 Sendable
// 闭包捕获的是 Data,而非 UIImage
let work: @Sendable () -> Void = {
    // 在闭包内部重建 UIImage
    let img = UIImage(data: imageData)!
    print("处理图片: \(img.size)")
}


// ── 定义接受 @Sendable 闭包的函数 ──
func schedule(_ work: @Sendable @escaping () -> Void) {
    Task.detached {   // detached task 要求 @Sendable
        work()
    }
}

// @Sendable 捕获规则总结
// ✅ 可以捕获的类型
@Sendable () -> Void = {
    let _ = someInt        // Int        ✅ 值类型
    let _ = someString     // String     ✅ 值类型
    let _ = someData       // Data       ✅ 值类型
    let _ = someStruct     // Struct     ✅ 值类型(默认 Sendable)
    let _ = someActor      // Actor      ✅ Actor 是 Sendable
    let _ = someEnum       // Enum       ✅ 通常 Sendable
}
// ❌ 不能捕获的类型
@Sendable () -> Void = {
    let _ = uiImage        // UIImage    ❌ class,非 Sendable
    let _ = uiView         // UIView     ❌ class,非 Sendable
    let _ = nsObject       // NSObject   ❌ class,非 Sendable
    var counter = 0        // var        ❌ 可变状态,线程不安全
}

        
常见问题

// ── 常见问题:UIImage 不是 Sendable ──────────────────────
// UIImage 是 class,不满足 Sendable(在 Swift strict concurrency 下警告/错误)

// 解决方案 1:在 actor 边界之前完成转换
actor ImageProcessor {
    func process(imageData: Data) async -> Data {   // 传 Data(Sendable),不传 UIImage
        let image = UIImage(data: imageData)!
        // 在 actor 内部处理
        return compress(image)
    }
}

// 解决方案 2:用 @Sendable 闭包
// ✅ @Sendable 闭包 —— 编译器强制检查捕获内容
// UIImage → Data(Data 是 Sendable)
let image = UIImage(named: "photo")!
let imageData = image.pngData()!  // ✅ Data 是 Sendable
// 闭包捕获的是 Data,而非 UIImage。
// 闭包可以安全地跨线程/actor 传递
let work: @Sendable () -> Void = {
    // 在闭包内部重建 UIImage
    let img = UIImage(data: imageData)!
    print("处理图片: \(img.size)")
}
    
思维模型

跨并发边界传递数据的原则:
UIImage (非Sendable)
    ↓ 在边界之前转换
Data / CGSize / String (Sendable)
    ↓ 安全传入 @Sendable 闭包
另一个并发域
    ↓ 需要时重建
UIImage (在新的上下文中)
✅本质:
@Sendable 是编译器在并发边界上设置的"安检",强迫你在过边界之前把"危险物品"(非线程安全对象)转换为"安全物品"(值类型/Sendable类型)。
swift


// ── Swift 6 严格并发检查 ──────────────────────────────────
// Swift 6 默认开启严格并发检查(SE-0337)
// 在此之前可以逐步迁移:Build Settings → Swift Language Version = Swift 6
// 或在文件/模块级别使用 @preconcurrency import 过渡
🆚 Swift 5 vs Swift 6 — Sendable 检查强度对比
特性Swift 5(宽松)Swift 6(严格)
Sendable 违规警告(可忽略)编译错误
跨 actor 传递非 Sendable警告编译错误
@unchecked Sendable可用可用(需更谨慎)
闭包 Sendable 推断手动标注编译器自动推断更多场景
全局变量并发访问无限制必须在 actor 或为 Sendable 常量
Swift 6 的严格并发是选择性加入的:你可以按模块逐步升级,使用 @preconcurrency 导入旧模块以消除迁移期间的误报。详细迁移策略见 §1-8。

⑥ AsyncSequence 与 AsyncStream SE-0298

AsyncSequence 是什么?

普通 Sequence 是同步的(for-in 立即消耗所有元素)。AsyncSequence 是异步版本——每次获取下一个元素都可能挂起,等待数据就绪。类比 Combine 的 Publisher,但更贴近 for-in 的心智模型。

swift
// ── for await in —— 消费 AsyncSequence ──────────────────

// URLSession.bytes(from:) 返回 AsyncSequence<UInt8>(iOS 15+)
func streamLargeFile(url: URL) async throws {
    let (bytes, _) = try await URLSession.shared.bytes(from: url)
    var buffer = Data()
    for try await byte in bytes {          // 逐字节异步读取(低内存占用)
        buffer.append(byte)
        if buffer.count >= 4096 {
            await processChunk(buffer)
            buffer.removeAll()
        }
    }
}

// Notification 转 AsyncSequence(iOS 15+)
func waitForKeyboardShow() async {
    let notifications = NotificationCenter.default.notifications(
        named: UIResponder.keyboardDidShowNotification
    )
    for await notification in notifications {    // 每次键盘弹出执行一次循环体
        if let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
            await adjustLayout(for: frame)
        }
        break   // 只处理第一次,退出循环
    }
}

// ── AsyncStream —— 桥接回调/委托到 AsyncSequence ─────────

// 场景:把 CLLocationManager 委托转成 AsyncStream
func makeLocationStream() -> AsyncStream<CLLocation> {
    AsyncStream { continuation in
        let manager = LocationDelegateWrapper { location in
            continuation.yield(location)    // 产生新值
        }
        continuation.onTermination = { _ in
            manager.stop()                  // 流结束时清理
        }
        manager.start()
    }
}

// 使用
Task {
    let stream = makeLocationStream()
    for await location in stream {
        print("新位置:\(location.coordinate)")
        if shouldStop { break }             // 主动退出,触发 onTermination
    }
}

// ── AsyncThrowingStream —— 带错误的流 ────────────────────
func makeWebSocketStream(url: URL) -> AsyncThrowingStream<Data, Error> {
    AsyncThrowingStream { continuation in
        let task = URLSession.shared.webSocketTask(with: url)
        task.resume()

        Task {
            do {
                while true {
                    let message = try await task.receive()
                    switch message {
                    case .data(let data):    continuation.yield(data)
                    case .string(let str):   continuation.yield(str.data(using: .utf8)!)
                    @unknown default: break
                    }
                }
            } catch {
                continuation.finish(throwing: error)   // 结束并传递错误
            }
        }
        continuation.onTermination = { _ in task.cancel() }
    }
}

// 消费带错误的流
Task {
    do {
        for try await data in makeWebSocketStream(url: wsURL) {
            await handleMessage(data)
        }
    } catch {
        print("WebSocket 错误:\(error)")
    }
}

⑦ 从 GCD 迁移实战指南 迁移

withCheckedContinuation —— 包装回调 API

现有大量基于回调的第三方 SDK 和老代码,可以用 withCheckedContinuation 包装成 async 函数,是迁移的"万能胶"。

swift
// ── withCheckedContinuation(不会 throw)────────────────

// 旧接口
func oldFetchUser(id: String, completion: @escaping (User?) -> Void) { ... }

// 包装成 async
func fetchUser(id: String) async -> User? {
    await withCheckedContinuation { continuation in
        oldFetchUser(id: id) { user in
            continuation.resume(returning: user)   // ⚠️ 必须且只能调用一次!
        }
    }
}
// "Checked" 版本会在 DEBUG 下检测:
// - continuation 被调用超过一次 → 崩溃
// - continuation 从未被调用   → 崩溃(任务泄漏)

// ── withCheckedThrowingContinuation(会 throw)──────────

// 旧接口(Result 回调)
func oldFetchData(url: URL, completion: @escaping (Result<Data, Error>) -> Void) { ... }

// 包装成 async throws
func fetchData(url: URL) async throws -> Data {
    try await withCheckedThrowingContinuation { continuation in
        oldFetchData(url: url) { result in
            continuation.resume(with: result)   // 直接传 Result
        }
    }
}

// ── withUnsafeContinuation(性能更高,无检查)────────────
// 只在对性能极度敏感且确定不会误用时才使用
func fetchDataFast(url: URL) async throws -> Data {
    try await withUnsafeThrowingContinuation { continuation in
        oldFetchData(url: url) { result in
            continuation.resume(with: result)
        }
    }
}

// ── 包装委托(Delegate)模式 ──────────────────────────────
// CLGeocoder 示例
extension CLGeocoder {
    func reverseGeocode(location: CLLocation) async throws -> [CLPlacemark] {
        try await withCheckedThrowingContinuation { continuation in
            self.reverseGeocodeLocation(location) { placemarks, error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(returning: placemarks ?? [])
                }
            }
        }
    }
}

常见 GCD 模式迁移速查表

GCD 模式Swift Concurrency 等价写法
DispatchQueue.global().async { }Task.detached { }Task(priority: .background) { }
DispatchQueue.main.async { }await MainActor.run { }@MainActor func
DispatchQueue.main.sync { }@MainActor 上下文中直接执行,无需显式 sync
DispatchGroup.enter/leave/notifyasync letwithTaskGroup
DispatchSemaphore(阻塞!)async let / await task.value(非阻塞挂起)
DispatchQueue(label:, attributes: .concurrent)withTaskGroup 并发执行
DispatchWorkItem.cancel()Task.cancel() + 协作式检查
NSLock / os_unfair_lockactor(隔离共享状态)
DispatchQueue + barrier asyncactor 的普通方法(天然串行)
回调闭包传递结果async throws 函数返回值
Timer + GCDTask.sleep(for:)Clock.sleep
swift — Task.sleep 替代 DispatchQueue after
// 旧:DispatchQueue.main.asyncAfter
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    self.hideLoadingView()
}

// 新:Task.sleep(非阻塞,可取消)
Task {
    try await Task.sleep(for: .seconds(2))   // iOS 16+,支持取消
    // 或 Task.sleep(nanoseconds: 2_000_000_000)  iOS 15+
    hideLoadingView()
}

// 更精确的写法(iOS 16+ ContinuousClock)
Task {
    try await Task.sleep(until: .now + .seconds(2), clock: .continuous)
    hideLoadingView()
}

⑧ Swift 6 严格并发迁移 Swift 6

Swift 6 将并发安全检查从"警告"升级为"编译错误",彻底消灭数据竞争。迁移不必一步到位——Swift 提供了模块级别的渐进升级路径。

Swift 5 宽松模式 vs Swift 6 严格模式

维度Swift 5(-strict-concurrency=minimal)Swift 6(-strict-concurrency=complete)
Sendable 违规可选警告编译错误
Actor 隔离违规部分警告编译错误
全局可变状态无限制必须隔离(actor / Sendable const)
数据竞争检测运行时(偶发)编译期静态保证
迁移成本中等(通常数小时~数天)

逐步迁移策略

推荐按模块粒度逐步升级,不必一次性改动整个代码库:

swift — Package.swift 模块级升级
// Package.swift 中为单个 target 启用 Swift 6 严格并发
.target(
    name: "MyNetworkLayer",
    dependencies: [...],
    swiftSettings: [
        // 步骤1:先用 targeted 模式,只检查你自己的代码(推荐起点)
        .enableExperimentalFeature("StrictConcurrency"),
        // 步骤2:全量检查(等价于 Swift 6 语言模式)
        // .swiftLanguageVersion(.version("6"))
    ]
)

// Xcode Build Settings 等价设置:
// Swift Language Version = Swift 6
// 或:Other Swift Flags = -strict-concurrency=targeted

常见编译器报错及修复

① 全局可变变量:隔离到 actor

Swift 5 — 报警告/错误
// ❌ 全局可变变量(Swift 6 编译错误)
var currentUser: User? = nil

func updateUser(_ user: User) {
    currentUser = user   // 数据竞争!
}
Swift 6 — 修复方案
// ✅ 方案1:隔离到 @MainActor
@MainActor var currentUser: User? = nil

// ✅ 方案2:放入 actor
actor UserStore {
    var currentUser: User? = nil
    func update(_ user: User) { currentUser = user }
}

② 非 Sendable 类型跨 actor 传递

Swift 5 — 警告(Swift 6 报错)
// ❌ URLRequest 是 Sendable,但自定义 class 不是
class RequestBuilder {
    var headers: [String: String] = [:]
}

actor NetworkActor {
    func execute(_ builder: RequestBuilder) async {
        // Swift 6: error: passing argument of non-Sendable type
    }
}
Swift 6 — 修复方案
// ✅ 改为 struct(值语义,自动 Sendable)
struct RequestBuilder: Sendable {
    var headers: [String: String] = [:]
}

// ✅ 或加 @unchecked Sendable(需自行保证安全)
final class RequestBuilder: @unchecked Sendable {
    private let lock = NSLock()
    var headers: [String: String] = [:]
}

③ 回调闭包中的并发违规

Swift 5 — 可能数据竞争
// ❌ 在非隔离闭包里修改 actor 状态
actor Counter {
    var count = 0
}
let c = Counter()
// Swift 6 error: mutation of actor-isolated property
// 'count' can not be done from a nonisolated context
DispatchQueue.global().async {
    Task { c.count += 1 }  // 跨 actor 边界未 await
}
Swift 6 — 正确写法
// ✅ 通过 actor 方法修改
actor Counter {
    var count = 0
    func increment() { count += 1 }
}
let c = Counter()
Task {
    await c.increment()   // ✅ 跨 actor 边界用 await
}

④ @preconcurrency 过渡旧模块

swift — @preconcurrency 用法
// 当第三方库或旧模块尚未迁移到 Swift 6 时,用 @preconcurrency 抑制误报
@preconcurrency import OldFramework   // 导入时标注:此模块的 Sendable 警告降级处理

// 在遵从协议时标注(协议来自旧模块)
@preconcurrency
extension MyModel: OldProtocol {
    func oldMethod() { ... }
}

// 标注回调闭包来自旧模块
func legacyAPI(completion: @preconcurrency @escaping (Result<Data, Error>) -> Void) { ... }
✅ 迁移建议
推荐顺序:网络层 → 数据层 → ViewModel → UI 层。从无 UI 依赖的底层模块开始,逐步向上。每个模块改好后跑一次测试,确保功能不回归后再继续。

⑨ ~Copyable 非可复制类型 SE-0390

Swift 中所有值类型(struct/enum)默认都是可复制的——每次赋值/传参都产生一份拷贝。~Copyable 打破这一约定,让你创建"只能移动、不能复制"的类型,适合建模独占资源(文件句柄、加密密钥、网络连接等)。

为什么需要 ~Copyable?

问题:可复制导致资源泄漏
// ❌ 普通 struct:每次赋值都复制
struct FileHandle {
    let fd: Int32
    func close() { Darwin.close(fd) }
}
var h1 = FileHandle(fd: open("/tmp/log", O_RDONLY))
var h2 = h1          // 无声地复制了文件描述符!
h1.close()           // fd 关闭一次
h2.close()           // fd 再次关闭 —— 双重关闭 Bug!
解决:~Copyable 保证唯一所有权
// ✅ ~Copyable struct:不可复制,只能移动
struct FileHandle: ~Copyable {
    let fd: Int32
    consuming func close() {   // consuming 消耗所有权
        Darwin.close(fd)
    }
    deinit { Darwin.close(fd) }  // 自动关闭
}
var h1 = FileHandle(fd: open("/tmp/log", O_RDONLY))
// var h2 = h1  // ❌ 编译错误:不可复制类型不能被复制
let fd = consume h1  // ✅ 显式移动所有权

参数修饰符:consuming / borrowing / inout

swift — 三种参数修饰符对比
struct CryptoKey: ~Copyable {
    let rawBytes: [UInt8]

    // consuming —— 函数拿走所有权,调用后原变量不可用
    consuming func encrypt(_ data: Data) -> Data {
        defer { /* rawBytes 在此被销毁 */ }
        return performEncryption(data, key: rawBytes)
    }

    // borrowing —— 函数只读借用,不消耗所有权(默认行为)
    borrowing func keyID() -> String {
        return rawBytes.prefix(8).map { String($0, radix: 16) }.joined()
    }

    // inout —— 函数可读写,不消耗所有权
    mutating func rotate() {
        rawBytes = generateNewKey()
    }
}

// 使用示例
func useKey() {
    var key = CryptoKey(rawBytes: generateKey())

    let id = key.keyID()              // borrowing:key 仍可用
    key.rotate()                       // inout:key 仍可用
    let encrypted = key.encrypt(data) // consuming:key 不可再用
    // print(key.keyID())             // ❌ 编译错误:key 已被消耗
}

典型用例:唯一资源管理

swift — 加密密钥生命周期管理
// 建模一次性令牌(只能使用一次)
struct OneTimeToken: ~Copyable {
    private let value: String

    init(_ value: String) { self.value = value }

    // consuming 方法:使用后令牌失效
    consuming func redeem() -> Bool {
        return validateToken(value)
        // value 在函数返回后被销毁
    }
}

// 使用
func processToken(_ token: consuming OneTimeToken) {
    if token.redeem() {   // consume token here
        print("令牌有效")
    }
    // token 已被消耗,不会再次被使用 —— 编译器静态保证
}

// ~Copyable 与泛型(Swift 6.0+)
func wrap<T: ~Copyable>(_ value: consuming T) -> Box<T> {
    return Box(value: consume value)
}
💡 ~Copyable vs class
class 是引用类型,多个引用可以指向同一对象(共享语义)。~Copyable struct 是值类型但具有独占所有权语义——不存在多个引用共享,因此更适合建模"资源令牌"而非"共享状态"。
~Copyable 不等于 class:它在栈上分配,没有引用计数开销。

⑩ Typed Throws SE-0413

Swift 5 的 throws 关键字会擦除错误类型——函数签名只说明"可能抛错",但不说明抛什么类型的错误。调用方必须处理 any Error,丢失了类型信息。Swift 6 引入 throws(ErrorType) 解决这一痛点。

Swift 5 问题:错误类型擦除

Swift 5 — 类型信息丢失
enum NetworkError: Error {
    case noConnection
    case timeout
    case badStatus(Int)
}

// throws 擦除类型:只知道会抛 Error
func fetchData(url: URL) throws -> Data {
    // ...
    throw NetworkError.timeout
}

// 调用方:必须处理 any Error
// 无法针对 NetworkError 的 case 进行穷举
do {
    let data = try fetchData(url: url)
} catch let e as NetworkError {
    // 需要手动 as? 转换
} catch {
    // 必须有兜底 catch,即使你知道只会抛 NetworkError
}
Swift 6 — 具体错误类型
enum NetworkError: Error {
    case noConnection
    case timeout
    case badStatus(Int)
}

// throws(NetworkError):明确声明错误类型
func fetchData(url: URL) throws(NetworkError) -> Data {
    // ...
    throw NetworkError.timeout  // 只能抛 NetworkError
}

// 调用方:可以穷举,无需兜底 catch
do {
    let data = try fetchData(url: url)
} catch .noConnection {
    showOfflineUI()
} catch .timeout {
    retryAfterDelay()
} catch .badStatus(let code) {
    handleHTTPError(code)
}
// ✅ 无需 catch { } 兜底,编译器知道已穷举

泛型抛出:throws(E)

swift — 泛型 throws 替代 rethrows
// Swift 5:rethrows 只能传播闭包的错误,无法指定类型
func map<T, E: Error>(_ array: [T], _ transform: (T) throws -> T) rethrows -> [T] { ... }

// Swift 6:throws(E) 泛型抛出,精确传播错误类型
func map<T, E: Error>(
    _ array: [T],
    _ transform: (T) throws(E) -> T
) throws(E) -> [T] {
    var result: [T] = []
    for element in array {
        result.append(try transform(element))
    }
    return result
}

// 使用:错误类型自动推断
let parsed = try map(strings) { s throws(ParseError) -> Int in
    guard let n = Int(s) else { throw ParseError.invalidFormat(s) }
    return n
}
// parsed 的 throws 类型是 ParseError,不是 any Error

// 永不抛错(等价于非 throwing)
func safeMap<T>(_ array: [T], _ transform: (T) throws(Never) -> T) -> [T] {
    // throws(Never) 表示永不抛错,可安全去掉 try
    return array.map(transform)
}

与 Result<T, E> 的关系

swift — Typed Throws ↔ Result 互转
// Swift 6 中 Typed Throws 与 Result<T,E> 完全等价
// 两者可以无缝互转

func fetchUser(id: String) throws(APIError) -> User { ... }

// throws → Result
let result: Result<User, APIError> = Result { try fetchUser(id: "42") }

// Result → throws
func withResult() throws(APIError) -> User {
    return try result.get()  // get() 自动传播 APIError
}

// 好处:async throws(E) 让异步代码也能精确错误类型
func fetchAsync(id: String) async throws(APIError) -> User {
    let data = try await network.get("/users/\(id)")
    return try JSONDecoder().decode(User.self, from: data)
    // 这里的 throws 只传播 APIError(需要确保所有抛错都是 APIError)
}
✅ 何时使用 Typed Throws?
适合:① 库/模块 API(调用方需要区分不同错误类型);② 错误类型有穷举意义的场景(枚举错误)。
不适合:① 内部实现细节;② 错误可能来自多个不同来源(此时 any Error 更灵活)。

⑪ Swift 6 Actor 新特性 SE-0423

Swift 6 对 actor 模型进行了多项改进:全局 actor 推断规则收紧、新增 nonisolated(unsafe)、协议遵从与 actor isolation 的交互更清晰。

全局 Actor 推断规则变化(SE-0423)

Swift 5 — 隐式全局 actor 推断宽松
// Swift 5:UIViewController 子类隐式推断为 @MainActor
// 但规则不一致,有时推断,有时不推断

class MyVC: UIViewController {
    var data: [Item] = []
    // Swift 5: data 可能隐式被认为在 @MainActor 上
    // 但不同版本行为不同,造成混淆

    func loadData() {
        Task {
            // 是否在 MainActor 上?不确定…
            self.data = await fetchItems()
        }
    }
}
Swift 6 — 显式推断,规则清晰
// Swift 6:UIViewController 子类明确推断为 @MainActor
// SE-0423 规定:只有直接符合标注了 @MainActor 协议的类型才自动推断

@MainActor
class MyVC: UIViewController {   // 显式标注,语义清晰
    var data: [Item] = []

    func loadData() {
        Task { @MainActor in      // 明确 actor 上下文
            self.data = await fetchItems()
        }
    }

    nonisolated func pureComputation() -> Int {
        return data.count * 2   // ❌ 仍需 await 访问 data
        // ✅ nonisolated 只能访问 nonisolated 成员
    }
}

nonisolated(unsafe) 修饰符

swift — nonisolated(unsafe) 用法场景
// nonisolated(unsafe) 用于:
// 1. 与旧代码互操作时需要跨 actor 访问某属性
// 2. 开发者自行用锁/原子操作保证安全,不需要编译器检查

actor AppState {
    // 普通 nonisolated let:只允许不可变值
    nonisolated let appVersion: String = "2.0"

    // nonisolated(unsafe):可变,但编译器不检查安全性
    // 适合:已有外部同步机制(如 OSAllocatedUnfairLock)
    nonisolated(unsafe) var legacyObservers: [AnyObject] = []
}

// 典型场景:与 Objective-C 代理模式互操作
@MainActor
class DataManager: NSObject {
    // delegate 属性需要从非 actor 上下文访问(ObjC 运行时)
    nonisolated(unsafe) weak var delegate: DataManagerDelegate?

    func fetchData() async {
        let result = await network.fetch()
        // delegate 的调用需要自行保证在正确线程
        delegate?.dataManager(self, didReceive: result)
    }
}

Actor Isolation 与协议遵从

swift — Swift 6 协议遵从规则
// Swift 6 规则:actor 遵从协议时,协议要求必须匹配 isolation

protocol Refreshable {
    func refresh() async
}

// ✅ actor 遵从协议 —— 方法自动 actor-isolated
actor DataStore: Refreshable {
    func refresh() async {
        // 在 DataStore 的 actor 上下文中执行
        await loadFromNetwork()
    }
}

// ✅ @MainActor 类遵从协议 —— 方法在 MainActor 上执行
@MainActor
class ViewModel: Refreshable {
    func refresh() async {
        // 在 MainActor 上执行
        await updateUI()
    }
}

// ⚠️ Swift 6 新增:协议可以声明 isolation 要求
protocol MainActorProtocol {
    @MainActor func updateUI()
}

// 遵从者必须在 @MainActor 上实现
struct MyView: MainActorProtocol {
    @MainActor func updateUI() { ... }
}

// Swift 6 中 async 协议要求的 isolation 更严格:
// 不再允许在 nonisolated 上下文中遵从 actor-isolated 协议

@MainActor 在 Swift 6 下的行为变化

swift — @MainActor 闭包推断变化
// Swift 5:@MainActor 推断有时不一致
// Swift 6:规则统一 ——
// @MainActor 方法内创建的 Task 闭包,不自动继承 @MainActor

@MainActor
class ViewController: UIViewController {
    var items: [Item] = []

    func loadData() {
        // Swift 5:Task 闭包可能隐式继承 @MainActor(取决于上下文)
        // Swift 6:需要显式标注才在主线程执行
        Task { @MainActor in   // ✅ 显式标注
            let data = await fetch()
            self.items = data  // 安全:在 @MainActor 上
        }

        // 不标注时,Task 在协作线程池中运行(非主线程)
        Task {
            let data = await fetch()
            // self.items = data  // ❌ Swift 6 编译错误:跨 actor 边界
            await MainActor.run { self.items = data }  // ✅ 需要显式切换
        }
    }
}
🔑 Swift 6 Actor 核心原则
  1. 显式优于隐式:不再依赖 actor isolation 的隐式推断,所有隔离意图明确标注
  2. nonisolated(unsafe) 是逃生舱口,不是常规写法——优先用正确的 actor 隔离
  3. 协议遵从的 isolation 必须精确匹配,Swift 6 编译器会强制执行
  4. 迁移旧代码时,@preconcurrency 是过渡期的好工具
Part 2

Combine 响应式编程

Combine 是 Apple 官方的函数响应式框架,与 RxSwift 思想相同但深度集成 Swift 类型系统。本部分从 Publisher/Subscriber 生命周期出发,系统讲解操作符、调度器、内存管理,以及与 async/await 的互操作。

Combine iOS 13+ Apple First-Party

① Publisher · Subscriber · Subscription 生命周期

核心三角关系

Combine 的整个框架建立在三个协议上:

  • Publisher:数据/事件的生产者。定义输出类型 Output 和失败类型 Failure
  • Subscriber:数据的消费者。向 Publisher 请求数据,接收值和完成/错误事件。
  • Subscription:连接 Publisher 和 Subscriber 的令牌,控制背压(backpressure)。
swift — 完整生命周期
import Combine

// ── 生命周期步骤:──────────────────────────────────────────
// 1. Subscriber 调用 Publisher.subscribe(subscriber)
// 2. Publisher 调用 subscriber.receive(subscription:)
// 3. Subscriber 调用 subscription.request(.unlimited) 或 request(.max(n)) 发出需求
// 4. Publisher 向 Subscriber 发送 0...n 个值:subscriber.receive(value)
// 5a. Publisher 发送 completion(.finished) 正常结束
// 5b. Publisher 发送 completion(.failure(error)) 错误结束
// 6. Subscriber/Subscription 被释放,取消订阅

// ── 最简单的订阅:sink ────────────────────────────────────
let publisher = [1, 2, 3].publisher   // 数组自带 .publisher 属性

let cancellable = publisher
    .sink(
        receiveCompletion: { completion in
            switch completion {
            case .finished:
                print("完成!")
            case .failure(let error):
                print("错误:\(error)")
            }
        },
        receiveValue: { value in
            print("收到值:\(value)")
        }
    )
// 输出:收到值:1  收到值:2  收到值:3  完成!

// ── assign(to:on:) —— 直接绑定到属性 ─────────────────────
class ViewModel: ObservableObject {
    @Published var title = ""
}
let vm = ViewModel()
let c = Just("Hello Combine")
    .assign(to: \.title, on: vm)   // 每次值更新自动写入 vm.title

// ── 自定义 Subscriber ─────────────────────────────────────
struct PrintSubscriber: Subscriber {
    typealias Input   = Int
    typealias Failure = Never

    func receive(subscription: Subscription) {
        subscription.request(.unlimited)   // 请求所有值
    }
    func receive(_ input: Int) -> Subscribers.Demand {
        print("收到:\(input)")
        return .none   // 不追加需求
    }
    func receive(completion: Subscribers.Completion<Never>) {
        print("订阅结束:\(completion)")
    }
}
[1,2,3].publisher.subscribe(PrintSubscriber())

背压(Backpressure)流程图

Combine 是 拉模型(pull-based):Subscriber 主动向 Publisher 索要值,而不是 Publisher 随意推送。这防止了"快生产者压垮慢消费者"的问题。

背压交互时序
// Subscriber → request(.max(3))  → Publisher   ← 先告诉我要 3 个
// Subscriber ← value(1)          ← Publisher
// Subscriber ← value(2)          ← Publisher
// Subscriber ← value(3)          ← Publisher
// (Publisher 停止,等待下一次 request)
// Subscriber → request(.max(2))  → Publisher   ← 再要 2 个
// Subscriber ← value(4)          ← Publisher
// Subscriber ← value(5)          ← Publisher
// Subscriber ← completion(.finished) ← Publisher

// 实际开发中 sink 默认用 .unlimited(不限流)
// 需要精细控制背压时,实现自定义 Subscriber 并在 receive(_:) 里返回精确 Demand
struct ThrottledSubscriber: Subscriber {
    typealias Input = Int; typealias Failure = Never

    func receive(subscription: Subscription) {
        subscription.request(.max(1))   // 每次只要 1 个
    }
    func receive(_ input: Int) -> Subscribers.Demand {
        process(input)
        return .max(1)   // 处理完再要下一个
    }
    func receive(completion: Subscribers.Completion<Never>) { }
}

② 内置 Publisher 类型详解

swift — Just / Empty / Fail
// Just —— 发出单个值然后 finish(Failure = Never)
let justPub = Just(42)
justPub.sink { print($0) }   // 42

// Empty —— 立即 finish,不发任何值
let empty = Empty<Int, Never>()
// 适合"没有数据但需要一个 Publisher"的占位场景

// Fail —— 立即发出错误
let fail = Fail<Int, URLError>(error: URLError(.notConnectedToInternet))

// Optional.Publisher —— Optional 的内置 Publisher
let opt: Int? = 42
let optPub = opt.publisher   // 有值时发一个值,nil 时什么都不发
swift — Future(封装单次异步操作)
// Future 是 Combine 版的"Promise":发出单个值或错误,然后结束
// 注意:Future 的闭包在创建时立即执行(eager),不是在订阅时才执行

func fetchCurrentUser() -> Future<User, Error> {
    Future { promise in
        // 这段代码在 Future 创建时立即执行
        networkService.getUser { result in
            switch result {
            case .success(let user): promise(.success(user))
            case .failure(let err):  promise(.failure(err))
            }
        }
    }
}

// 使用:像普通 Publisher 一样链式操作
fetchCurrentUser()
    .map { $0.displayName }
    .receive(on: DispatchQueue.main)
    .sink(
        receiveCompletion: { _ in },
        receiveValue: { name in self.nameLabel.text = name }
    )
    .store(in: &cancellables)

// ⚠️ Future 的"热启动"陷阱:
// let future = fetchCurrentUser()  ← 这行代码就已经触发了网络请求!
// 不管有没有订阅者,Future 内部的工作已经开始了
// 如果想要"懒启动",用 Deferred { Future { ... } } 包裹
swift — Subject(可命令式发值)
// Subject 是可以从外部主动发送值的 Publisher,有两种:

// ── PassthroughSubject:不保存值,只传递 ─────────────────
// 等同于 RxSwift 的 PublishSubject
class EventBus {
    static let shared = EventBus()
    let userLoggedIn = PassthroughSubject<User, Never>()
    let userLoggedOut = PassthroughSubject<Void, Never>()
}

// 发布事件
EventBus.shared.userLoggedIn.send(currentUser)

// 订阅事件
EventBus.shared.userLoggedIn
    .sink { user in updateProfile(user) }
    .store(in: &cancellables)
// 新订阅者不会收到之前的值

// ── CurrentValueSubject:保存最新值 ─────────────────────
// 等同于 RxSwift 的 BehaviorSubject
class SettingsModel {
    // 初始值为 false,新订阅者会立即收到当前值
    let isDarkMode = CurrentValueSubject<Bool, Never>(false)

    func toggleDarkMode() {
        isDarkMode.send(!isDarkMode.value)   // 发送新值
    }
}

let settings = SettingsModel()
settings.isDarkMode
    .sink { isDark in applyTheme(isDark) }
    .store(in: &cancellables)
// 立即执行一次(收到初始值 false),之后每次 toggle 都会触发

// ── @Published —— 属性包装器版的 CurrentValueSubject ────
class ViewModel: ObservableObject {
    @Published var count = 0

    // $count 是一个 Published<Int>.Publisher
    // 相当于:let count = CurrentValueSubject<Int, Never>(0)
    // 但更简洁,且与 SwiftUI 深度集成

    func increment() { count += 1 }   // 自动触发 $count 发布新值
}

let vm = ViewModel()
vm.$count
    .dropFirst()          // 跳过初始值(如果不需要)
    .sink { print("count 变为:\($0)") }
    .store(in: &cancellables)

③ 核心操作符大全

变换操作符(Transforming)

swift — map / tryMap / flatMap / compactMap
// map —— 一对一转换(同步)
["1", "2", "3"].publisher
    .map { Int($0)! }          // String → Int
    .sink { print($0) }        // 1  2  3

// tryMap —— 可能 throw 的 map(错误会终止流)
URLSession.shared.dataTaskPublisher(for: url)
    .tryMap { data, response -> Data in
        guard let http = response as? HTTPURLResponse,
              http.statusCode == 200 else {
            throw URLError(.badServerResponse)
        }
        return data
    }
    .decode(type: User.self, decoder: JSONDecoder())
    .sink(receiveCompletion: { _ in }, receiveValue: { user in ... })
    .store(in: &cancellables)

// flatMap —— 一对多,内部 Publisher 展平(最重要的操作符之一!)
// 经典用法:用一个 Publisher 的输出去创建另一个 Publisher
searchTextField.textPublisher            // 搜索框文本变化
    .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
    .removeDuplicates()
    .flatMap { query in                  // 每次文本变化,创建新的搜索请求
        searchAPI.search(query: query)
            .catch { _ in Just([]) }     // 单个请求出错不终止整个流
    }
    .assign(to: &vm.$results)

// compactMap —— map + 过滤 nil
["1", "two", "3", "four"].publisher
    .compactMap { Int($0) }   // 自动过滤转换失败(nil)的值
    .sink { print($0) }       // 1  3
swift — filter / debounce / removeDuplicates
// filter —— 过滤不满足条件的值
(1...10).publisher
    .filter { $0 % 2 == 0 }   // 只通过偶数
    .sink { print($0) }        // 2 4 6 8 10

// debounce —— 防抖:停止输入 n 毫秒后才发出(搜索框必备)
searchField.$text
    .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
    .sink { query in performSearch(query) }
    .store(in: &cancellables)

// throttle —— 节流:每隔 n 时间最多发出一次(与 debounce 区别!)
// debounce:最后一次输入后等 n 时间才发出
// throttle:每 n 时间最多发出一次(first/latest 可选)
scrollView.$contentOffset
    .throttle(for: .milliseconds(100), scheduler: RunLoop.main, latest: true)
    .sink { offset in updateScrollIndicator(offset) }
    .store(in: &cancellables)

// removeDuplicates —— 去重:只有值改变时才发出
vm.$isLoading
    .removeDuplicates()        // 避免 false→false 触发不必要更新
    .sink { isLoading in spinner.isHidden = !isLoading }
    .store(in: &cancellables)

组合操作符(Combining)

swift — combineLatest / zip / merge
// combineLatest —— 任一 Publisher 有新值,就用两者最新值组合发出
// 适合:表单验证(任一字段变化时重新验证)
let emailValid   = emailField.$text.map { isValidEmail($0) }
let passwordValid = passwordField.$text.map { $0.count >= 8 }

Publishers.CombineLatest(emailValid, passwordValid)
    .map { $0 && $1 }          // 两者都有效,才开放提交按钮
    .assign(to: \.isEnabled, on: submitButton)
    .store(in: &cancellables)

// zip —— 一一配对:等两边都有值才发出一对(有则消费,无则等待)
// 适合:确保两个请求都完成,且保持对应关系
let userPub  = fetchUser(id: 1)
let orderPub = fetchOrders(userId: 1)

Publishers.Zip(userPub, orderPub)
    .sink { user, orders in
        // user 和 orders 是同一次"配对"的结果
        showProfile(user, orders)
    }
    .store(in: &cancellables)

// merge —— 合并多个同类型 Publisher,先到先出
let tap1 = button1.tapPublisher
let tap2 = button2.tapPublisher
Publishers.Merge(tap1, tap2)
    .sink { handleTap() }
    .store(in: &cancellables)

// switchToLatest —— 取消旧 Publisher,切换到最新(防止竞态)
// 适合:搜索时,新请求发出前取消旧请求
let searchPublisher: AnyPublisher<[Item], Never> = searchText
    .map { query in searchAPI.search(query).catch { _ in Just([]) } }
    .switchToLatest()   // ⭐ 每次 searchText 有新值,取消上一个 search 请求
    .eraseToAnyPublisher()

错误处理操作符

swift
// catch —— 捕获错误,替换为另一个 Publisher(错误后继续)
fetchData()
    .catch { error -> Just<Data> in
        print("出错了:\(error),返回空数据")
        return Just(Data())
    }
    .sink { data in handle(data) }
    .store(in: &cancellables)

// retry —— 出错后重试 n 次
fetchData()
    .retry(3)    // 最多重试 3 次(共 4 次请求)
    .catch { _ in Just(Data()) }
    .sink { ... }
    .store(in: &cancellables)

// replaceError —— 出错时替换为默认值(快捷方式)
fetchUser()
    .replaceError(with: User.anonymous)   // 出错时用匿名用户替代
    .sink { user in showUser(user) }
    .store(in: &cancellables)

// mapError —— 转换错误类型
fetchData()
    .mapError { error in AppError.network(error) }   // Error → AppError
    .sink { ... }
    .store(in: &cancellables)

时序操作符

swift
// delay —— 延迟发出值
Just("延迟消息")
    .delay(for: .seconds(2), scheduler: DispatchQueue.main)
    .sink { print($0) }   // 2 秒后打印

// timeout —— 超时后发出错误或结束
fetchData()
    .timeout(.seconds(10), scheduler: DispatchQueue.main, customError: { TimeoutError() })
    .sink { ... }
    .store(in: &cancellables)

// collect —— 收集 n 个值再一起发出
(1...10).publisher
    .collect(3)    // 每 3 个打包一次
    .sink { print($0) }   // [1,2,3]  [4,5,6]  [7,8,9]  [10]

// buffer —— 缓冲区
// 当下游处理慢,buffer 防止值丢失(背压策略)
fastPublisher
    .buffer(size: 10, prefetch: .keepFull, whenFull: .dropOldest)
    .sink { handle($0) }
    .store(in: &cancellables)

④ Scheduler 与线程切换

Combine 的 Scheduler 协议抽象了"在哪个线程/队列执行"的概念,等同于 GCD 的 DispatchQueue,但更灵活(可以是 RunLoop、OperationQueue 等)。

swift
// subscribe(on:) —— 指定上游工作(subscription 和接收值)在哪个调度器执行
// receive(on:)   —— 指定下游操作符和订阅者在哪个调度器执行

// ── 典型网络请求模式(类比 GCD)────────────────────────────
URLSession.shared.dataTaskPublisher(for: url)
    // dataTaskPublisher 本身在内部队列工作,无需 subscribe(on:)
    .map { $0.data }
    .decode(type: User.self, decoder: JSONDecoder())
    // .decode 是 CPU 密集操作,放在后台队列
    .receive(on: DispatchQueue.main)    // ⭐ 切换到主线程更新 UI
    .sink(
        receiveCompletion: { _ in },
        receiveValue: { user in
            self.nameLabel.text = user.name   // ✅ 主线程,安全
        }
    )
    .store(in: &cancellables)

// ── subscribe(on:) 的使用场景 ─────────────────────────────
// 当 Publisher 的创建/工作本身在主线程(如 @Published 属性变化),
// 需要把耗时操作移到后台

vm.$data                               // @Published 在主线程发出
    .subscribe(on: DispatchQueue.global())   // 后续操作移到后台队列
    .map { heavyProcessing($0) }            // 耗时操作在后台
    .receive(on: DispatchQueue.main)        // 结果返回主线程
    .sink { processed in updateUI(processed) }
    .store(in: &cancellables)

// ── 常用 Scheduler ────────────────────────────────────────
// DispatchQueue.main           —— 主线程(最常用)
// DispatchQueue.global()       —— 系统全局并发队列
// DispatchQueue(label:)        —— 自定义串行队列
// RunLoop.main                 —— 主 RunLoop(与 DispatchQueue.main 类似,但在 RunLoop 模式上有细微差异)
// OperationQueue.main          —— 主队列
// ImmediateScheduler           —— 同步立即执行(测试用)

⑤ 内存管理与 AnyCancellable

swift
// AnyCancellable 是订阅的"句柄"
// 它释放时(deinit)自动取消订阅
// 这解决了 RxSwift 中容易忘记 dispose 的问题

class MyViewModel: ObservableObject {
    // 方式1:Set 集合存储(最常见)
    private var cancellables = Set<AnyCancellable>()

    init() {
        // .store(in:) 把 AnyCancellable 放入 Set
        apiService.fetchData()
            .sink { _ in } receiveValue: { data in self.process(data) }
            .store(in: &cancellables)   // 随 ViewModel 生命周期管理

        // 多个订阅都放进同一个 Set
        timer.publisher
            .sink { _ in self.tick() }
            .store(in: &cancellables)
    }
    // deinit 时 cancellables Set 被释放,所有订阅自动取消
}

// 方式2:单独保存引用(需要单独控制取消时机)
class SearchController {
    private var searchCancellable: AnyCancellable?

    func startSearch(query: String) {
        searchCancellable?.cancel()   // 取消上一次搜索
        searchCancellable = api.search(query: query)
            .sink(receiveCompletion: { _ in },
                  receiveValue: { self.results = $0 })
    }
}

// 方式3:assign(to:) —— 直接绑定到 @Published,自动管理生命周期
class StockViewModel: ObservableObject {
    @Published var price: Double = 0

    init() {
        priceStream   // AsyncStream → Publisher
            .assign(to: &$price)   // ⭐ 不返回 AnyCancellable,自动绑定生命周期
        // 注意:assign(to: &$price) 只能用于 @Published 属性
        // 它解决了 assign(to:on:) 造成的 self 强引用循环问题
    }
}

// ── 引用循环陷阱与解决 ────────────────────────────────────
class BadViewController: UIViewController {
    var cancellables = Set<AnyCancellable>()

    func setup() {
        // ❌ 强引用循环:cancellable → ViewController(self)→ cancellable
        somePublisher
            .sink { value in
                self.label.text = value    // 强引用 self
            }
            .store(in: &cancellables)
    }
}

class GoodViewController: UIViewController {
    var cancellables = Set<AnyCancellable>()

    func setup() {
        // ✅ 用 [weak self] 打破循环
        somePublisher
            .sink { [weak self] value in
                self?.label.text = value
            }
            .store(in: &cancellables)
    }
}

⑥ Combine + async/await 互操作

Combine 和 async/await 不是竞争关系,而是互补:Combine 擅长多值流、操作符链;async/await 擅长单次异步操作的线性表达。掌握两者互操作是现代 iOS 开发的必备技能。

swift — Publisher → async/await
// ── .values:把 Publisher 转成 AsyncSequence ──────────────
// iOS 15+ 所有 Publisher 都有 .values 属性

let publisher = URLSession.shared.dataTaskPublisher(for: url)
    .map { $0.data }

// 在 async 函数里消费 Publisher
func processData() async throws {
    for try await data in publisher.values {
        await handle(data)   // 每个值都等待处理完再继续
    }
}

// ── .value:等待单个值(类似 Combine 的 first())───────────
// 等待 Publisher 发出第一个值(或错误)
func fetchOnce() async throws -> User {
    return try await apiPublisher.values.first(where: { _ in true })!
    // 更简洁的写法(需要 Combine Publisher 是有限的):
}

// 用 AsyncThrowingStream 桥接持续的 Publisher
func bridgePublisher<P: Publisher>(_ publisher: P) -> AsyncThrowingStream<P.Output, Error> {
    AsyncThrowingStream { continuation in
        let cancellable = publisher.sink(
            receiveCompletion: { completion in
                switch completion {
                case .finished:     continuation.finish()
                case .failure(let e): continuation.finish(throwing: e)
                }
            },
            receiveValue: { value in
                continuation.yield(value)
            }
        )
        continuation.onTermination = { _ in cancellable.cancel() }
    }
}

// ── async/await → Publisher ────────────────────────────────
// 用 Future 包装 async 函数(最简单方式)
func asyncFunctionAsPublisher() -> AnyPublisher<User, Error> {
    Future { promise in
        Task {
            do {
                let user = try await fetchUser()
                promise(.success(user))
            } catch {
                promise(.failure(error))
            }
        }
    }
    .eraseToAnyPublisher()
}

// 更优雅:Deferred + Future(懒启动)
func lazyAsyncPublisher<T>(operation: @escaping () async throws -> T) -> AnyPublisher<T, Error> {
    Deferred {
        Future { promise in
            Task { do { promise(.success(try await operation())) }
                   catch { promise(.failure(error)) } }
        }
    }
    .eraseToAnyPublisher()
}
// 使用:
let userPublisher = lazyAsyncPublisher { try await fetchUser() }
userPublisher.sink { ... }.store(in: &cancellables)

Combine vs async/await 选择指南

维度Combineasync/await
适合场景持续数据流、多事件、UI 绑定单次异步操作
表达方式链式操作符,声明式线性代码,命令式
学习曲线较陡(操作符多)平缓(像同步代码)
SwiftUI 集成@Published 原生支持配合 .task { }
可取消AnyCancellable / .cancel()Task.cancel()
错误处理操作符链(catch/retry)try/catch
Apple 态度维护但不主推新特性主推方向(Swift 5.5+)
典型用途搜索防抖、表单验证、事件总线网络请求、文件 IO、数据库

实践建议:两者互补,不必非此即彼。SwiftUI 状态绑定用 Combine(@Published),网络/IO 用 async/await,用 Future { Task { ... } } 在两者之间桥接。

⑦ MVVM + Combine 完整实战

swift — 完整搜索 ViewModel
import Combine
import Foundation

// ── Model ─────────────────────────────────────────────────
struct Article: Identifiable, Decodable {
    let id: Int
    let title: String
    let summary: String
    let publishedAt: Date
}

// ── Service(网络层)──────────────────────────────────────
protocol ArticleServiceProtocol {
    func search(query: String) -> AnyPublisher<[Article], Error>
}

class ArticleService: ArticleServiceProtocol {
    private let decoder: JSONDecoder = {
        let d = JSONDecoder()
        d.keyDecodingStrategy = .convertFromSnakeCase
        d.dateDecodingStrategy = .iso8601
        return d
    }()

    func search(query: String) -> AnyPublisher<[Article], Error> {
        guard !query.isEmpty else {
            return Just([]).setFailureType(to: Error.self).eraseToAnyPublisher()
        }
        let url = URL(string: "https://api.example.com/search?q=\(query.urlEncoded)")!
        return URLSession.shared.dataTaskPublisher(for: url)
            .tryMap { data, response in
                guard (response as? HTTPURLResponse)?.statusCode == 200
                else { throw URLError(.badServerResponse) }
                return data
            }
            .decode(type: [Article].self, decoder: decoder)
            .eraseToAnyPublisher()
    }
}

// ── ViewModel ─────────────────────────────────────────────
@MainActor
final class SearchViewModel: ObservableObject {
    // 输入(Input)—— 外部写入
    @Published var searchQuery = ""

    // 输出(Output)—— 外部只读
    @Published private(set) var articles: [Article] = []
    @Published private(set) var isLoading = false
    @Published private(set) var errorMessage: String?
    @Published private(set) var hasSearched = false

    private let service: ArticleServiceProtocol
    private var cancellables = Set<AnyCancellable>()

    init(service: ArticleServiceProtocol = ArticleService()) {
        self.service = service
        setupBindings()
    }

    private func setupBindings() {
        $searchQuery
            // 防抖:停止输入 500ms 后触发搜索
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            // 去重:避免相同 query 重复搜索
            .removeDuplicates()
            // 更新 loading 状态(在 flatMap 之前)
            .handleEvents(receiveOutput: { [weak self] query in
                self?.isLoading = !query.isEmpty
                self?.errorMessage = nil
                if !query.isEmpty { self?.hasSearched = true }
            })
            // switchToLatest 自动取消过时的搜索请求
            .flatMap { [weak self] query -> AnyPublisher<[Article], Never> in
                guard let self = self, !query.isEmpty else {
                    return Just([]).eraseToAnyPublisher()
                }
                return self.service.search(query: query)
                    .catch { [weak self] error -> Just<[Article]> in
                        self?.errorMessage = error.localizedDescription
                        return Just([])
                    }
                    .eraseToAnyPublisher()
            }
            // 更新 articles 并关闭 loading
            .receive(on: RunLoop.main)
            .sink { [weak self] results in
                self?.articles = results
                self?.isLoading = false
            }
            .store(in: &cancellables)
    }

    func clearSearch() {
        searchQuery = ""
        articles = []
        hasSearched = false
        errorMessage = nil
    }
}

// ── UIKit View(UITableViewController)────────────────────
class ArticleSearchViewController: UITableViewController {
    private var viewModel = SearchViewModel()
    private var cancellables = Set<AnyCancellable>()

    private lazy var searchBar: UISearchBar = {
        let bar = UISearchBar()
        bar.placeholder = "搜索文章…"
        return bar
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(ArticleCell.self, forCellReuseIdentifier: "cell")
        navigationItem.titleView = searchBar

        // 绑定 UISearchBar 文本 → ViewModel.searchQuery
        searchBar.searchTextField.textPublisher
            .assign(to: &viewModel.$searchQuery)   // 注意:这样会有强引用,实际应用考虑 [weak self]

        // 绑定 ViewModel 输出 → UI
        viewModel.$articles
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in self?.tableView.reloadData() }
            .store(in: &cancellables)

        viewModel.$isLoading
            .receive(on: DispatchQueue.main)
            .sink { [weak self] isLoading in
                isLoading
                    ? self?.showSpinner()
                    : self?.hideSpinner()
            }
            .store(in: &cancellables)

        viewModel.$errorMessage
            .compactMap { $0 }
            .receive(on: DispatchQueue.main)
            .sink { [weak self] message in self?.showError(message) }
            .store(in: &cancellables)
    }
}

⑧ Part 2 总结与速查

核心概念

Combine 是 Apple 在 2019 年推出的响应式编程框架,本质是数据流 + 异步事件处理。

Publisher(发布者)→ Operator(操作符)→ Subscriber(订阅者)

核心记忆口诀

Publisher 发 · Operator 变 · Subscriber 收 · Cancellable 存

常用类型速查

类型用途记忆点
Just单值立即完成测试/默认值占位
Future单次异步结果创建时立即执行(eager),需懒启动用 Deferred 包裹
PassthroughSubject命令式发值,无缓存桥接旧回调/通知进 Combine
CurrentValueSubject命令式发值,有当前值替代简单 @Published 场景
@Published属性自动成为 PublisherObservableObject 的核心,$属性名 访问

操作符选择速查

需求操作符
搜索框防抖debounce(for:scheduler:)
避免重复触发removeDuplicates()
取消旧请求(竞态)flatMap + switchToLatest
两个字段都满足才触发combineLatest
两个请求配对使用结果zip
出错不终止整个流flatMap { ... .catch { Just([]) } }
切回主线程更新 UIreceive(on: DispatchQueue.main)
过滤 nil / 类型转换compactMap
Part 3

SwiftUI 声明式 UI

从 UIKit 转向 SwiftUI 最大的思维转变:UI 是状态的函数(UI = f(state))。你不再"操作"视图,而是"描述"视图应该长什么样,SwiftUI 负责高效地把描述渲染成像素。本部分系统讲解状态管理、布局、导航、动画,以及与 UIKit 的集成策略。

SwiftUI iOS 14~17+ Observation Framework

① 声明式 vs 命令式编程范式

核心思维转变

swift — UIKit 命令式 vs SwiftUI 声明式
// ── UIKit(命令式):你告诉系统"怎么做" ─────────────────

class LoginViewController: UIViewController {
    let usernameField = UITextField()
    let passwordField = UITextField()
    let loginButton   = UIButton()
    let spinner       = UIActivityIndicatorView(style: .medium)

    var isLoading = false {
        didSet {
            // 每次 isLoading 变化,手动更新每个相关控件
            DispatchQueue.main.async {
                self.spinner.isHidden    = !self.isLoading
                self.loginButton.isEnabled = !self.isLoading
                self.loginButton.alpha    = self.isLoading ? 0.5 : 1.0
            }
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // 手动布局、手动设置约束、手动处理每种状态
        setupConstraints()
        loginButton.addTarget(self, action: #selector(loginTapped), for: .touchUpInside)
    }
    // ... 还有大量 layout、configure、update 代码
}

// ── SwiftUI(声明式):你告诉系统"是什么" ───────────────

struct LoginView: View {
    @State private var username  = ""
    @State private var password  = ""
    @State private var isLoading = false

    // UI = f(state):SwiftUI 根据当前状态自动渲染正确的 UI
    var body: some View {
        VStack(spacing: 20) {
            TextField("用户名", text: $username)
                .textFieldStyle(.roundedBorder)

            SecureField("密码", text: $password)
                .textFieldStyle(.roundedBorder)

            // 根据 isLoading 状态,UI 自动正确
            if isLoading {
                ProgressView()             // 加载中显示转圈
            } else {
                Button("登录") { login() }
                    .buttonStyle(.borderedProminent)
                    .disabled(username.isEmpty || password.isEmpty)
            }
        }
        .padding()
    }

    private func login() {
        isLoading = true
        // 只需改变状态,UI 自动更新
        // 不需要手动操作任何控件
    }
}
// SwiftUI 的 View 是值类型(struct),每次状态变化都重新计算 body
// SwiftUI 内部做 diff,只更新真正变化的部分(不是全量重建)
✅ SwiftUI 的核心规则
1. View 是值类型(struct):不要尝试持有 View 引用或修改它
2. body 必须是纯函数:给定相同的状态,body 应该始终生成相同的 UI
3. 不要在 body 外修改 @State:所有状态变化都应该是用户操作或事件的响应
4. View 被频繁重新计算:body 的计算要轻量,耗时操作放在 .task 或 ViewModel 里

② 状态管理全解:@State · @Binding · @StateObject · @ObservedObject · @EnvironmentObject · @Observable

决策树:选哪个?

属性包装器数据来源数据类型典型场景
@StateView 本身拥有简单值类型展开/收起、输入文本、选中状态
@Binding父 View 传入任意子组件双向修改父 View 的状态
@StateObjectView 本身创建ObservableObject该 View 拥有并管理 ViewModel 生命周期
@ObservedObject外部传入ObservableObject接收并观察外部 ViewModel(不拥有)
@EnvironmentObject环境(祖先注入)ObservableObject全局数据(登录状态、主题、路由)
@Environment系统/自定义环境值值类型colorScheme、locale、自定义配置
@Observable任意class(iOS 17+)新一代替代 ObservableObject,更高效
swift — @State 与 @Binding
// @State:View 局部私有状态
// - 存储在 SwiftUI 的持久化存储中,View 重新创建时保持不变
// - 只能在 View 内部修改(用 $ 前缀创建 Binding 传给子 View)
// - 适合简单的 UI 状态(不需要持久化,不需要跨 View 共享)

struct CounterView: View {
    @State private var count = 0            // 私有,只有这个 View 修改
    @State private var showDetail = false

    var body: some View {
        VStack {
            Text("计数:\(count)")
                .font(.largeTitle)
            HStack {
                Button("−") { count -= 1 }
                Button("+") { count += 1 }
            }
            Button("详情") { showDetail.toggle() }
                .sheet(isPresented: $showDetail) {   // $ 创建 Binding
                    DetailView(count: $count)         // 传 Binding 给子 View
                }
        }
    }
}

// @Binding:从父 View 接收"双向引用"
// - 不拥有数据,只是引用
// - 修改 @Binding 会同步修改父 View 的源数据
struct DetailView: View {
    @Binding var count: Int     // 不用 @State!这个值由父 View 拥有

    var body: some View {
        VStack {
            Text("父 View 的计数:\(count)")
            Button("在子 View 里修改") { count = 100 }   // 修改的是父 View 的 @State
        }
    }
}

// ── 自定义 Binding(无 @State 的 Binding)────────────────
// 当源数据不是简单 @State,而是来自其他地方时
struct CustomBindingExample: View {
    @ObservedObject var vm: ViewModel

    var body: some View {
        // 手动创建 Binding,指定 get/set
        let usernameBinding = Binding(
            get: { vm.username },
            set: { vm.updateUsername($0) }
        )
        TextField("用户名", text: usernameBinding)
    }
}
swift — @StateObject vs @ObservedObject
// ⚠️ 最常见的错误:混淆 @StateObject 和 @ObservedObject

// @StateObject:View 负责创建 ViewModel,拥有其生命周期
// 当 View 被重新创建时,@StateObject 不会重新初始化(SwiftUI 缓存它)
struct ProfileView: View {
    @StateObject private var vm = ProfileViewModel()   // ✅ View 拥有 ViewModel
    // ProfileView 被销毁时,vm 也被销毁

    var body: some View {
        VStack {
            if vm.isLoading { ProgressView() }
            Text(vm.username)
        }
        .onAppear { vm.loadProfile() }
    }
}

// @ObservedObject:ViewModel 由外部(父 View/DI容器)创建,View 只观察
// ⚠️ 如果父 View 重新渲染,@ObservedObject 的对象可能被重新创建!
struct ProfileDetailView: View {
    @ObservedObject var vm: ProfileViewModel   // 外部传入,不负责生命周期
    // 正确:vm 由父 View 的 @StateObject 创建,ProfileDetailView 只观察
    var body: some View { ... }
}

// ── 错误示范 ──────────────────────────────────────────────
struct WrongView: View {
    @ObservedObject private var vm = ProfileViewModel()   // ❌ 用 @ObservedObject 创建!
    // 每次父 View 重新渲染导致 WrongView 重新初始化时,vm 会被重建,数据丢失!
    // 必须用 @StateObject 才能保持稳定
}
swift — @EnvironmentObject
// @EnvironmentObject:通过 View 树注入共享对象
// 适合:认证状态、用户设置、路由器等全局数据
// 优势:不需要逐层传递,深层子 View 直接访问

// 1. 定义共享数据
class AppState: ObservableObject {
    @Published var currentUser: User?
    @Published var isAuthenticated = false
    @Published var selectedTab = 0
}

// 2. 在根 View 注入
@main
struct MyApp: App {
    @StateObject private var appState = AppState()   // 根 View 拥有

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(appState)          // 注入到整个 View 树
        }
    }
}

// 3. 任意深度的子 View 直接使用
struct UserProfileView: View {
    @EnvironmentObject var appState: AppState   // 无需层层传递

    var body: some View {
        if let user = appState.currentUser {
            Text("欢迎,\(user.name)")
            Button("登出") {
                appState.isAuthenticated = false
                appState.currentUser = nil
            }
        }
    }
}
swift — @Observable(iOS 17+,新一代方案)
// @Observable 宏(Swift 5.9 / iOS 17+)
// 优势:
// 1. 比 ObservableObject + @Published 粒度更细(属性级别追踪)
// 2. 不需要 @Published,所有存储属性自动可观察
// 3. 视图只在实际用到的属性变化时重绘(更少不必要的重绘)
// 4. 可以用 @State 持有(不再需要 @StateObject/@ObservedObject 区分)

import Observation

@Observable
class CounterModel {
    var count = 0           // 自动可观察(无需 @Published)
    var name  = "计数器"    // 同上

    // 不可观察的属性(不影响视图的内部状态)
    @ObservationIgnored
    private var internalCache: [Int: String] = [:]

    func increment() { count += 1 }
}

// 在 View 里使用(不需要 @StateObject)
struct NewCounterView: View {
    @State private var model = CounterModel()   // ✅ 用 @State 持有 @Observable 类

    var body: some View {
        VStack {
            Text(model.name)
            Text("计数:\(model.count)")
            Button("加") { model.increment() }
        }
    }
}

// 作为 @Environment 值传递(替代 @EnvironmentObject)
struct ParentView: View {
    @State private var appModel = AppModel()
    var body: some View {
        ChildView()
            .environment(appModel)   // 注意:没有 .environmentObject,直接 .environment
    }
}

struct ChildView: View {
    @Environment(AppModel.self) private var appModel   // 注意:没有 .self 之前的类型声明方式不同
    var body: some View { Text(appModel.title) }
}

③ 布局系统:Stack · Grid · GeometryReader

Auto Layout vs SwiftUI 布局

swift — HStack / VStack / ZStack
// VStack:垂直排列(类比 UIStackView axis = .vertical)
VStack(alignment: .leading, spacing: 12) {
    Text("标题").font(.headline)
    Text("副标题").font(.subheadline).foregroundStyle(.secondary)
    Divider()
    Text("正文内容…").lineLimit(3)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 4)

// HStack:水平排列
HStack(alignment: .center) {
    Image(systemName: "person.circle")
        .font(.system(size: 40))
    VStack(alignment: .leading, spacing: 4) {
        Text("张三").bold()
        Text("iOS 开发工程师").foregroundStyle(.secondary)
    }
    Spacer()        // 弹性空间,把右边内容推到右侧(类比 flexbox flex: 1)
    Image(systemName: "chevron.right").foregroundStyle(.secondary)
}

// ZStack:层叠(类比 z-index)
ZStack(alignment: .bottomTrailing) {
    AsyncImage(url: imageURL) { image in
        image.resizable().scaledToFill()
    } placeholder: {
        Color.gray.opacity(0.3)
    }
    .frame(width: 120, height: 120)
    .clipped()

    // 悬浮在图片右下角的小徽章
    Image(systemName: "checkmark.circle.fill")
        .foregroundStyle(.green)
        .background(Color.white, in: Circle())
        .offset(x: 4, y: 4)
}

// ── 对齐指南(Alignment Guide)──────────────────────────
// 自定义对齐基线
extension HorizontalAlignment {
    struct CustomCenter: AlignmentID {
        static func defaultValue(in context: ViewDimensions) -> CGFloat {
            context[HorizontalAlignment.center]
        }
    }
    static let customCenter = HorizontalAlignment(CustomCenter.self)
}
swift — LazyVGrid / LazyHGrid
// LazyVGrid:懒加载垂直网格(类比 UICollectionViewCompositionalLayout)

struct PhotoGridView: View {
    let photos: [Photo]

    // 固定列数
    let fixedColumns = [GridItem(.fixed(100)), GridItem(.fixed(100)), GridItem(.fixed(100))]

    // 自适应列数(根据屏幕宽度自动计算列数)
    let adaptiveColumns = [GridItem(.adaptive(minimum: 100, maximum: 200))]

    // 灵活比例列
    let flexColumns = [GridItem(.flexible()), GridItem(.flexible(2))]  // 1:2 比例

    var body: some View {
        ScrollView {
            LazyVGrid(columns: adaptiveColumns, spacing: 4) {
                ForEach(photos) { photo in
                    PhotoThumbnail(photo: photo)
                        .aspectRatio(1, contentMode: .fit)
                        .clipped()
                }
            }
            .padding(4)
        }
    }
}

// ── ViewThatFits(iOS 16+)自适应布局 ────────────────────
// 尝试第一个 View,如果放不下就用下一个
ViewThatFits {
    HStack { /* 宽屏:横向排列 */ longContent() }
    VStack { /* 窄屏:纵向排列 */ longContent() }
}
swift — GeometryReader
// GeometryReader:读取父 View 提供的尺寸(类比 UIView.bounds)
// ⚠️ GeometryReader 会贪婪地占满所有可用空间,谨慎使用

struct AdaptiveCard: View {
    var body: some View {
        GeometryReader { geometry in
            let width  = geometry.size.width
            let height = geometry.size.height
            let isLandscape = width > height

            if isLandscape {
                HStack { content() }    // 横屏:横向排列
            } else {
                VStack { content() }    // 竖屏:纵向排列
            }
        }
    }
}

// 更好的替代方案(iOS 17+):.containerRelativeFrame
// 避免 GeometryReader 的贪婪扩展行为
Text("自适应宽度")
    .frame(maxWidth: .infinity)
    .containerRelativeFrame(.horizontal) { width, _ in
        width * 0.8    // 父容器宽度的 80%
    }

// 读取自身尺寸(onGeometryChange,iOS 17+)
struct SelfSizingView: View {
    @State private var size = CGSize.zero

    var body: some View {
        Text("内容")
            .onGeometryChange(for: CGSize.self) { proxy in
                proxy.size
            } action: { newSize in
                size = newSize   // 尺寸变化时回调
            }
    }
}

④ NavigationStack 与程序化导航 iOS 16+

旧的 NavigationView 已弃用。NavigationStack(iOS 16+)提供了真正的程序化导航能力——你可以用数据驱动导航栈,而不是手动 push/pop。

swift — NavigationStack 基础
// ── 旧方式(弃用)────────────────────────────────────────
NavigationView {                         // ❌ 已弃用
    List(items) { item in
        NavigationLink(item.title, destination: DetailView(item: item))
    }
}

// ── 新方式:NavigationStack ───────────────────────────────
struct AppRootView: View {
    // 导航路径:数组代表导航栈(空数组=根页面)
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            ItemListView(path: $path)
                .navigationDestination(for: Item.self) { item in
                    ItemDetailView(item: item)
                }
                .navigationDestination(for: User.self) { user in
                    UserProfileView(user: user)
                }
        }
    }
}

struct ItemListView: View {
    @Binding var path: NavigationPath
    let items: [Item] = loadItems()

    var body: some View {
        List(items) { item in
            // 方式1:声明式 NavigationLink(推荐,自动管理 path)
            NavigationLink(value: item) {      // value 必须是 Hashable
                ItemRow(item: item)
            }
        }
        .toolbar {
            // 方式2:程序化导航(替代 UINavigationController.pushViewController)
            Button("直接跳到详情") {
                path.append(items[0])          // 相当于 push
            }
            Button("回到根页面") {
                path.removeLast(path.count)    // 清空栈,相当于 popToRoot
            }
        }
    }
}

// ── Deep Link 处理 ────────────────────────────────────────
// 通过 URL 直接跳到指定页面
.onOpenURL { url in
    if let itemID = url.itemID {
        path.append(Item(id: itemID))   // 直接 push 到对应页面
    }
}
swift — Sheet / FullScreenCover / Popover
struct ContentView: View {
    @State private var showSheet        = false
    @State private var showFullScreen   = false
    @State private var selectedItem: Item? = nil   // 可选值驱动的 sheet(iOS 16+)
    @State private var sheetDetent = PresentationDetent.medium   // 半屏/全屏

    var body: some View {
        Button("显示半屏 Sheet") { showSheet = true }
            .sheet(isPresented: $showSheet) {
                SheetContent()
                    .presentationDetents([.medium, .large])   // 支持多个停靠位置
                    .presentationDragIndicator(.visible)       // 显示拖拽条
            }

        Button("显示全屏") { showFullScreen = true }
            .fullScreenCover(isPresented: $showFullScreen) {
                FullScreenContent()
            }

        // 用可选值驱动 sheet(更推荐的模式)
        Button("显示选中项详情") { selectedItem = items.first }
            .sheet(item: $selectedItem) { item in
                // item 非 nil 时显示,nil 时自动消失
                ItemDetailSheet(item: item)
            }
    }
}

// 自定义 Detent(iOS 16+)
extension PresentationDetent {
    static let threeQuarter = Self.fraction(0.75)   // 3/4 屏
    static let fixedHeight  = Self.height(300)       // 固定高度
}

⑤ List 与数据展示

swift — List 全特性
struct TaskListView: View {
    @State private var tasks: [Task] = loadTasks()
    @State private var editMode: EditMode = .inactive
    @State private var selection = Set<Task.ID>()

    var body: some View {
        List(selection: $selection) {
            // ── Section 分组 ───────────────────────────────
            Section("今日任务") {
                ForEach(tasks.filter { $0.isToday }) { task in
                    TaskRow(task: task)
                        .swipeActions(edge: .trailing) {
                            Button("删除", role: .destructive) {
                                delete(task)
                            }
                            Button("归档") {
                                archive(task)
                            }
                            .tint(.blue)
                        }
                        .swipeActions(edge: .leading) {
                            Button("完成") { complete(task) }
                                .tint(.green)
                        }
                }
                .onDelete { indexSet in tasks.remove(atOffsets: indexSet) }
                .onMove  { from, to in tasks.move(fromOffsets: from, toOffset: to) }
            }

            Section("已完成") {
                ForEach(tasks.filter { $0.isCompleted }) { task in
                    TaskRow(task: task)
                }
            }
        }
        .listStyle(.insetGrouped)
        .environment(\.editMode, $editMode)
        .toolbar {
            EditButton()
            Button("全选") { selection = Set(tasks.map(\.id)) }
        }
        // 搜索(iOS 15+)
        .searchable(text: $searchText, prompt: "搜索任务")
        .refreshable {
            await refreshTasks()    // 下拉刷新(自动显示刷新指示器)
        }
    }
}

// ── 高性能列表:ScrollView + LazyVStack ──────────────────
// List 自动懒加载,但样式固定
// ScrollView + LazyVStack 完全自定义样式
ScrollView {
    LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) {
        ForEach(sections) { section in
            Section {
                ForEach(section.items) { item in
                    ItemCard(item: item)
                }
            } header: {
                SectionHeader(title: section.title)
                    .background(.bar)   // 毛玻璃效果的 sticky header
            }
        }
    }
}

⑥ 动画与转场

隐式动画 vs 显式动画

swift
// ── 隐式动画(.animation)——————————————————————————
// 该 View 上的所有可动画属性变化都会应用此动画
struct HeartButton: View {
    @State private var isLiked = false

    var body: some View {
        Image(systemName: isLiked ? "heart.fill" : "heart")
            .foregroundStyle(isLiked ? .red : .gray)
            .scaleEffect(isLiked ? 1.3 : 1.0)
            .animation(.spring(response: 0.3, dampingFraction: 0.5), value: isLiked)
            // value: 只在 isLiked 变化时才触发动画(推荐!避免意外动画)
            .onTapGesture { isLiked.toggle() }
    }
}

// ── 显式动画(withAnimation)————————————————————————
// 精确控制哪个状态变化触发动画
Button("展开") {
    withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
        isExpanded.toggle()   // 只有这个变化有动画
    }
    noAnimationChange = true   // 这个变化不受 withAnimation 影响(因为没有 .animation 修饰符)
}

// ── 动画类型 ──────────────────────────────────────────────
.animation(.linear(duration: 0.3), value: x)          // 线性
.animation(.easeInOut(duration: 0.5), value: x)       // 缓入缓出
.animation(.spring(), value: x)                        // 弹簧(iOS 17 新语法)
.animation(.spring(response: 0.3, dampingFraction: 0.6), value: x)  // 自定义弹簧
.animation(.bouncy, value: x)                          // iOS 17+ 预设
.animation(.smooth, value: x)                          // iOS 17+ 预设
.animation(.snappy, value: x)                          // iOS 17+ 预设
.animation(.default.repeatCount(3, autoreverses: true), value: x)  // 重复动画

// ── Keyframe 动画(iOS 17+)─────────────────────────────
Image("rocket")
    .keyframeAnimator(initialValue: AnimationValues()) { content, value in
        content
            .scaleEffect(value.scale)
            .rotationEffect(.degrees(value.rotation))
            .offset(y: value.offsetY)
    } keyframes: { _ in
        KeyframeTrack(\.offsetY) {
            LinearKeyframe(-10, duration: 0.1)
            SpringKeyframe(0, duration: 0.3, spring: .bouncy)
        }
        KeyframeTrack(\.scale) {
            CubicKeyframe(1.2, duration: 0.1)
            CubicKeyframe(1.0, duration: 0.2)
        }
    }
swift — 转场(Transition)
// .transition 控制 View 出现/消失时的动画
struct NotificationBanner: View {
    @State private var show = false

    var body: some View {
        VStack {
            if show {
                Text("操作成功!")
                    .padding()
                    .background(.green)
                    .cornerRadius(8)
                    .transition(.move(edge: .top).combined(with: .opacity))
                    // combined:组合多个转场效果
            }
            Button("显示通知") {
                withAnimation(.spring(response: 0.3)) {
                    show = true
                }
                // 2 秒后自动消失
                Task {
                    try await Task.sleep(for: .seconds(2))
                    withAnimation { show = false }
                }
            }
        }
    }
}

// ── 自定义转场 ────────────────────────────────────────────
extension AnyTransition {
    static var flyIn: AnyTransition {
        .modifier(
            active:   FlyInModifier(offset: -200, opacity: 0),
            identity: FlyInModifier(offset: 0,    opacity: 1)
        )
    }
}

struct FlyInModifier: ViewModifier {
    let offset: CGFloat
    let opacity: Double
    func body(content: Content) -> some View {
        content.offset(x: offset).opacity(opacity)
    }
}

// ── matchedGeometryEffect:英雄动画(Hero Animation)────
// 让相同元素在两个 View 之间平滑过渡
@Namespace private var heroNamespace

struct HeroAnimationDemo: View {
    @State private var isExpanded = false

    var body: some View {
        if isExpanded {
            // 展开的大卡片
            RoundedRectangle(cornerRadius: 20)
                .fill(.blue)
                .frame(maxWidth: .infinity, maxHeight: 300)
                .matchedGeometryEffect(id: "card", in: heroNamespace)
                .onTapGesture {
                    withAnimation(.spring()) { isExpanded = false }
                }
        } else {
            // 折叠的小卡片
            RoundedRectangle(cornerRadius: 10)
                .fill(.blue)
                .frame(width: 100, height: 60)
                .matchedGeometryEffect(id: "card", in: heroNamespace)
                .onTapGesture {
                    withAnimation(.spring()) { isExpanded = true }
                }
        }
    }
}

⑦ .task 修饰符与异步数据加载 iOS 15+

.task 是 SwiftUI 官方提供的异步任务入口,在 View 出现时启动,View 消失时自动取消。完全替代 .onAppear { Task { } } 的写法,且更安全。

swift — .task 修饰符
// ── .onAppear + Task(旧,不推荐)────────────────────────
struct OldWay: View {
    @State private var user: User?

    var body: some View {
        Text(user?.name ?? "加载中…")
            .onAppear {
                Task {   // ❌ View 消失时任务不会自动取消!
                    user = try? await fetchUser()
                }
            }
    }
}

// ── .task(推荐)——————————————————————————————————————
struct NewWay: View {
    @State private var user: User?
    @State private var isLoading = true
    @State private var error: Error?

    var body: some View {
        Group {
            if isLoading {
                ProgressView("加载用户信息…")
            } else if let error {
                ErrorView(error: error) { await loadUser() }   // 重试按钮
            } else if let user {
                UserProfileCard(user: user)
            }
        }
        .task {   // ✅ View 出现时执行,View 消失时自动取消
            await loadUser()
        }
    }

    private func loadUser() async {
        isLoading = true
        error = nil
        do {
            user = try await userService.fetchCurrentUser()
        } catch {
            if !(error is CancellationError) {   // 被取消不算错误
                self.error = error
            }
        }
        isLoading = false
    }
}

// ── .task(id:):当依赖变化时重启任务 ─────────────────────
struct UserPostsView: View {
    let userID: String            // 可能从外部切换用户
    @State private var posts: [Post] = []

    var body: some View {
        List(posts) { post in PostRow(post: post) }
        .task(id: userID) {       // ⭐ userID 变化时:取消旧任务,启动新任务
            do {
                posts = try await api.fetchPosts(userID: userID)
            } catch { }
        }
    }
}

// ── 实战:分页加载 ────────────────────────────────────────
struct PaginatedListView: View {
    @State private var items: [Item] = []
    @State private var currentPage = 1
    @State private var isLoadingMore = false
    @State private var hasMore = true

    var body: some View {
        List {
            ForEach(items) { item in ItemRow(item: item) }

            // 当滚动到列表末尾,触发加载更多
            if hasMore {
                ProgressView()
                    .onAppear {
                        guard !isLoadingMore else { return }
                        Task { await loadMore() }
                    }
            }
        }
        .task { await loadInitial() }
    }

    func loadInitial() async {
        let result = try? await api.fetchItems(page: 1)
        items = result?.items ?? []
        hasMore = result?.hasNextPage ?? false
        currentPage = 1
    }

    func loadMore() async {
        guard hasMore, !isLoadingMore else { return }
        isLoadingMore = true
        defer { isLoadingMore = false }
        let nextPage = currentPage + 1
        if let result = try? await api.fetchItems(page: nextPage) {
            items.append(contentsOf: result.items)
            hasMore = result.hasNextPage
            currentPage = nextPage
        }
    }
}

⑧ UIViewRepresentable:UIKit 与 SwiftUI 互操作

SwiftUI 无法完全覆盖所有 UIKit 功能,UIViewRepresentable 是桥梁协议,让你把任何 UIView 包装成 SwiftUI View

swift — UIViewRepresentable 完整模板
// ── 包装 UITextView(富文本编辑器)──────────────────────

struct RichTextEditor: UIViewRepresentable {
    @Binding var text: NSAttributedString
    var placeholder: String = ""
    var onTextChange: ((NSAttributedString) -> Void)?

    // 1. 创建 UIView 实例(只调用一次)
    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.font = .preferredFont(forTextStyle: .body)
        textView.backgroundColor = .clear
        textView.delegate = context.coordinator   // 设置代理
        return textView
    }

    // 2. 当 SwiftUI 状态变化时,更新 UIView
    func updateUIView(_ textView: UITextView, context: Context) {
        // 避免无限循环:只在外部变化时才更新
        if textView.attributedText != text {
            textView.attributedText = text
        }
        // 更新其他属性
        context.coordinator.onTextChange = onTextChange
    }

    // 3. Coordinator:处理 UIView 的委托回调,并回传给 SwiftUI
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, UITextViewDelegate {
        var parent: RichTextEditor
        var onTextChange: ((NSAttributedString) -> Void)?

        init(_ parent: RichTextEditor) { self.parent = parent }

        // UITextViewDelegate 回调
        func textViewDidChange(_ textView: UITextView) {
            // 将 UIView 的变化回传给 SwiftUI @Binding
            parent.text = textView.attributedText
            onTextChange?(textView.attributedText)
        }
    }
}

// 使用
struct NoteEditorView: View {
    @State private var content = NSAttributedString(string: "")

    var body: some View {
        RichTextEditor(
            text: $content,
            onTextChange: { print("字数:\($0.length)") }
        )
        .frame(minHeight: 200)
        .padding()
        .overlay(RoundedRectangle(cornerRadius: 8).stroke(.secondary.opacity(0.3)))
    }
}

// ── 包装 MKMapView ────────────────────────────────────────
struct MapView: UIViewRepresentable {
    var region: MKCoordinateRegion
    var annotations: [MKPointAnnotation]
    @Binding var selectedAnnotation: MKPointAnnotation?

    func makeUIView(context: Context) -> MKMapView {
        let map = MKMapView()
        map.delegate = context.coordinator
        return map
    }

    func updateUIView(_ mapView: MKMapView, context: Context) {
        mapView.setRegion(region, animated: true)

        // 更新标注点
        let current = Set(mapView.annotations.compactMap { $0 as? MKPointAnnotation })
        let new     = Set(annotations)
        mapView.removeAnnotations(Array(current.subtracting(new)))
        mapView.addAnnotations(Array(new.subtracting(current)))
    }

    func makeCoordinator() -> Coordinator { Coordinator(self) }

    class Coordinator: NSObject, MKMapViewDelegate {
        var parent: MapView
        init(_ parent: MapView) { self.parent = parent }

        func mapView(_ mapView: MKMapView, didSelect annotation: MKAnnotation) {
            parent.selectedAnnotation = annotation as? MKPointAnnotation
        }
    }
}

// ── UIViewControllerRepresentable(包装 VC)──────────────
struct ImagePickerView: UIViewControllerRepresentable {
    @Binding var selectedImage: UIImage?
    @Environment(\.dismiss) var dismiss

    func makeUIViewController(context: Context) -> UIImagePickerController {
        let picker = UIImagePickerController()
        picker.delegate = context.coordinator
        picker.sourceType = .photoLibrary
        return picker
    }

    func updateUIViewController(_ vc: UIImagePickerController, context: Context) {}

    func makeCoordinator() -> Coordinator { Coordinator(self) }

    class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        var parent: ImagePickerView
        init(_ parent: ImagePickerView) { self.parent = parent }

        func imagePickerController(_ picker: UIImagePickerController,
                                   didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
            parent.selectedImage = info[.editedImage] as? UIImage
                                ?? info[.originalImage] as? UIImage
            parent.dismiss()
        }
        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            parent.dismiss()
        }
    }
}

// 使用
struct AvatarUploadView: View {
    @State private var showPicker = false
    @State private var avatar: UIImage?

    var body: some View {
        VStack {
            if let avatar {
                Image(uiImage: avatar)
                    .resizable()
                    .scaledToFill()
                    .frame(width: 100, height: 100)
                    .clipShape(Circle())
            } else {
                Circle().fill(.secondary.opacity(0.2)).frame(width: 100, height: 100)
                    .overlay(Image(systemName: "camera").font(.title2))
            }
            Button("选择头像") { showPicker = true }
        }
        .sheet(isPresented: $showPicker) {
            ImagePickerView(selectedImage: $avatar)
        }
    }
}
💡 UIViewRepresentable 生命周期
makeUIView:View 第一次出现时调用一次,相当于 UIView 的 init + setup
updateUIView:每次 SwiftUI 重新渲染时调用,同步最新状态到 UIView
makeCoordinator:在 makeUIView 之前调用,创建处理委托/回调的 Coordinator
dismantleUIView(可选):View 消失时清理资源