.onDisappear: 動作原理とSwiftUIでの使用

著者: IT Sectr 公開日: 2026-06-26 読了時間: 10 分

.onDisappearは、インターフェース階層からViewが削除されたときにクロージャを実行するSwiftUIの修飾子です。呼び出しは、画面を閉じるとき、タブを切り替えるとき、モーダルウィンドウを閉じるとき、または要素が表示範囲外にスクロールされたときに発生します。Apple Developer Documentation(2026)によると、onDisappearはクラッシュシナリオやシステムのウォッチドッグによるアプリ終了時に呼び出しを保証しません。SwiftUIの詳細はSwiftUIの記事をご覧ください。

重要なポイント

  • .onDisappear — Viewが非表示になるときにコードを実行するSwiftUIの修飾子。
  • 対称性 — onDisappearはonAppearと対になっており、逆順で呼び出されます。
  • リソースのクリーンアップ — 主なシナリオ:タイマーのキャンセル、接続の切断、状態の保存。
  • 分析 — onDisappearを使用して画面クローズ、滞在時間、セッション終了のイベントを送信します。
  • 保証なし — アプリがクラッシュした場合、onDisappearは呼び出されません。

.onDisappearとは?

.onDisappearは、Voidクロージャを受け取り、Viewが階層から削除されたときに実行するSwiftUIのView修飾子です。.onAppearとともに、画面の完全なライフサイクル(表示→作業→非表示)を形成します。AppleはonDisappearをSwiftUIとともにiOS 13でUIKitのviewDidDisappearの類似機能として導入しました。

構文的に.onDisappearはonAppearと同じです。任意のViewを修飾し、SwiftUIレンダラーがViewの削除時に呼び出すクロージャをアタッチします。遷移アニメーションの完了後にのみviewDidDisappearが作動するUIKitとは異なり、SwiftUIのonDisappearはアニメーションが終了する前でも呼び出される可能性があります。

onDisappearの構文

基本構文はonAppearと同じく簡潔です。修飾子は追加パラメータを取らず、メインスレッドで同期的に実行されるクロージャのみを受け取ります。

swift
struct DetailView: View {
    var body: some View {
        Text("Detail Screen")
            .onDisappear {
                print("DetailViewが画面から消えました")
            }
    }
}

.onDisappearの仕組み

呼び出しメカニズムはonAppearの逆です。子要素が最初にonDisappearを受け取り、次に親が受け取ります。このchild-firstルールにより、親のリソースが解放される前に子のリソースが解放されます。子要素が親のデータに依存する場合、親のコンテキストなしで正しく終了できる必要があります。

SwiftUIは、Viewがレンダリンググラフから削除されるとonDisappearを呼び出します。トリガーには、NavigationStackからのポップ、TabViewの切り替え、モーダル(sheet/fullScreenCover)の解除、条件付きでレンダリングされるView(if/switch)が含まれます。ListやScrollViewでは、セルがプリフェッチバッファの外にスクロールされたときにonDisappearが呼び出されます。

Child-first呼び出し順序

Child-first順序とは、VStackに3つの子Viewがある場合、最初に各子に対して逆順でonDisappearが呼び出され、次に親に対して呼び出されることを意味します。これは適切なクリーンアップのために重要です。親のViewModelが共有リソースを解放する前に、子のタイマーがキャンセルされます。

swift
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("Child \(id)")
            .onDisappear {
                print("Child \(id) onDisappear")
            }
    }
}

コンソール出力:Child B onDisappear、Child A onDisappear、Parent onDisappear — 最後。onAppearと比較して逆順になることで、正しいクリーンアップチェーンが保証されます。

.onDisappearが呼び出されるタイミング

呼び出しシナリオはコンテナの種類によって異なります。NavigationStackでは、pop-to-root、通常のpop-back、またはスワイプ(interactivePopGestureRecognizer)による画面解除時にonDisappearが作動します。TabViewでは、タブを切り替えると非表示のタブにonDisappearが、表示されるタブにonAppearが呼び出されます。両方の修飾子はほぼ同時に作動します。

SheetとfullScreenCoverでは、プログラムによる解除(@Environment(\.dismiss)経由)または下スワイプジェスチャーによってonDisappearが呼び出されます。重要な詳細:sheetが開かれていてもユーザーがアプリを切り替えた場合、実際に閉じられるまでonDisappearは呼び出されません。

シナリオonDisappearの呼び出し備考
NavigationStackのPopはいアニメーション直後
TabViewのタブ切替はい現在のタブ
Sheetの解除はいアニメーション完了前
Listのスクロールはいセルがプリフェッチ領域外に
アプリの最小化いいえ呼び出し保証なし
クラッシュ/watchdog killいいえ呼び出されない

.onDisappearの例

主なユースケースはリソースのクリーンアップ、状態の保存、トラッキングです。onAppearとは異なり、onDisappearのタスクは終了時に実行され、Viewは一度だけ非表示になるため重複チェックは不要です。

状態の保存とタイマーのキャンセル

状態の保存は、画面を離れるときにデータを保存する必要があるフォームで特に便利です。タイマーとCombineサブスクリプションは、画面に戻ったときのリークを防ぐためにonDisappearでキャンセルされます。

swift
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")
    }
}

タイマーのキャンセルにより、画面が閉じられた後のコード実行を防ぎます。キャンセルしないと、タイマーが現在のViewに属さなくなった@Stateを更新しようとし、実行時警告が発生する可能性があります。

