Highlight
SwiftUI 在 2021 年新增了 AsyncImage、refreshable、task、Table、searchable、Canvas、Materials、FocusState 等数十个 API,覆盖列表交互、图形绘制、文本处理、按钮样式和键盘导航,让开发者用更少的代码实现更丰富的跨平台体验。
核心内容
做 iOS 开发,加载网络图片是基本功。以前你要么手写 URLSession + UIImageView,要么引入第三方库。图片加载中的占位图、加载失败的处理、列表下拉刷新,每个环节都要自己搭。
SwiftUI 今年直接内置了 AsyncImage,给一个 URL 就能自动下载并显示图片,还自带默认占位图。配合 refreshable 和 task 修饰符,下拉刷新和页面加载时的异步数据获取也变成了声明式的一行代码。
列表交互方面,以前想在 List 的每一行里放一个可编辑的 TextField,你需要用索引去取 binding,这会导致整个列表重绘。今年 SwiftUI 支持直接把 binding 数组传给 List,闭包里直接拿到每个元素的 binding,代码简洁且性能更好。同时新增了 swipeActions,左右滑动触发自定义操作,用 Button 就能配置。
macOS 上终于有 Table 了。用 TableColumn 声明多列表格,支持单选、多选、排序,几行代码就能搭出 Finder 风格的文件列表。@FetchRequest 也新增了对排序描述符的 binding,Core Data 驱动的表格可以直接用。
搜索不再需要自己拼 UI。searchable 修饰符加在 NavigationView 上,SwiftUI 自动根据平台把搜索框放在正确位置,从 Apple TV 到 Apple Watch 都能用同一套代码。
图形方面,SF Symbols 新增了 Hierarchical 和 Palette 两种渲染模式,Tab Bar 的符号变体也自动适配平台。Canvas 视图支持即时模式绘制,配合 TimelineView 可以做动画效果。Material 效果直接通过 .ultraThinMaterial 等修饰符使用,自动处理 vibrancy 和 Dark Mode。
文本处理迎来了 Markdown 原生支持,Text("**Hello**, World!") 直接渲染加粗。AttributedString 提供了类型安全的属性操作。TextField 支持 format styles 和 prompt,键盘工具栏可以自定义,FocusState 让你精确控制焦点在表单中的移动。
按钮样式也大幅扩展。.bordered 提供标准边框按钮,.controlSize(.large) 和 .controlProminence(.increased) 控制尺寸和视觉层级。Menu 支持 primary action,点击执行默认操作,长按展开菜单。Toggle 可以用 .toggleStyle(.button) 变成按钮样式。ControlGroup 把相关按钮组合在一起。
详细内容
AsyncImage:一行代码加载网络图片
(03:22)
以前加载远程图片,你需要管理 URLSession 任务、处理缓存、写占位图逻辑。SwiftUI 今年直接提供了 AsyncImage 视图。
struct ContentView: View {
@StateObject private var photoStore = PhotoStore()
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 420))]) {
ForEach(photoStore.photos) { photo in
AsyncImage(url: photo.url)
.frame(width: 400, height: 266)
.mask(RoundedRectangle(cornerRadius: 16))
}
}
.padding()
}
.navigationTitle("Superhero Recruits")
}
.navigationViewStyle(.stack)
}
}
class PhotoStore: ObservableObject {
@Published var photos: [Photo] = []
}
struct Photo: Identifiable {
var id: URL { url }
var url: URL
}
关键点:
AsyncImage(url:)接收一个 URL,自动发起网络请求并显示图片- 默认提供灰色占位图,加载完成后自动切换
- 配合
LazyVGrid使用,图片在滚动到可视区域时才开始加载 - 全平台可用,包括 iOS、macOS、watchOS 和 tvOS
想要自定义占位图和加载动画?用完整闭包形式:
AsyncImage(url: photo.url, transaction: .init(animation: .spring())) { phase in
switch phase {
case .empty:
randomPlaceholderColor()
.opacity(0.2)
.transition(.opacity.combined(with: .scale))
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.transition(.opacity.combined(with: .scale))
case .failure(let error):
ErrorView(error)
@unknown default:
ErrorView()
}
}
关键点:
transaction参数传入动画配置,控制图片加载时的过渡效果phase枚举包含.empty、.success、.failure三种状态- 可以在每种状态下返回不同的视图,实现自定义占位图和错误处理
refreshable 与 task:异步操作声明化
(04:23)
下拉刷新是列表的常见需求。以前你需要集成 UIRefreshControl,处理 begin/endRefreshing 的状态。现在用 refreshable 修饰符:
List {
ForEach(photoStore.photos) { photo in
AsyncImage(url: photo.url)
.frame(minHeight: 200)
.listRowSeparator(.hidden)
}
}
.refreshable {
await photoStore.update()
}
关键点:
refreshable配置刷新动作,通过 environment 向下传递- iOS 和 iPadOS 上的 List 会自动显示下拉刷新控件
await表示异步操作,UI 不会被阻塞
页面首次加载数据用 task 修饰符:
List {
// ...
}
.refreshable {
await photoStore.update()
}
.task {
await photoStore.update()
}
关键点:
task在视图出现时启动异步任务,视图消失时自动取消- 适合页面初始化加载、监听实时数据等场景
- 配合
AsyncSequence可以持续接收数据更新:
.task {
for await photo in photoStore.newestPhotos {
photoStore.push(photo)
}
}
List 的 binding 元素与交互增强
(08:08)
List 的每一行需要编辑数据时,以前的做法是用索引取 binding,这会导致 SwiftUI 重绘整个列表。今年可以直接把 binding 数组传给 List:
List($directions) { $direction in
Label {
TextField("Instructions", text: $direction.text)
} icon: {
DirectionsIcon(direction)
}
}
关键点:
$directions把数组变成 binding,闭包里的$direction就是每个元素的 binding- 只读代码不需要改动,需要编辑的地方直接用
$direction.text - 这个语法是 Swift 语言特性,可以 back-deploy 到之前支持 SwiftUI 的版本
- 在
ForEach中同样适用
列表分割线也可以自定义了:
.listRowSeparatorTint(direction.color) // 修改分割线颜色
.listRowSeparator(.hidden) // 隐藏分割线
Swipe actions 让列表交互更灵活:
ForEach(characters) { $character in
CharacterProfile(character)
.swipeActions(edge: .leading) {
Button {
togglePinned(for: $character)
} label: {
Label(character.isPinned ? "Unpin" : "Pin", systemImage: character.isPinned ? "pin.slash" : "pin")
}
.tint(.yellow)
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
delete(character)
} label: {
Label("Delete", systemImage: "trash")
}
}
}
关键点:
swipeActions用 Button 定义操作,通过tint设置颜色edge参数控制出现在左侧还是右侧- 可以同时添加 leading 和 trailing 两组操作
role: .destructive自动使用红色,表示删除操作
macOS 专属:Table 多列表格
(12:13)
macOS 应用经常需要展示表格数据。SwiftUI 今年新增了 Table 视图:
Table(characters) {
TableColumn("") { CharacterIcon($0) }
.width(20)
TableColumn("Villain") { Text($0.isVillain ? "Villain" : "Hero") }
.width(40)
TableColumn("Name", value: \.name)
TableColumn("Powers", value: \.powers)
}
关键点:
Table接收一个数据集合,内部用TableColumn定义每一列value: \.name是简写形式,直接用 key path 显示文本- 也可以传闭包自定义列内容
Table 支持单选和多选:
@State private var singleSelection: StoryCharacter.ID?
@State private var multipleSelection: Set<StoryCharacter.ID>()
Table(characters, selection: $singleSelection) {
// ...columns
}
还支持点击表头排序:
@State private var sortOrder = [KeyPathComparator(\StoryCharacter.name)]
@State private var sorted: [StoryCharacter]?
Table(sorted ?? characters, selection: $selection, sortOrder: $sortOrder) {
// ...columns
}
.onChange(of: characters) { sorted = $0.sorted(using: sortOrder) }
.onChange(of: sortOrder) { sorted = characters.sorted(using: $0) }
关键点:
sortOrder是 binding,用户点击表头时自动更新KeyPathComparator用 key path 指定排序字段- 配合
@FetchRequest的 sort descriptor binding,Core Data 驱动的表格只需几行代码
searchable:跨平台搜索
(15:20)
搜索是跨平台需求,从 Apple TV 到 Apple Watch 都需要。SwiftUI 提供了统一的 searchable 修饰符:
NavigationView {
List {
if characters.filterText.isEmpty {
Section("Pinned") { sectionContent(for: characters.pinned) }
Section("Heroes & Villains") { sectionContent(for: characters.unpinned) }
} else {
sectionContent(for: characters.filtered)
}
}
.listStyle(.sidebar)
.searchable(text: $characters.filterText)
.navigationTitle("Characters")
}
关键点:
searchable(text:)接收一个 binding,自动在各平台放置搜索框- iOS 放在导航栏下方,macOS 放在工具栏,Apple TV 放在顶部
- 可以配合搜索建议实现更复杂的搜索体验
Canvas 与 TimelineView:高性能自定义绘制
(21:31)
需要绘制大量图形元素时,SwiftUI 的常规视图开销太大。Canvas 提供了即时模式绘制,类似 UIKit 的 drawRect:
Canvas { context, size in
let metrics = gridMetrics(in: size)
for (index, symbol) in symbols.enumerated() {
let rect = metrics[index]
let image = context.resolve(symbol.image)
context.draw(image, in: rect.fit(image.size))
}
}
关键点:
Canvas接收一个绘制闭包,context提供绘图接口context.resolve()把 SwiftUI 的 Image 转成可绘制的图形对象- 适合绘制大量不需要单独跟踪状态的几何图形
Canvas 可以附加手势和无障碍支持:
Canvas { context, size in
// 绘制代码...
}
.gesture(DragGesture(minimumDistance: 0).updating($focalPoint) { value, focalPoint, _ in
focalPoint = value.location
})
.accessibilityLabel("Symbol Browser")
.accessibilityChildren {
List(symbols) { Text($0.name) }
}
配合 TimelineView 实现动画:
TimelineView(.animation) { timeline in
let time = timeline.date.timeIntervalSince1970
Canvas { context, size in
// 用 time 更新绘制参数...
}
}
关键点:
TimelineView按 schedule 触发重绘,.animation表示每帧更新- 适合 tvOS 屏保、watchOS Always On 显示等场景
privacySensitive(true)可以让敏感内容在 Always On 状态下自动隐藏
Materials 与 safeAreaInset
(25:03)
Material 效果以前只在系统控件中可用,今年开放给开发者:
VStack {
Text("Symbol Browser")
.font(.largeTitle.bold())
Text("\(symbols.count) symbols")
.foregroundStyle(.secondary)
}
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16))
关键点:
.ultraThinMaterial、.thinMaterial、.regularMaterial、.thickMaterial等选项控制模糊程度- 自动处理 vibrancy,
.primary、.secondary、.tertiary、.quaternary前景样式在 material 上有正确表现 - emoji 自动排除在 vibrancy 处理之外
safeAreaInset 让浮动工具栏不遮挡内容:
ScrollView {
symbolGrid
}
.safeAreaInset(edge: .bottom, spacing: 0) {
VStack(spacing: 0) {
Divider()
Text("\(newSymbols.count) new symbols")
.foregroundStyle(.primary)
.font(.body.bold())
}
.padding()
.background(.regularMaterial)
}
关键点:
safeAreaInset在滚动视图边缘插入内容,同时让滚动内容自动避开该区域- 适合底部工具栏、悬浮按钮等场景
文本与格式化
(27:15)
Text 现在支持 Markdown:
Text("**Hello**, World!")
Text("""
Have a *happy* [WWDC](https://developer.apple.com/wwdc21/)!
""")
AttributedString 提供类型安全的属性操作:
var formattedDate: AttributedString {
var result = Date().formatted(
Date.FormatStyle()
.day()
.month(.wide)
.weekday(.wide)
.attributed
)
let weekday = AttributeContainer.dateField(.weekday)
let color = AttributeContainer.foregroundColor(.orange)
result.replaceAttributes(weekday, with: color)
return result
}
关键点:
.attributed获取带属性的格式化结果replaceAttributes按属性容器批量替换样式- 支持自定义属性,可以在 Markdown 语法中使用
文本选择:
Text(activity.info)
.textSelection(.enabled)
关键点:
textSelection(.enabled)让不可编辑的文本可以被选中复制- 可以应用在任意视图层级上,内部所有 Text 都会生效
- iOS 上长按弹出复制菜单,macOS 上直接拖拽选择
Format styles 让格式化变得类型安全:
Text(activity.people.map(\.nameComponents).formatted(
.list(memberStyle: .name(style: .short), type: .and)
))
关键点:
.list把数组格式化成 “A, B and C” 的形式.name(style:)格式化人名- 自动处理本地化,不同语言使用正确的连接词
TextField 也支持 format styles:
@State private var newAttendee = PersonNameComponents()
TextField("New Person", value: $newAttendee, format: .name(style: .medium))
关键点:
value绑定到PersonNameComponents类型,不是 String- format style 自动处理输入解析和输出格式化
- 类型安全,编译器会检查绑定类型和 format style 是否匹配
键盘与焦点
(31:38)
onSubmit 和 submitLabel 控制键盘提交行为:
TextField("New Person", value: $newAttendee, format: .name(style: .medium))
.onSubmit {
activity.append(Person(newAttendee))
newAttendee = PersonNameComponents()
}
.submitLabel(.done)
关键点:
onSubmit在用户按下 Return 键时触发submitLabel(.done)把键盘上的 Return 键改成 “Done”- 可以应用在单个 TextField 或整个 Form 上
键盘工具栏:
Form {
TextField("Name", text: $activity.name, prompt: Text("New Activity"))
TextField("Location", text: $activity.location)
DatePicker("Date", selection: $activity.date)
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button(action: selectPreviousField) {
Label("Previous", systemImage: "chevron.up")
}
.disabled(!hasPreviousField)
Button(action: selectNextField) {
Label("Next", systemImage: "chevron.down")
}
.disabled(!hasNextField)
}
}
关键点:
placement: .keyboard把工具栏放在键盘上方(iOS)或 Touch Bar(macOS)- 适合表单导航,用户不用收起键盘就能跳到下一个字段
FocusState 精确控制焦点:
private enum Field: Int, Hashable, CaseIterable {
case name, location, date, addAttendee
}
struct ContentView: View {
@State private var activity: Activity = .sample
@FocusState private var focusedField: Field?
var body: some View {
VStack {
Form {
TextField("Name:", text: $activity.name, prompt: Text("New Activity"))
.focused($focusedField, equals: .name)
TextField("Location:", text: $activity.location)
.focused($focusedField, equals: .location)
DatePicker("Date:", selection: $activity.date)
.focused($focusedField, equals: .date)
}
TextField("New Person", value: $newAttendee, format: .name(style: .medium))
.focused($focusedField, equals: .addAttendee)
Button {
focusedField = .addAttendee
} label: {
Label("Add Attendee", systemImage: "plus")
}
}
}
}
关键点:
FocusState用 property wrapper 追踪焦点状态- 可以绑定到
Bool(简单场景)或任意Hashable类型(多字段场景) .focused($focusedField, equals: .name)把字段和枚举值关联- 通过赋值
focusedField = .addAttendee主动移动焦点 - 设
focusedField = nil可以收起键盘
按钮新样式
(34:55)
标准边框按钮:
Button("Add") { }
.buttonStyle(.bordered)
关键点:
.buttonStyle(.bordered)给按钮添加标准边框样式- 可以应用在容器视图上,自动应用到所有子按钮
- 自动处理 pressed、disabled、Dark Mode 状态
控制尺寸和视觉层级:
HStack {
ForEach(entry.tags) { tag in
Button(tag.name) { }
.tint(tag.color)
}
}
.buttonStyle(.bordered)
.controlSize(.small)
.controlProminence(.increased)
关键点:
.controlSize(.small / .regular / .large)控制按钮尺寸.controlProminence(.increased)让按钮使用高对比度的强调色填充- 适合标记主要操作,但同一屏幕不宜过多
大型按钮:
VStack {
Button(action: addToJar) {
Text("Add to Jar").frame(maxWidth: 300)
}
.controlProminence(.increased)
.keyboardShortcut(.defaultAction)
Button(action: addToWatchlist) {
Text("Add to Watchlist").frame(maxWidth: 300)
}
.tint(.accentColor)
}
.buttonStyle(.bordered)
.controlSize(.large)
关键点:
.controlSize(.large)产生圆角矩形大按钮.keyboardShortcut(.defaultAction)让 Return 键触发该按钮- 次要按钮可以用
.tint(.accentColor)添加颜色但不填充
破坏性按钮和确认对话框:
.contextMenu {
Button("Open") { }
Button("Delete...", role: .destructive) {
showConfirmation = true
}
}
.confirmationDialog(
"Are you sure you want to delete \(entry.name)?",
isPresented: $showConfirmation
) {
Button("Delete", role: .destructive) { }
} message: {
Text("Deleting \(entry.name) will remove it from all of your jars.")
}
关键点:
role: .destructive自动使用红色,表示危险操作confirmationDialog在 iOS 显示为 action sheet,iPad 为 popover,macOS 为 alert- SwiftUI 自动按平台规范处理呈现方式
Menu 按钮支持 primary action:
Menu("Add") {
ForEach(jarStore.allJars) { jar in
Button("Add to \(jar.name)") {
jarStore.add(buttonEntry, to: jar)
}
}
} primaryAction: {
jarStore.addToDefaultJar(buttonEntry)
}
.menuStyle(BorderedButtonMenuStyle())
.menuIndicator(.hidden)
关键点:
primaryAction定义点击时的默认操作.menuIndicator(.hidden)隐藏下拉箭头- macOS 上显示为分段按钮,左侧触发 primary action,右侧展开菜单
- iOS 上单击触发 primary action,长按展开菜单
Toggle 按钮样式和 ControlGroup:
Toggle(isOn: $showOnlyNew) {
Label("Show New Buttons", systemImage: "sparkles")
}
.toggleStyle(.button)
ControlGroup {
Button(action: archive) {
Label("Archive", systemImage: "archiveBox")
}
Button(action: delete) {
Label("Delete", systemImage: "trash")
}
}
关键点:
.toggleStyle(.button)把 Toggle 变成可点击的按钮样式ControlGroup把相关按钮组合在一起,iOS 上紧凑排列,macOS 上有视觉分组- 可以嵌套使用,比如 ControlGroup 里放两个带 primaryAction 的 Menu
核心启发
1. 用 AsyncImage + refreshable + task 重构你的图片列表
做什么:把现有的图片列表换成 SwiftUI 原生方案。
为什么值得做:删除第三方依赖,代码从几十行减到几行,自动获得占位图、动画过渡、错误处理。
怎么开始:把 Image 换成 AsyncImage(url:),List 上加 .refreshable { await load() },视图上加 .task { await load() }。
2. 给 macOS 应用加上 Table 视图
做什么:在 macOS 版本的数据管理界面中使用多列表格。
为什么值得做:Table 让 macOS 应用立刻有了原生 Finder 风格,支持排序、多选,代码比 AppKit 的 NSTableView 少一个数量级。
怎么开始:用 Table(data) { TableColumn(...) } 替换现有的 List,加上 selection 和 sortOrder binding。
3. 用 Canvas 做高性能数据可视化
做什么:用 Canvas 绘制大量几何图形,比如图表、网格、粒子效果。
为什么值得做:Canvas 避免了创建成千上万个 SwiftUI 视图的开销,配合 TimelineView 可以轻松做动画。
怎么开始:把 ZStack { ForEach(...) } 换成 Canvas { context, size in ... },用 context.draw() 直接绘制。
4. 用 FocusState 优化表单体验
做什么:给包含多个输入框的表单添加键盘导航和焦点控制。
为什么值得做:用户不用手动点击每个字段,按键盘上的上/下按钮就能在表单间跳转,移动端体验大幅提升。
怎么开始:定义一个 Field 枚举,用 @FocusState private var focusedField: Field?,给每个输入框加 .focused($focusedField, equals: .fieldName)。
5. 用 searchable 统一全平台搜索
做什么:给列表页面添加搜索功能,一套代码覆盖 iPhone、iPad、Mac、Apple TV、Apple Watch。
为什么值得做:不用为每个平台写不同的搜索 UI,SwiftUI 自动把搜索框放在正确位置。
怎么开始:在 NavigationView 上加 .searchable(text: $searchText),根据 searchText 过滤列表数据。
关联 Session
- Discover concurrency in SwiftUI — 深入理解 SwiftUI 的并发模型,以及 async/await 如何与声明式更新机制配合。
- Demystify SwiftUI — 从身份、生命周期和依赖关系三个维度理解 SwiftUI 的核心设计哲学。
- SwiftUI on the Mac: Build the fundamentals — 手把手教你用 SwiftUI 构建原生 macOS 应用。
- Add rich graphics to your SwiftUI app — 探索 Canvas、Material 和安全区域等图形相关 API。
- Direct and reflect focus in SwiftUI — 学习如何在 SwiftUI 中精确控制焦点导航。
评论
GitHub Issues · utterances