Skip to content
配图

onAppear、task 与容器语义

参考 SwiftyPlace 对 View Lifecycle 的拆解:生产 bug 的根源常是把 onAppear 当成 viewDidLoad,而 SwiftUI 里二者不对等。

四条轨道(建议分开记)

轨道问题
结构节点是否在树里(if/else、pop)
身份是否是「同一个」逻辑实例(.id
可见性用户是否看到(Tab 切换、cover)
数据@State / Store 是否仍存活

onAppear 横跨 结构 + 可见性onDisappear 同理。
@State 生死 只跟 结构 + 身份 走。

应用级数据加载放哪

优先:.task(id:)

swift
struct FeedView: View {
    let userId: String
    @State private var posts: [Post] = []

    var body: some View {
        List(posts) { ... }
            .task(id: userId) {
                posts = await api.feed(userId: userId)
            }
    }
}
  • userId 变 → 取消旧任务、重新加载
  • View 从树移除 → 自动 cancel
  • onAppear + 手动 Task 更少泄漏

慎用:裸 onAppear

swift
.onAppear {
    Task { await load() }  // 无自动 cancel;重复 appear 可能并发多次
}

若必须用,配合 taskId 标志或 async let 去重。

容器 cheat sheet(生产向)

容器onAppear 典型时机@State 何时没
if进入分支离开分支
sheet 展示sheet 出现dismiss
TabView首次选中 tab(iOS 18+ 懒加载)极少因切换 tab 销毁
NavigationStack push目标页入场pop
List / LazyVStackcell 入屏滚出回收可能丢

TabView iOS 17:未选中的 tab 也可能在启动时 onAppear → 后台 tab 里写 fetch() 会在启动就打 API。支持 17 要实测。

push DetailView

  • onAppear:每次显示都会调(含从子页 pop 回来)
  • 若只要「首次加载」,用 @State private var didLoad 或把数据放在 不因 pop 销毁 的 Store
swift
.task(id: item.id) { detail = await api.detail(item.id) }

pop 再 push 同一 item.id:task 可能 不重启(身份相同)——要强制刷新用 id: "\(item.id)-\(refreshToken)"

List 预取 vs 重复请求

swift
List(items) { item in
    Row(item: item)
        .task { await prefetch(item) }
}

快速滚动 → 多个 .task 并发 → 需要 取消友好prefetch(检查 Task.isCancelled)。

fullScreenCover 与 Alert

fullScreenCover 内容子树的 onAppear 与底层页 并存;底层 onDisappear 不一定 调用。
不要在 cover 打开时假设底层已「消失」。

与 UIKit 混用

UIViewControllerRepresentable 里:

  • updateUIViewController 可能极频繁
  • 应用 makeCoordinator + context.coordinator 做一次性 setup
  • 数据加载仍建议放在 SwiftUI 层 .task,而非 updateUIViewController

调试清单

  1. 打 log:onAppear / onDisappear / deinit(class)
  2. 是否缺 id 导致 List 复用错行
  3. Instrument Time Profiler + SwiftUI 看重复 body
  4. .task(id:) 替换散落的 onAppear Task

延伸阅读


容器行为以 Apple Release Notes 为准,跨版本务必回归。

Visitors · Page views