SwiftUI 入门指南
Introduction to SwiftUI
2020年6月22日
一句话判断
SwiftUI 在 2020 年进入了第二个版本,新增了 App 生命周期管理、Lazy 布局、MapKit 集成、更好的列表支持等关键功能,它已经从”实验性框架”进化为可以用于生产环境的核心 UI 框架。
这场 Session 讲了什么
这是 SwiftUI 的入门级全面介绍,面向还没有接触过 SwiftUI 或者只初步了解的开发者。Session 从 SwiftUI 的核心理念(声明式 UI、数据驱动、实时预览)开始,逐步介绍了 SwiftUI 的主要组件和使用方式。
2020 年版本的重点更新包括:新的 App 协议替代了 AppDelegate/SceneDelegate 的组合,让 App 入口更简洁;LazyVStack 和 LazyHStack 解决了 ScrollView 中大量内容的性能问题;ScrollViewReader 提供了编程式滚动控制;Map 视图直接集成了 MapKit;ProgressView 和 Gauge 提供了标准的进度展示组件;Label 统一了图标+文字的组合展示。Session 还介绍了 Xcode 12 中 SwiftUI 预览的改进,包括支持多个预览和更快的刷新速度。
值得深挖的点
App 协议:SwiftUI 的全新入口
iOS 14 之前,SwiftUI App 仍然需要 UIKit 的 AppDelegate 和 SceneDelegate 作为入口。iOS 14 引入了 App 协议,你只需要一个 @main 标注的结构体就能定义整个 App 的入口、窗口场景和根视图。这个改动让 SwiftUI App 可以完全不依赖 UIKit 的代码。
Lazy 容器的性能意义
之前的 VStack 和 HStack 会一次性渲染所有子视图。如果你有一个包含 1000 个元素的列表,内存和渲染时间都会成为问题。LazyVStack 和 LazyHStack 只渲染当前可见区域的子视图,滚动到新区域时才创建新视图。这对长列表和无限滚动的场景至关重要。
代码片段
使用新的 App 协议创建入口
场景:用最少的代码创建一个完整的 SwiftUI App。
import SwiftUI
@main
struct MyApp: App {
// App 的入口,替代 AppDelegate + SceneDelegate
var body: some Scene {
WindowGroup {
// 根视图
ContentView()
}
}
}
// 数据模型
struct Task: Identifiable {
let id = UUID()
var title: String
var isCompleted: Bool = false
}
// 主内容视图
struct ContentView: View {
@State private var tasks: [Task] = [
Task(title: "学习 SwiftUI"),
Task(title: "完成项目"),
Task(title: "发布 App")
]
var body: some View {
NavigationView {
List {
ForEach($tasks) { $task in
TaskRow(task: $task)
}
.onDelete { indexSet in
tasks.remove(atOffsets: indexSet)
}
}
.navigationTitle("待办事项")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: addTask) {
Image(systemName: "plus")
}
}
}
}
}
private func addTask() {
tasks.append(Task(title: "新任务"))
}
}
struct TaskRow: View {
@Binding var task: Task
var body: some View {
HStack {
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundColor(task.isCompleted ? .green : .gray)
.onTapGesture {
task.isCompleted.toggle()
}
Text(task.title)
.strikethrough(task.isCompleted)
.foregroundColor(task.isCompleted ? .secondary : .primary)
}
}
}
使用 LazyVStack 和 ScrollViewReader
场景:构建一个高性能的长列表。
struct LongListView: View {
let items: [Item] = Array(1...1000).map { Item(id: $0, title: "项目 \($0)") }
var body: some View {
ScrollViewReader { proxy in
ScrollView {
// 使用 LazyVStack 只渲染可见的视图
LazyVStack(spacing: 12) {
ForEach(items) { item in
ItemRow(item: item)
.id(item.id) // 用于 ScrollViewReader 定位
}
}
.padding()
}
.navigationTitle("长列表")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("跳到底部") {
// 编程式滚动到指定位置
withAnimation {
proxy.scrollTo(items.last?.id, anchor: .bottom)
}
}
}
}
}
}
}
struct Item: Identifiable {
let id: Int
let title: String
}
struct ItemRow: View {
let item: Item
var body: some View {
HStack {
Image(systemName: "\(item.id).circle")
Text(item.title)
Spacer()
}
.padding()
.background(Color(.secondarySystemBackground))
.cornerRadius(8)
}
}
使用 Map 视图
场景:在 SwiftUI 中显示地图。
import SwiftUI
import MapKit
struct MapView: View {
// 地图区域状态
@State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 39.9042, longitude: 116.4074),
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
)
// 标注点
let annotations: [Landmark] = [
Landmark(name: "天安门", coordinate: CLLocationCoordinate2D(latitude: 39.9087, longitude: 116.3975)),
Landmark(name: "故宫", coordinate: CLLocationCoordinate2D(latitude: 39.9163, longitude: 116.3972))
]
var body: some View {
Map(coordinateRegion: $region,
annotationItems: annotations
) { landmark in
MapAnnotation(coordinate: landmark.coordinate) {
VStack {
Text(landmark.name)
.font(.caption)
.padding(4)
.background(Color.white)
.cornerRadius(4)
Image(systemName: "mappin.circle.fill")
.foregroundColor(.red)
.font(.title)
}
}
}
.edgesIgnoringSafeArea(.all)
}
}
struct Landmark: Identifiable {
let id = UUID()
let name: String
let coordinate: CLLocationCoordinate2D
}
多平台预览
场景:在 Xcode 中同时预览 iPhone 和 iPad 版本。
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
// iPhone 预览
ContentView()
.previewDevice("iPhone 11")
.previewDisplayName("iPhone")
// iPad 预览
ContentView()
.previewDevice("iPad Pro (11-inch)")
.previewDisplayName("iPad")
// 暗色模式预览
ContentView()
.environment(\.colorScheme, .dark)
.previewDisplayName("暗色模式")
}
}
最佳实践
已有项目:如果你的 App 已经使用 UIKit,不需要一次性迁移到 SwiftUI。从新功能开始用 SwiftUI 实现,利用 UIHostingController 和 UIViewRepresentable 在 UIKit 和 SwiftUI 之间桥接。优先用 SwiftUI 实现新页面,特别是设置页、表单页等标准化界面。
新项目:全面使用 SwiftUI。新的 App 协议让项目结构更简洁。利用 SwiftUI Preview 来加速 UI 开发——在 Preview 中调试比编译运行整个 App 快得多。使用 LazyVStack/LazyHStack 来确保长列表的性能。
还有什么值得关注
- SwiftUI 的
@StateObject和@ObservedObject的区别:@StateObject负责创建和持有对象,@ObservedObject只负责观察。 @AppStorage提供了简单的方式来读写 UserDefaults。- Xcode 12 支持在 SwiftUI Preview 中使用 Instruments 进行性能分析。