本节摘要:SwiftUI 的世界观是"界面是数据的函数":结构体视图描述界面,@State 状态驱动 body 重算,框架做差异更新;生命周期浓缩为 onAppear 与 onDisappear 两个语义时刻。本节从最小视图写到带异步任务的排片页,把 5.2 节的任务取消闭环接进生命周期回调。
大幕拉开。第六章的三件机关(some、属性包装器、结果构建器)在这一节全部现身——现在你知道它们为什么提前装好了。
import SwiftUI struct SessionRow: View { // 遵循 View 协议(第3章的契约机制) let filmName: String let time: String var body: some View { // some:第6.2 节的不透明类型 Text("《\(filmName)》 \(time)") // 一行描述就是一棵视图树的全部 } } let row = SessionRow(filmName: "雾中灯塔", time: "19:00") print(type(of: row.body)) // 教学观察:body 的具体类型由编译器掌握
视图是结构体(值语义,第3章的选择在此兑现):不可变、轻量、随时可被框架重建。body 里没有"执行"只有"描述"——描述本身就是产品。
界面要能动,需要状态。@State 是属性包装器(6.2 节亲手造过的那个模式)在框架里的官方实现:
struct BoardView: View { @State private var seats = 42 // 状态:驱动重算的源 @State private var isBooking = false var body: some View { VStack { // 结果构建器把多行组装成视图树(6.2 节) Text("余票 \(seats)") // 描述随 seats 变化 Button(isBooking ? "出票中…" : "立即购票") { isBooking = true // 写状态,不是改视图 seats -= 1 // 框架感知变化 → 重算 body → 差异更新 isBooking = false } } } }
按钮闭包里没有一句"更新标签文字"。写状态 → 框架重算 body → 比较新旧描述 → 只改有差异的部分。这条链路是 SwiftUI 的一生心跳:

⚠️ 常见坑:在 body 里做耗时计算或直接改状态(body 求值期间写 @State 会引发未定义行为)。重活放 onAppear 或按钮闭包,body 保持纯描述。
SwiftUI 把界面一生浓缩成两个语义时刻:出现(进入屏幕)与消失(离开屏幕)。这两个时刻正好安放 5.2 节的异步任务闭环:
struct BoardScreen: View { @State private var showtimes: [String] = [] @State private var loadTask: Task<Void, Never>? // 任务句柄:退场的抓手 var body: some View { List(showtimes, id: \.self) { item in Text(item) } .onAppear { // 出现:加载并持柄 loadTask = Task { showtimes = await fetchShowtimes() } } .onDisappear { // 消失:取消未完任务(5.2 节闭环落地) loadTask?.cancel() loadTask = nil } } private func fetchShowtimes() async -> [String] { try? await Task.sleep(nanoseconds: 200_000_000) // 模拟网络 return ["雾中灯塔 19:00", "星际列车 20:30", "深海回声 22:00"] } }
列表从空到三条的刷新不是你操作的——是 showtimes 状态变化后框架重算的结果。onDisappear 里取消任务,界面即使被导航销毁,僵尸任务也不会白耗流量(4.2 节的 weak 语义与这里的 cancel 语义互为补充)。
变式:把 fetchShowtimes 改成 async throws 版本,加一个 @State private var errorMsg: String?,在 Task 里 do-catch(2.3 节)——加载失败态就齐了。三个状态(数据、错误、加载中)合成一个枚举 LoadingState(3.1 节的 SessionPhase 模式),是 SwiftUI 工程的标准建模。
背景:把"加载中 / 成功 / 失败"建成一个枚举状态,body 按状态分支描述。操作与结果:
enum LoadingState { case idle // 初始 case loading // 加载中 case loaded([String]) // 成功:关联数据 case failed(String) // 失败:关联原因 } struct RobustBoardScreen: View { @State private var state: LoadingState = .idle var body: some View { Group { switch state { // switch 穷举:编译器保证三态都有戏(3.1 节红利) case .idle, .loading: ProgressView("正在拉取排片…") case .loaded(let list): List(list, id: \.self) { Text($0) } case .failed(let reason): VStack { Text("加载失败:\(reason)") Button("重试") { Task { await reload() } } } } } .onAppear { Task { await reload() } } } private func reload() async { state = .loading do { try await Task.sleep(nanoseconds: 150_000_000) state = .loaded(["雾中灯塔 19:00", "星际列车 20:30"]) } catch { state = .failed("网络超时") // 2.3 节的错误分支收敛为状态 } } }
结果解读:SwiftUI 的生命周期本质上就是这个状态机的流转——idle → loading → loaded/failed,界面随每次流转自动换装。命令式框架里要手写的"转圈控件显隐、错误标签显隐、重试按钮绑定",在这里全是 switch 分支的描述。这个模式可以直接搬进真实工程,只要把 reload 的实现换成网络层。
💡 关键直觉:判断自己有没有转进声明式思维的测试题——"我想让这行字变红",命令式答案是找到标签改颜色,声明式答案是给状态加个字段。前者操作界面,后者操作数据。
SwiftUI 的世界观练完,下一节看它的前辈:UIKit 用一串按时间报到的回调管理界面一生——控制感更强、样板也更多,两种世界观都要会。