Skip to content
配图

视图身份与生命周期

SwiftyPlace 关于 onAppear 何时触发 的文章把问题拆成两条轨:节点生命周期(图里有没有这个 View)与 可见性(用户看不看得到)。很多「onAppear 乱了」其实是 身份容器策略 叠在一起。

两种身份来源

1. 结构身份(Structural Identity)

由 View 树 位置 决定:

swift
VStack {
    Header()
    if showDetail { Detail() }  // 分支切换 → 不同身份
    Footer()
}

if 从 false→true,Detail 首次插入 图;false 时节点被移除,其 @State 随之销毁(除非用别的容器保留)。

2. 显式身份(Explicit Identity)

swift
ChildView()
    .id(user.id)

user.id 变化 → SwiftUI 认为是 另一个 ChildView@State 重置。

ForEach 同理:

swift
ForEach(items) { item in
    Row(item: item)
}
// 依赖 item.id(Identifiable)稳定;用 indices 且无稳定 id 会整行闪烁重置

反模式ForEach(items.indices, id: \.self) 在删除中间元素时 id 错位,状态串台。

状态「还在」但 onDisappear fired?

常见场景:TabView 切走、 NavigationStack pop、List 滚出屏幕。

容器节点是否销毁onDisappear 含义
if/else 假分支销毁真消失
TabView(iOS 18+ 懒加载)未访问的 tab 可能未创建首次选中才 onAppear
NavigationStack pop销毁(除非用 .navigationDestination 特殊保留)pop 时触发
List / LazyVStack滚出可能销毁 cell可见性 + 回收

iOS 17 vs 18:TabView 是否在启动时 eager 构建所有 tab,会影响「后台 tab 的 onAppear 是否在启动时就跑」。支持 iOS 17 时要按 最低版本 测一遍。

设计模式:何时用 .id() 重置

合理用法

  • 登录用户切换,表单必须清空
  • 选中列表项变化,详情页完全重载
swift
DetailView(item: item)
    .id(item.id)

滥用:每次 body 里 UUID() 当 id → 每帧新身份 → 动画闪、输入框丢字、网络请求死循环。

与 @StateObject / @Observable 的边界

swift
struct Screen: View {
    @StateObject private var vm = ViewModel()
    var body: some View { ... }
}

@StateObject首次创建 绑在 View 身份上。身份被 .id() 换掉 → 新 ViewModel,旧订阅若未 cancel 会泄漏。

@Observable + @State 持有:

swift
@State private var vm = ViewModel()

同样受身份影响;换用户时应 显式 vm = ViewModel().id(userId)

工程 checklist

  1. 列表用 稳定业务 id,不用纯 index
  2. 条件分支切换前问:要不要保留子树状态?要则换 opacity/hidden 或上层 @State
  3. 数据加载放 .task(id:) 而非裸 onAppear,与身份解耦
  4. 深链进 NavigationStack 时,path 与 View 身份 一起设计(见 NavigationPath 深度

最小实验

swift
struct IdentityLab: View {
    @State private var flag = true
    @State private var counter = 0
    var body: some View {
        VStack {
            if flag {
                Child(counter: $counter).id("a")
            } else {
                Child(counter: $counter).id("b")
            }
            Button("Toggle branch") { flag.toggle() }
        }
    }
}

观察切换分支时 counter 是否归零;再去掉 .id 对比。

延伸阅读


容器行为随 iOS 版本变化,请在目标 deployment target 上实测。

Visitors · Page views