画面滞在時間のトラッキング

画面滞在時間は古典的なトラッキングシナリオです。onAppearで時間を記録し、onDisappearで差分を計算してセッション時間を含む分析イベントを送信します。

swift
struct TrackedView: View {
    @State private var appearTime: Date?
    
    var body: some View {
        Text("Tracked Screen")
            .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 vs .onAppear — 違い

主な違いは呼び出し順序と実行保証です。onAppearはViewが階層に追加されるときに呼び出され、再作成時に再度呼び出される可能性があります。onDisappearは削除時に呼び出され、通常のクローズ時にのみ作動が保証され、クラッシュシナリオでは保証されません。

WWDC 2024によると、AppleはonDisappearをクリーンアップポイントとして扱い、データコミットポイントとしては扱わないことを推奨しています。重要なデータ(支払い、登録)はViewが非表示になる瞬間ではなくリアルタイムで保存する必要があります。アプリが最小化された場合、onDisappearは保証されないからです。

特性.onAppear.onDisappear
呼び出しタイミングViewが階層に追加Viewが階層から削除
順序Parent-firstChild-first
保証高いクラッシュ時はなし
主なタスク初期化クリーンアップ
再呼び出しView再作成時非表示ごとに1回

推奨:onDisappearは重要でないクリーンアップとトラッキングにのみ使用してください。データの永続化にはAppDelegateのscenePhaseまたはapplicationWillTerminate通知を使用してください。

典型的な使用シナリオ

シナリオ1:YouTubeのような再生。動画詳細画面で、onDisappearが再生位置をUserDefaultsに保存します。再開時にonAppearがUserDefaultsから位置を復元し、継続的な視聴体験を提供します。

シナリオ2:Combineサブスクリプションのキャンセル。ViewModelがCombineパブリッシャーを使用する場合、onDisappearがcancellable?.cancel()でサブスクリプションをキャンセルします。これにより、画面を離れた後のUI更新を防ぎます。これはページネーションを含むリストや検索クエリで特に重要です。

シナリオ3:WebSocketの切断。リアルタイム接続を使用するアプリ(メッセンジャー、相場フィード)では、onDisappearがバッテリーとデータを節約するためにWebSocket接続を閉じます。画面に戻るときにonAppearで再接続が行われます。

onDisappearでのWebSocket切断

WebSocketは、画面を離れるときに閉じる必要がある典型的なリソースです。以下の例では、onDisappearがソケットを切断し、onAppearが再接続します。これにより、複数の画面を持つアプリで大幅なデータ節約が実現します。

swift
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の場合、これにより切断-再接続のサイクルが発生し、不要な負荷が生じる可能性があります。解決策は遅延を使用するか、次の画面でソケットが必要かどうかを確認することです。

よくある質問

.onDisappearが呼び出されないことはありますか?

はい、.onDisappearはアプリのクラッシュ(crash、watchdog kill)、画面を閉じずにアプリを最小化する場合、またはアプリがバックグラウンドで終了されるシステムシナリオでは呼び出しを保証しません。重要なデータにはscenePhaseまたはapplicationWillTerminateを使用してください。

.onDisappearと.scenePhaseの違いは?

.onDisappearはViewが階層から削除されたときに作動する、特定の画面に対するローカルコールバックです。.scenePhase(@Environment(\.scenePhase)経由)はアプリ全体の状態(active、inactive、background)が変化したときに作動します。画面時間のトラッキングにはonAppear+onDisappearを、グローバルな状態の永続化にはscenePhaseを使用してください。

.onDisappearが連続して複数回呼び出されるのはなぜ?

原因はタブの高速切り替えや同じ画面の繰り返しpush/popです。SwiftUIが新しいViewインスタンスを作成し、古いものを削除し、再度作成することがあり、そのたびにonDisappearとonAppearが呼び出されます。.id()、.equatable()を使用していないか、親のbodyでViewを再作成していないか確認してください。

macOSのSwiftUIで.onDisappearは動作しますか?

はい、.onDisappearはmacOS(10.15+)で完全にサポートされており、同じ動作をします。ウィンドウを閉じる、スプリットビューパネルを削除する、モーダルを解除するときに呼び出されます。macOSでは、onDisappearはウィンドウを閉じるだけでなく非表示にするときにも呼び出されるため、macOSアプリではこれを考慮することが重要です。

.onDisappearでURLSessionTaskをキャンセルするには?

@StateにURLSessionTaskへの参照を保存し、onDisappearでtask.cancel()を呼び出します。代替として、Viewが非表示になると自動的に非同期操作をキャンセルする.task修飾子を使用します。.taskはURLSessionを含むすべての非同期操作に推奨されます。

まとめ

  • .onDisappear — View非表示時のリソースクリーンアップとトラッキングのためのonAppearと対の修飾子。
  • Child-first順序 — 子Viewが親より先にonDisappearを受け取り、正しいクリーンアップチェーンを実現。
  • 呼び出し保証なし — クラッシュ、最小化、システム終了時にはonDisappearが作動しない可能性あり。
  • 主なユースケース — タイマーキャンセル、WebSocket切断、状態保存、滞在時間トラッキング。
  • 分析 — onDisappearはDate().timeIntervalSinceで計算された滞在時間とともにscreen_closeイベントを送信。
  • 非同期操作の場合 — Task {}を伴うonDisappearより.taskを使用することを推奨。.taskはリクエストを自動的にキャンセルするため。

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください