What's new in SwiftUI
Swift & UI 进阶 40m

SwiftUI 的新特性

What's new in SwiftUI

2021年6月7日

在 Apple 官方观看视频

一句话判断

SwiftUI 在 2021 年终于补齐了生产级 app 所需的最后几块拼图——async/await 集成、List 的刷新控制、macOS 原生支持——如果你之前还在观望,现在是认真考虑全面采用的时候了。

这场 Session 讲了什么

这是 WWDC 2021 中关于 SwiftUI 最重要的 overview session。Apple 展示了 SwiftUI 在 iOS 15、macOS 12、watchOS 8 和 tvOS 15 上的全面改进。改动范围非常广,从基础的列表交互到高级的图形渲染都有涉及。

最大的变化是 Swift Concurrency 的深度集成。@MainActor 现在自动应用于 SwiftUI 视图的 body 计算,task 修饰符让异步操作的启动和取消变得极其自然。配合 AsyncImage,从网络加载图片终于不需要第三方库了。

List 和 ScrollView 也得到了大幅增强。refreshable 修饰符让下拉刷新只需要一行代码。LazyVStackLazyHStack 现在在 tvOS 和 watchOS 上也可用了。macOS 方面,SwiftUI 终于支持了 Table 组件和 commands 修饰符,这让构建 Mac app 变得现实。

值得深挖的点

async/await 与 SwiftUI 视图生命周期的绑定

task 修饰符是 SwiftUI 对 async/await 最优雅的封装。它会在视图出现时启动异步任务,在视图消失时自动取消(cooperative cancellation)。这意味着你不需要再手动管理 Task 的生命周期。更关键的是,task 的闭包运行在 @MainActor 上下文中,所以你可以在里面直接更新 @State 属性,不用 DispatchQueue.main.async

AsyncImage 的内存管理

AsyncImage 内部使用了 URLSessionImageCache,但它不会无限缓存图片。Session 提到 AsyncImage 默认使用系统的图片缓存策略,在内存压力大时会自动清理。如果你需要更精细的缓存控制(比如离线缓存、自定义 TTL),仍然需要自己实现图片加载层。AsyncImage 更适合快速原型和简单场景。

代码片段

// 使用 task 修饰符加载异步数据
struct UserListView: View {
    @State private var users: [User] = []
    
    var body: some View {
        List(users) { user in
            Text(user.name)
        }
        .task {
            // 视图出现时自动执行
            // 视图消失时自动取消
            do {
                users = try await fetchUsers()
            } catch {
                print("加载失败: \(error)")
            }
        }
    }
    
    func fetchUsers() async throws -> [User] {
        let url = URL(string: "https://api.example.com/users")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode([User].self, from: data)
    }
}
// 下拉刷新只需要一个修饰符
struct NewsFeedView: View {
    @State private var articles: [Article] = []
    
    var body: some View {
        List(articles) { article in
            ArticleRow(article: article)
        }
        .refreshable {
            // 用户下拉时执行,完成后隐藏刷新指示器
            articles = try! await loadLatestArticles()
        }
    }
}
// AsyncImage 加载并显示网络图片
struct AvatarView: View {
    let urlString: String
    
    var body: some View {
        AsyncImage(url: URL(string: urlString)) { phase in
            switch phase {
            case .success(let image):
                image
                    .resizable()
                    .scaledToFill()
                    .frame(width: 60, height: 60)
                    .clipShape(Circle())
            case .failure:
                Image(systemName: "person.circle.fill")
                    .foregroundColor(.gray)
            case .empty:
                ProgressView()
            @unknown default:
                EmptyView()
            }
        }
    }
}

最佳实践

从 iOS 14 迁移到 iOS 15 时,优先把现有的 onAppear + URLSession 数据加载模式替换为 task 修饰符。这个改动风险低但收益大:自动取消意味着不会有视图已消失但网络请求仍在执行的问题。

如果你还在用 CombineonReceive 来响应数据流,考虑迁移到 task + AsyncStreamAsyncStreamPublisher 的 async/await 等价物,在 SwiftUI 中使用更自然。但 Combine 不会被废弃,两者可以共存。

还有什么值得关注

  • searchable 修饰符让搜索栏的添加变成一行代码,配合 @Environment(\.isSearching) 可以控制搜索状态。
  • swipeActions 修饰符终于替代了 UIKit 的 trailingSwipeActionsConfiguration
  • Material 背景效果(.ultraThinMaterial 等)现在在 SwiftUI 中原生支持,不用再hack UIKit 的 UIVisualEffectView
  • macOS 上的 Table 组件支持排序、选择和多列布局,是构建数据管理类 app 的利器。
WWDC 2021