① async/await 基础 Swift 5.5
心智模型:从回调到直线型代码
用了十年 GCD,你对 DispatchQueue.async { ... } 加回调的模式已经非常熟悉。然而一旦需要多个异步操作串行,就会陷入回调地狱(Callback Hell)——缩进层层嵌套,错误处理散落各处,线程安全靠肉眼保证。
async/await 的本质是:编译器把"挂起点"之后的代码自动打包成续体(Continuation),让你可以用同步的写法表达异步逻辑,线程切换由运行时负责。
GCD 写法 vs async/await 写法对比
// ❌ 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)
}
}
}
// ✅ 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)
}
}
await 表达式都是一个挂起点。Swift 运行时在此暂停当前任务,把线程还给系统去做其他工作,异步操作完成后再恢复执行——整个过程不阻塞任何线程。这正是它比 GCD + 信号量方案高效得多的原因。
async 函数定义规则
// 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 的组合
// 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 等同步方法中桥接
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 等待。
| 概念 | GCD | Swift Concurrency |
|---|---|---|
| 异步执行单元 | DispatchWorkItem / 闭包 | Task |
| 优先级 | QoS (.userInteractive / .background …) | TaskPriority (.high / .medium / .low / .background) |
| 取消 | DispatchWorkItem.cancel()(弱取消) | Task.cancel() + 协作式取消 |
| 等待结果 | DispatchGroup + notify / semaphore(阻塞!) | await task.value(非阻塞挂起) |
| 错误传播 | 回调参数传递 | throws / rethrows 直接传播 |
创建非结构化任务
// ① 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 的"弱取消"类似,但更完善。取消只是设置一个标志,任务本身必须检查并响应。
// 取消任务
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 优先级与调度
// 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()
}
}
self,记得用 [weak self] 或确保任务在对象销毁时取消,否则会造成内存泄漏。
③ 结构化并发:async let 与 TaskGroup SE-0317
结构化并发(Structured Concurrency)的核心思想:子任务的生命周期不能超过父任务。就像函数调用栈一样,父任务结束时所有子任务必须已完成或已取消。这提供了可预测的生命周期和自动取消传播。
async let —— 并行执行固定数量任务
当你知道需要并行执行几个固定数量的任务时,async let 是最简洁的写法。对比 GCD 的 DispatchGroup,代码量减少了 80%。
// ❌ 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,但更灵活、安全。
// 场景:并发下载多张图片
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] }
}
}
2. 错误传播:任意子任务抛错,其他子任务被取消,错误向上传播。
3. 生命周期保证:编译器确保父任务作用域结束时所有子任务已处理完毕,杜绝"野任务"。
④ Actor 与 MainActor SE-0306
Actor 解决了什么问题?
GCD 时代,保护共享可变状态靠的是:os_unfair_lock、NSLock、串行 DispatchQueue + sync。这些都需要工程师自己记住并正确使用,一旦遗漏就是数据竞争 Bug。
actor 是 Swift 内置的引用类型,编译器强制要求所有对 actor 内部状态的访问都经过隔离(isolation),彻底消除数据竞争。
// ❌ 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 { } 的用法,而且类型系统会帮你检查是否正确使用。
// 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
① 不支持继承(类似 struct)
② 访问其存储属性/方法必须跨越 actor 边界(加
await)③ 同一时间只有一个任务在 actor 内部执行(串行化保证)
④
nonisolated 方法可以绕过隔离,适合纯计算或不访问状态的方法
Swift 6(严格模式):所有跨 actor 访问均为编译期错误;新增
nonisolated(unsafe) 修饰符,用于明确声明"我保证安全,编译器不必检查";全局 actor 推断规则更严格(详见 §1-11)。
// 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 ──────────────────────────────
// ✅ 值类型(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 闭包的约束:
// 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 6 严格并发检查 ──────────────────────────────────
// Swift 6 默认开启严格并发检查(SE-0337)
// 在此之前可以逐步迁移:Build Settings → Swift Language Version = Swift 6
// 或在文件/模块级别使用 @preconcurrency import 过渡
| 特性 | Swift 5(宽松) | Swift 6(严格) |
|---|---|---|
| Sendable 违规 | 警告(可忽略) | 编译错误 |
| 跨 actor 传递非 Sendable | 警告 | 编译错误 |
@unchecked Sendable | 可用 | 可用(需更谨慎) |
| 闭包 Sendable 推断 | 手动标注 | 编译器自动推断更多场景 |
| 全局变量并发访问 | 无限制 | 必须在 actor 或为 Sendable 常量 |
@preconcurrency 导入旧模块以消除迁移期间的误报。详细迁移策略见 §1-8。
⑥ AsyncSequence 与 AsyncStream SE-0298
AsyncSequence 是什么?
普通 Sequence 是同步的(for-in 立即消耗所有元素)。AsyncSequence 是异步版本——每次获取下一个元素都可能挂起,等待数据就绪。类比 Combine 的 Publisher,但更贴近 for-in 的心智模型。
// ── 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 函数,是迁移的"万能胶"。
// ── 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/notify | async let 或 withTaskGroup |
DispatchSemaphore(阻塞!) | async let / await task.value(非阻塞挂起) |
DispatchQueue(label:, attributes: .concurrent) | withTaskGroup 并发执行 |
DispatchWorkItem.cancel() | Task.cancel() + 协作式检查 |
NSLock / os_unfair_lock | actor(隔离共享状态) |
DispatchQueue + barrier async | actor 的普通方法(天然串行) |
| 回调闭包传递结果 | async throws 函数返回值 |
Timer + GCD | Task.sleep(for:) 或 Clock.sleep |
// 旧: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) |
| 数据竞争检测 | 运行时(偶发) | 编译期静态保证 |
| 迁移成本 | — | 中等(通常数小时~数天) |
逐步迁移策略
推荐按模块粒度逐步升级,不必一次性改动整个代码库:
// 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 6 编译错误)
var currentUser: User? = nil
func updateUser(_ user: User) {
currentUser = user // 数据竞争!
}
// ✅ 方案1:隔离到 @MainActor
@MainActor var currentUser: User? = nil
// ✅ 方案2:放入 actor
actor UserStore {
var currentUser: User? = nil
func update(_ user: User) { currentUser = user }
}
② 非 Sendable 类型跨 actor 传递
// ❌ 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
}
}
// ✅ 改为 struct(值语义,自动 Sendable)
struct RequestBuilder: Sendable {
var headers: [String: String] = [:]
}
// ✅ 或加 @unchecked Sendable(需自行保证安全)
final class RequestBuilder: @unchecked Sendable {
private let lock = NSLock()
var headers: [String: String] = [:]
}
③ 回调闭包中的并发违规
// ❌ 在非隔离闭包里修改 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
}
// ✅ 通过 actor 方法修改
actor Counter {
var count = 0
func increment() { count += 1 }
}
let c = Counter()
Task {
await c.increment() // ✅ 跨 actor 边界用 await
}
④ @preconcurrency 过渡旧模块
// 当第三方库或旧模块尚未迁移到 Swift 6 时,用 @preconcurrency 抑制误报
@preconcurrency import OldFramework // 导入时标注:此模块的 Sendable 警告降级处理
// 在遵从协议时标注(协议来自旧模块)
@preconcurrency
extension MyModel: OldProtocol {
func oldMethod() { ... }
}
// 标注回调闭包来自旧模块
func legacyAPI(completion: @preconcurrency @escaping (Result<Data, Error>) -> Void) { ... }
⑨ ~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 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
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 已被消耗
}
典型用例:唯一资源管理
// 建模一次性令牌(只能使用一次)
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)
}
class 是引用类型,多个引用可以指向同一对象(共享语义)。~Copyable struct 是值类型但具有独占所有权语义——不存在多个引用共享,因此更适合建模"资源令牌"而非"共享状态"。~Copyable 不等于 class:它在栈上分配,没有引用计数开销。
⑩ Typed Throws SE-0413
Swift 5 的 throws 关键字会擦除错误类型——函数签名只说明"可能抛错",但不说明抛什么类型的错误。调用方必须处理 any Error,丢失了类型信息。Swift 6 引入 throws(ErrorType) 解决这一痛点。
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
}
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 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 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)
}
不适合:① 内部实现细节;② 错误可能来自多个不同来源(此时
any Error 更灵活)。
⑪ Swift 6 Actor 新特性 SE-0423
Swift 6 对 actor 模型进行了多项改进:全局 actor 推断规则收紧、新增 nonisolated(unsafe)、协议遵从与 actor isolation 的交互更清晰。
全局 Actor 推断规则变化(SE-0423)
// Swift 5:UIViewController 子类隐式推断为 @MainActor
// 但规则不一致,有时推断,有时不推断
class MyVC: UIViewController {
var data: [Item] = []
// Swift 5: data 可能隐式被认为在 @MainActor 上
// 但不同版本行为不同,造成混淆
func loadData() {
Task {
// 是否在 MainActor 上?不确定…
self.data = await fetchItems()
}
}
}
// 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) 修饰符
// 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 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 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 } // ✅ 需要显式切换
}
}
}
- 显式优于隐式:不再依赖 actor isolation 的隐式推断,所有隔离意图明确标注
nonisolated(unsafe)是逃生舱口,不是常规写法——优先用正确的 actor 隔离- 协议遵从的 isolation 必须精确匹配,Swift 6 编译器会强制执行
- 迁移旧代码时,
@preconcurrency是过渡期的好工具
① Publisher · Subscriber · Subscription 生命周期
核心三角关系
Combine 的整个框架建立在三个协议上:
- Publisher:数据/事件的生产者。定义输出类型
Output和失败类型Failure。 - Subscriber:数据的消费者。向 Publisher 请求数据,接收值和完成/错误事件。
- Subscription:连接 Publisher 和 Subscriber 的令牌,控制背压(backpressure)。
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 类型详解
// 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 时什么都不发
// 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 { ... } } 包裹
// 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)
// 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
// 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)
// 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()
错误处理操作符
// 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)
时序操作符
// 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 等)。
// 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
// 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 开发的必备技能。
// ── .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 选择指南
| 维度 | Combine | async/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 完整实战
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 | 属性自动成为 Publisher | ObservableObject 的核心,$属性名 访问 |
操作符选择速查
| 需求 | 操作符 |
|---|---|
| 搜索框防抖 | debounce(for:scheduler:) |
| 避免重复触发 | removeDuplicates() |
| 取消旧请求(竞态) | flatMap + switchToLatest |
| 两个字段都满足才触发 | combineLatest |
| 两个请求配对使用结果 | zip |
| 出错不终止整个流 | flatMap { ... .catch { Just([]) } } |
| 切回主线程更新 UI | receive(on: DispatchQueue.main) |
| 过滤 nil / 类型转换 | compactMap |
① 声明式 vs 命令式编程范式
核心思维转变
// ── 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,只更新真正变化的部分(不是全量重建)
2. body 必须是纯函数:给定相同的状态,body 应该始终生成相同的 UI
3. 不要在 body 外修改 @State:所有状态变化都应该是用户操作或事件的响应
4. View 被频繁重新计算:body 的计算要轻量,耗时操作放在 .task 或 ViewModel 里
② 状态管理全解:@State · @Binding · @StateObject · @ObservedObject · @EnvironmentObject · @Observable
决策树:选哪个?
| 属性包装器 | 数据来源 | 数据类型 | 典型场景 |
|---|---|---|---|
@State | View 本身拥有 | 简单值类型 | 展开/收起、输入文本、选中状态 |
@Binding | 父 View 传入 | 任意 | 子组件双向修改父 View 的状态 |
@StateObject | View 本身创建 | ObservableObject | 该 View 拥有并管理 ViewModel 生命周期 |
@ObservedObject | 外部传入 | ObservableObject | 接收并观察外部 ViewModel(不拥有) |
@EnvironmentObject | 环境(祖先注入) | ObservableObject | 全局数据(登录状态、主题、路由) |
@Environment | 系统/自定义环境值 | 值类型 | colorScheme、locale、自定义配置 |
@Observable | 任意 | class(iOS 17+) | 新一代替代 ObservableObject,更高效 |
// @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)
}
}
// ⚠️ 最常见的错误:混淆 @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 才能保持稳定
}
// @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
}
}
}
}
// @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 布局
// 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)
}
// 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() }
}
// 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。
// ── 旧方式(弃用)────────────────────────────────────────
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 到对应页面
}
}
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 与数据展示
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 显式动画
// ── 隐式动画(.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)
}
}
// .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 { } } 的写法,且更安全。
// ── .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。
// ── 包装 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)
}
}
}
updateUIView:每次 SwiftUI 重新渲染时调用,同步最新状态到 UIView
makeCoordinator:在 makeUIView 之前调用,创建处理委托/回调的 Coordinator
dismantleUIView(可选):View 消失时清理资源