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を保持する専用のモデルを用意する
Swiftfinal 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を監視し、必要なら処理する
Swiftstruct 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を一旦キャッチ&保持」する設計が最も確実
- 各画面で
onAppearやtaskでURLを監視し、必要なら即処理する
この記事を通して、DeepLinkを「見えない画面」に渡してしまうという落とし穴を回避でき、ユーザビリティの高いアプリを実装できるようになります。
次回は、SwiftDataと組み合わせた永続化パターンや、WatchOSでのDeepLink連携について深掘りする予定です。
参考資料
- Handling Universal Links in SwiftUI | Apple Developer Documentation
- SwiftUI NavigationStack Deep Dive – WWDC22
- onOpenURLの実装パターンまとめ | Zenn
