.onDisappear — 一个SwiftUI修饰符,当View从界面层次结构中移除时执行闭包。调用发生在关闭屏幕、切换标签页、dismiss模态窗口或滚动元素超出可见区域时。根据Apple Developer Documentation(2026),onDisappear在应用程序紧急终止或被system-watchdog销毁的场景下不保证调用。关于SwiftUI的更多信息,请阅读SwiftUI相关材料。
要点
.onDisappear — SwiftUI中的一个View修饰符,接受Void闭包并在View从层次结构中移除时执行。与.onAppear一起构成屏幕的完整生命周期:出现 — 运行 — 消失。Apple在iOS 13中随SwiftUI的发布引入了onDisappear,作为UIKit中viewDidDisappear的对应物。
在语法上.onDisappear与onAppear相同:修饰任何View并附加一个闭包,该闭包在视图被移除时由SwiftUI组件调用。与UIKit不同,viewDidDisappear仅在过渡动画完成后才触发,SwiftUI中的onDisappear可能在动画完成之前被调用——在View被标记为移除的时刻。
基本语法onDisappear与onAppear一样简洁。修饰符不接受额外参数——仅接受在主线程同步执行的闭包。
struct DetailView: View {
var body: some View {
Text("详情屏幕")
.onDisappear {
print("DetailView已从屏幕上消失")
}
}
}
调用机制onDisappear与onAppear相反:首先子元素接收onDisappear,然后是父元素。这种child-first规则保证子元素的资源在父元素资源之前被释放。如果子元素依赖于父元素的数据,它必须能够在没有父元素上下文的情况下正确终止。
SwiftUI在View被排除出渲染图时调用onDisappear。触发器可以是:从NavigationStack中pop、切换TabView、dismiss模态窗口(sheet/fullScreenCover)、条件显示View的变化(if/switch)。在List和ScrollView中,当单元格滚动出prefetch缓冲区时调用onDisappear。
Child-first顺序意味着如果VStack中有三个子View,onDisappear将首先以相反顺序为每个子元素调用,然后为父元素调用。这对于正确清理至关重要:子定时器在父ViewModel释放共享资源之前被取消。
struct ParentView: View {
var body: some View {
VStack {
ChildView(id: "A")
ChildView(id: "B")
}
.onDisappear {
print("Parent onDisappear — 最后")
}
}
}
struct ChildView: View {
let id: String
var body: some View {
Text("子节点 \(id)")
.onDisappear {
print("子节点 \(id) onDisappear")
}
}
}
控制台输出:Child B onDisappear, Child A onDisappear, Parent onDisappear — 最后。与onAppear相比的相反顺序保证了正确的释放链。
调用场景onDisappear取决于容器的类型。在NavigationStack中,onDisappear在pop-to-root、普通pop-back或通过滑动手势移除屏幕(interactivePopGestureRecognizer)时触发。在TabView中,切换标签页为隐藏的触发onDisappear,为显示的触发onAppear——两个修饰符几乎同时触发。
在Sheet和fullScreenCover中,onDisappear在程序性dismiss(通过@Environment(\.dismiss))或向下滑动手势时调用。重要特性:如果sheet已打开但用户切换了应用程序,onDisappear在实际关闭之前不会被调用。
| 场景 | onDisappear是否调用 | 备注 |
|---|---|---|
| NavigationStack中的Pop | 是 | 动画后立即 |
| TabView标签切换 | 是 | 当前标签 |
| Dismiss sheet | 是 | 动画完成前 |
| List中滚动 | 是 | 单元格超出prefetch区域 |
| 应用最小化 | 否 | 不保证调用 |
| Crash/watchdog kill | 否 | 不会被调用 |
主要场景onDisappear — 资源清理、状态保存和跟踪。与onAppear不同,onDisappear任务在退出时执行,不需要检查重复,因为View只消失一次。
保存状态在onDisappear中对于表单特别有用,数据在离开屏幕时必须保存。定时器和Combine订阅在onDisappear中被取消,以避免返回屏幕时发生内存泄漏。
struct FormView: View {
@State private var draftText = ""
@State private var timer: Timer?
var body: some View {
TextField("Enter text", text: $draftText)
.onAppear {
timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { _ in
saveDraft()
}
}
.onDisappear {
timer?.invalidate()
timer = nil
saveDraft()
}
}
private func saveDraft() {
UserDefaults.standard.set(draftText, forKey: "draft")
}
}
取消定时器在onDisappear中防止屏幕关闭后代码执行。如果不取消,定时器可能尝试更新不再属于当前View的@State,导致运行时警告。
停留时长在屏幕上 — 经典的跟踪场景。我们在onAppear中记录时间,在onDisappear中计算差值,并发送带有会话持续时间的分析事件。
struct TrackedView: View {
@State private var appearTime: Date?
var body: some View {
Text("被跟踪的屏幕")
.onAppear {
appearTime = Date()
Analytics.shared.logEvent("screen_view", params: ["screen": "TrackedView"])
}
.onDisappear {
if let start = appearTime {
let duration = Date().timeIntervalSince(start)
Analytics.shared.logEvent("screen_close", params: [
"screen": "TrackedView",
"duration_ms": Int(duration * 1000)
])
}
}
}
}
关键区别onDisappear与onAppear — 调用顺序和触发保证。onAppear在View添加到层次结构时调用,并具有在重新创建时重复调用的特性。onDisappear在移除时调用,仅在正常关闭时保证触发,但在紧急场景下不保证。
根据WWDC 2024,Apple建议将onDisappear视为清理点,而非数据保存点。关键重要数据(支付、注册)应实时保存,而不是在View消失时,因为onDisappear在应用程序最小化时不保证调用。
| 特性 | .onAppear | .onDisappear |
|---|---|---|
| 调用时刻 | View添加到层次结构 | View从层次结构移除 |
| 顺序 | Parent-first | Child-first |
| 保证 | 高 | 紧急终止时不保证 |
| 主要任务 | 初始化 | 清理 |
| 重复调用 | View重新创建时 | 每次消失一次 |
建议:仅在非关键清理和跟踪时使用onDisappear。保存数据请使用scenePhase或AppDelegate中的applicationWillTerminate通知。
场景1:类似YouTube的加载。在视频详情屏幕上,onDisappear将播放位置保存到UserDefaults。重新打开时,onAppear从UserDefaults恢复位置,产生连续观看的效果。
场景2:取消Combine订阅。如果ViewModel使用Combine发布者,onDisappear通过cancellable?.cancel()取消订阅。这防止了离开屏幕后的UI更新,对于分页列表和搜索查询尤为重要。
场景3:关闭WebSocket。在具有实时连接的应用程序中(即时通讯工具、行情信息流),onDisappear关闭WebSocket连接以节省电池和流量。重新打开在返回屏幕时的onAppear中完成。
WebSocket — 一种在离开屏幕时需要关闭的典型资源。在下面的示例中,onDisappear断开socket,onAppear重新连接它,为多屏幕应用程序提供显著的流量节省。
struct ChatView: View {
@StateObject private var socket = WebSocketManager()
var body: some View {
ChatListView(messages: socket.messages)
.onAppear {
socket.connect()
}
.onDisappear {
socket.disconnect()
}
}
}
重要:在TabView中切换标签页时,当前标签的onDisappear和下一标签的onAppear几乎同时触发。对于WebSocket,这可能导致disconnect-connect循环,产生额外负载。解决方案 — 使用延迟或检查下一个屏幕是否需要socket。
常见问题解答
是的,.onDisappear在应用程序紧急终止(crash、watchdog kill)、不关闭屏幕而最小化应用程序或应用程序在后台被销毁的系统场景下不保证调用。对于关键重要数据,请使用scenePhase或applicationWillTerminate。
.onDisappear在View从层次结构中被移除时触发 — 针对特定屏幕的本地回调。.scenePhase(通过@Environment(\.scenePhase))在整个应用程序状态改变时触发:active、inactive、background。跟踪屏幕时间使用onAppear+onDisappear,保存全局状态使用scenePhase。
原因 — 快速切换标签页或同一屏幕的重复push/pop。SwiftUI可能创建新的View实例、删除旧的、再次创建——每次都触发onDisappear和onAppear。检查您是否使用了.id()、.equatable(),或者在父级body中是否重新创建了View。
是的,.onDisappear在macOS(10.15+)上完全支持,行为相同:在关闭窗口、移除split view面板或dismiss模态窗口时调用。在macOS上,onDisappear不仅在关闭窗口时调用,在隐藏窗口(hide)时也会调用,这对macOS应用程序很重要。
将对URLSessionTask的引用保存在@State中,并在onDisappear中调用task.cancel()。或者使用.task修饰符,它在View消失时自动取消async操作。对于所有async操作(包括URLSession),优先使用.task。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。