Introduction to SwiftUI
Swift & UI 入门 36m

SwiftUI 入门指南

Introduction to SwiftUI

2020年6月22日

在 Apple 官方观看视频

一句话判断

SwiftUI 在 2020 年进入了第二个版本,新增了 App 生命周期管理、Lazy 布局、MapKit 集成、更好的列表支持等关键功能,它已经从”实验性框架”进化为可以用于生产环境的核心 UI 框架。

这场 Session 讲了什么

这是 SwiftUI 的入门级全面介绍,面向还没有接触过 SwiftUI 或者只初步了解的开发者。Session 从 SwiftUI 的核心理念(声明式 UI、数据驱动、实时预览)开始,逐步介绍了 SwiftUI 的主要组件和使用方式。

2020 年版本的重点更新包括:新的 App 协议替代了 AppDelegate/SceneDelegate 的组合,让 App 入口更简洁;LazyVStackLazyHStack 解决了 ScrollView 中大量内容的性能问题;ScrollViewReader 提供了编程式滚动控制;Map 视图直接集成了 MapKit;ProgressViewGauge 提供了标准的进度展示组件;Label 统一了图标+文字的组合展示。Session 还介绍了 Xcode 12 中 SwiftUI 预览的改进,包括支持多个预览和更快的刷新速度。

值得深挖的点

App 协议:SwiftUI 的全新入口

iOS 14 之前,SwiftUI App 仍然需要 UIKit 的 AppDelegateSceneDelegate 作为入口。iOS 14 引入了 App 协议,你只需要一个 @main 标注的结构体就能定义整个 App 的入口、窗口场景和根视图。这个改动让 SwiftUI App 可以完全不依赖 UIKit 的代码。

Lazy 容器的性能意义

之前的 VStackHStack 会一次性渲染所有子视图。如果你有一个包含 1000 个元素的列表,内存和渲染时间都会成为问题。LazyVStackLazyHStack 只渲染当前可见区域的子视图,滚动到新区域时才创建新视图。这对长列表和无限滚动的场景至关重要。

代码片段

使用新的 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 实现,利用 UIHostingControllerUIViewRepresentable 在 UIKit 和 SwiftUI 之间桥接。优先用 SwiftUI 实现新页面,特别是设置页、表单页等标准化界面。

新项目:全面使用 SwiftUI。新的 App 协议让项目结构更简洁。利用 SwiftUI Preview 来加速 UI 开发——在 Preview 中调试比编译运行整个 App 快得多。使用 LazyVStack/LazyHStack 来确保长列表的性能。

还有什么值得关注

  • SwiftUI 的 @StateObject@ObservedObject 的区别:@StateObject 负责创建和持有对象,@ObservedObject 只负责观察。
  • @AppStorage 提供了简单的方式来读写 UserDefaults。
  • Xcode 12 支持在 SwiftUI Preview 中使用 Instruments 进行性能分析。
WWDC 2020