markdown

はじめに (対象読者・この記事でわかること)

この記事は、SwiftUIでDeepLink(カスタムURLスキームやUniversal Link)を扱いたいが、onOpenURLで受け取ったURLを別の画面(NavigationStackの階層や.sheet/.fullScreenCoverで表示したView)に渡せずに困っている開発者向けです。

この記事を読むことで、以下のことがわかります。 - onOpenURLで受け取ったURLを、なぜ別画面に渡せないのか - 環境オブジェクトやビューモデルを使った、確実にURLを届ける方法 - 実装時にハマりやすいポイントと、それを回避するためのベストプラクティス

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - SwiftUIの基本的な画面遷移(NavigationStack、.sheet、.fullScreenCover) - ObservableObjectと@StateObject/@ObservedObjectの使い方 - URLスキームの登録方法(Info.plist or Xcode Target設定)

onOpenURLで受け取ったURLが「消える」理由

SwiftUI 3.0(iOS 16)以降、onOpenURLは「アプリがURLを受け取った瞬間」に呼ばれる独立したイベントです。
つまり、onOpenURL

  • 現在アクティブなWindow(Scene)に対して
  • 現在表示中のViewに対して

一度だけ呼ばれます。
しかし、実装の多くは「URLを受け取ったら特定の画面を開く」という流れになるため、以下のようなコードを書いてしまいがちです。

Swift
@main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() .onOpenURL { url in // ❌ ここでNavigationStackのPathを更新しても // すでに別の画面が表示されていると届かない Router.shared.navigate(to: .detail(url)) } } } }

このコードの問題点は、.onOpenURLが呼ばれた瞬間にContentViewが表示されていない(=別の画面が.sheetで覆われている)と、URLがそのまま消失してしまうことです。

確実にURLを届ける実装パターン

環境オブジェクトを使った「URLを一旦キャッチし、どの画面でも参照できるようにする」方法が最も確実です。

ステップ1:URLを保持する専用のモデルを用意する

Swift
final class DeepLinkHandler: ObservableObject { static let shared = DeepLinkHandler() @Published var pendingURL: URL? private init() {} func set(_ url: URL) { pendingURL = url } func consume() -> URL? { defer { pendingURL = nil } return pendingURL } }

consume()で「受け取ったらクリア」する設計にすることで、同じURLを2回処理することを防げます。

ステップ2:Appエントリポイントで受け取り、保管する

Swift
@main struct MyApp: App { @StateObject private var handler = DeepLinkHandler.shared var body: some Scene { WindowGroup { ContentView() .onOpenURL { url in // ① 一旦保管 DeepLinkHandler.shared.set(url) } .environmentObject(handler) // ② 環境オブジェクトとして注入 } } }

ステップ3:各画面でURLを監視し、必要なら処理する

Swift
struct DetailView: View { @EnvironmentObject var handler: DeepLinkHandler @State private var showSheet = false var body: some View { VStack { Text("Detail") } .onAppear { if let url = handler.consume() { // URLが届いていたら即実行 showSheet = true } } .sheet(isPresented: $showSheet) { DeepLinkHandlerView(url: handler.pendingURL) } } }

onAppearで明示的にconsume()を呼ぶことで、画面が表示されたタイミングで確実にURLを取得できます。

ハマった点とエラー解決

現象 原因 解決策
「.sheetが開かない」 URLを@Stateで持っていたため、再描画で初期化されていた 環境オブジェクトで保持
「2回目のDeepLinkが効かない」 前回のURLが残ったままだった consume()で必ずnilにする
「NavigationStackのpathに追加しても遷移しない」 遷移先がsheetで覆われていた sheetを閉じてからpath操作

解決策

上表の通り、「URLを@Stateで保持しない」「必ずconsumeでクリアする」「画面が表示されてから処理する」の3点を守ることで、ほぼ100%確実にDeepLinkを処理できます。

まとめ

本記事では、SwiftUIのonOpenURLで受け取ったDeepLinkを別画面に届ける方法を解決しました。

  • onOpenURLは独立したイベントであり、単純にNavigationStackを操作しても届かない
  • 環境オブジェクト(または@StateObject)を使って「URLを一旦キャッチ&保持」する設計が最も確実
  • 各画面でonAppeartaskでURLを監視し、必要なら即処理する

この記事を通して、DeepLinkを「見えない画面」に渡してしまうという落とし穴を回避でき、ユーザビリティの高いアプリを実装できるようになります。
次回は、SwiftDataと組み合わせた永続化パターンや、WatchOSでのDeepLink連携について深掘りする予定です。

参考資料