为什么 6 年后还要再聊 SwiftUI 生命周期?
2019 年 SwiftUI 发布时,我们像"翻译官"一样,把 UIKit 的 viewDidLoad
、viewWillAppear
强行映射到新框架。
如今苹果已围绕 SwiftUI 重新设计了应用级生命周期(App
、Scene
、WindowGroup
),UIKit 反而成了"遗留项目"。
再啃一遍纯 SwiftUI 视角的视图生命周期,已是刚需,而非可选项。
视图一生的"时间轴"总览
阶段 | 触发时机 | 同步/异步 | 典型用途 |
---|---|---|---|
init | 结构体创建 | 同步 | 初始化常量、属性默认值 |
task | 初始化后、appear 前 | 异步 | 网络请求、数据库操作、初始化重量级 Observable |
onAppear | 已渲染到屏幕 | 同步 | 滚动定位、几何计算、埋点统计 |
状态变更 | 任意时刻 | 同步 | @State / @Binding 驱动 body 重新计算 |
onDisappear | 即将离开屏幕 | 同步 | 停止定时器、手动取消网络任务 |
Observable 的 deinit | 引用计数=0(实例销毁时) | 同步 | "最后遗言":写日志、发通知(使用场景较少) |
逐阶段深度拆解
- init ------ 结构体的"出生证"
swift
struct DetailView: View {
let postID: Int
@State private var likes: Int
// 自定义初始化:可以注入常量、设置默认值
init(postID: Int) {
self.postID = postID
_likes = State(wrappedValue: 0) // 手动包裹 State
print("【init】postID=\(postID)")
}
var body: some View { EmptyView() }
}
注意
- View 是 struct,没有真正的"析构",init 可能被频繁调用(body 评估时)。
- 只放轻量级代码;重量级对象请放到
task
或@StateObject
里。
- task ------ 苹果推荐的"异步入口"
swift
var body: some View {
Text("Hello")
.task { // ✅ 异步闭包,自动管理生命周期
await loadData()
}
.task(priority: .userInitiated) { // 可指定优先级
await loadImage()
}
}
特性
- 在 appear 之前、init 之后 执行。
- 与视图生命周期强绑定:视图消失时,协程自动取消(
Task.isCancelled = true
)。 - 苹果官方推荐:把重量级 Observable 初始化放这里,避免阻塞主线程。
- onAppear ------ 屏幕亮起的"哨兵"
swift
ScrollView {
LazyVStack {
ForEach(items) { item in
RowView(item)
.onAppear {
// 滚动到指定 index
if item.id == targetID {
proxy.scrollTo(item.id, anchor: .center)
}
}
}
}
}
适用场景
- 需要几何信息已就绪(
GeometryReader
已布局)。 - 埋点、Analytics、页面追踪。
- 不能执行长时间阻塞任务,否则卡 UI。
- 状态与数据流 ------ 视图"心跳"的发动机
swift
struct Counter: View {
@State private var count = 0 // 局部
@Binding var globalTitle: String // 父级
@Environment(\.colorScheme) var scheme // 系统
var body: some View {
Button("+\(count)") {
count += 1
globalTitle = "已点 \(count) 次"
}
.foregroundColor(scheme == .dark ? .yellow : .black)
}
}
规则
- 任何
@State
/@Binding
/@Observable
变更都会触发 body 重新求值。 - SwiftUI 采用细粒度 diff,只刷新变化的子树。
- 不要在 body 里产生副作用(网络、文件 IO),副作用放到
task
/onAppear
里。
- onDisappear ------ 离开舞台的"幕布"
swift
struct PlayerView: View {
@StateObject private var player = VideoPlayer()
var body: some View {
VideoPlayerView()
.onDisappear {
player.pause() // 手动暂停
saveWatchProgress() // 持久化进度
}
}
}
注意
- 不保证视图被销毁(SwiftUI 可能缓存)。
- 若需要真正释放资源,请把清理逻辑放到
ObservableObject
的deinit
。
- Observable 的 deinit ------ 结构体没有"遗言",但类有
swift
final class VideoModel: ObservableObject {
@Published var progress: Double = 0
deinit {
// 最后广播
NotificationCenter.default.post(name: .playerDead, object: progress)
print("【deinit】最后进度=\(progress)")
}
}
使用场景
- 必须在对象释放前发送消息、写日志、刷磁盘。
- 因为 View 是 struct,没有 deinit,所以把"遗言"托管给引用类型。
完整示例:一个"会呼吸"的详情页
swift
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
DetailView1(postID: 42)
}
}
}
struct DetailView1: View {
let postID: Int
@State private var uiModel: Post? = nil
@State private var isLoading = true
// 1. 初始化
init(postID: Int) {
self.postID = postID
print("【init】")
}
var body: some View {
Group {
if isLoading {
ProgressView("加载中...")
} else if let data = uiModel {
ScrollView {
VStack(alignment: .leading) {
Text(data.title).font(.title)
Text(data.body)
}
.padding()
}
}
}
.task { // 2. 异步拉数据
print("【task】")
await fetchPost()
}
.onAppear { // 3. 埋点
print("【onAppear】")
}
.onDisappear { // 4. 清理
print("【onDisappear】")
URLSession.shared.invalidateAndCancel()
}
}
func fetchPost() async {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts/\(postID)") else { return }
do {
let (data, _) = try await URLSession.shared.data(from: url)
uiModel = try JSONDecoder().decode(Post.self, from: data)
isLoading = false
} catch {
// 简化错误处理
isLoading = false
}
}
}
struct Post: Decodable {
let title: String
let body: String
}
运行日志
csharp
【init】
【onAppear】
【task】
常见陷阱 & 调试技巧
-
init 被反复调用
在 body 里
print("init")
会发现日志刷屏 → 说明你把重逻辑放进了 init,应迁移到task
。 -
onDisappear 不调用
视图被嵌在
TabView
或NavigationStack
里,可能被缓存;依赖onDisappear
做"必须释放"逻辑时要谨慎。 -
task 不会重复执行
同一视图实例复用时,task 只跑第一次;若需要"每次出现都拉新数据",用
.id(uuid)
强制重建视图,或监听onAppear
。
扩展场景:把生命周期变成"业务事件"
-
预加载 + 缓存
在
task
里拉取下一页数据,用NSCache
缓存,用户滑到下一页时零等待。 -
权限申请
相机、相册权限首次弹窗放在
onAppear
,用户拒绝后下次进入再次引导。 -
屏幕旋转适配
在
onAppear
记录当前UIWindowScene.interfaceOrientation
,旋转后对比,决定是否重建布局。 -
SwiftData 自动保存
把
@Model
的save()
放在onDisappear
,用户离开页面前落盘,比scenePhase
更细粒度。
写在最后
6 年前我们拿 UIKit 的尺子量 SwiftUI,今天已经可以把尺子扔掉------SwiftUI 给了我们更少模板、更直观、更声明式的生命周期,
但也要求我们更自律:"轻 init、重 task、慎清理、勤递归最小状态。"
当你不再追问"viewDidLoad 在哪",而是自然而然把网络请求塞进 task
, 把清理逻辑交给 onDisappear
, 就真正从UIKit 移民变成了SwiftUI 原住民。
愿我们都能写出"呼吸感"十足的代码:该出现时出现,该消失时消失,该干活时绝不卡屏。