ネイティブモバイルアプリのディープリンク:ルート、トークン、アプリで開く挙動
ネイティブモバイルアプリのディープリンクを学ぶ:ルート設計、アプリで開く挙動の扱い方、KotlinやSwiftUIで煩雑なカスタムルーティングなしにトークンを安全に受け渡す方法。

ディープリンクに求められること(平易に)
電話でリンクをタップした人は、1つの結果を期待します:すぐに目的の場所に到達すること。近い場所ではなく、検索バーのあるホームでもなく、来た理由を忘れたログイン画面でもありません。
良いディープリンク体験は次のようになります:
- アプリがインストールされていれば、リンクが示す正確な画面を開く。
- アプリが未インストールでも、タップが役立つ(例:Webフォールバックやアプリストアを開き、インストール後に同じ目的地に戻せる)。
- ログインが必要なら一度だけログインして意図した画面に着地する。アプリの初期画面に戻されない。
- リンクが何らかのアクション(招待を受諾、注文を見る、メール確認など)を含むなら、そのアクションが明確で安全である。
多くのフラストレーションは「なんとなく動く」リンクから生じます。人は間違った画面を見たり、作業が失われたり、ループにハマったりします:リンクをタップ→ログイン→ダッシュボードに着地→再度リンクをタップ→再びログイン。たった一手間でもユーザーは諦めてしまう可能性が高く、特に招待やパスワードリセットのような一度きりの操作では致命的です。
KotlinやSwiftUIのコードを書く前に、リンクに何を意味させたいかを決めてください。外部からどの画面を開けるか?アプリが閉じている場合と動いている場合で何が変わるか?ログアウト中はどう振る舞うべきか?
この計画が後の多くの問題を防ぎます:明確なルート、予測可能な「アプリで開く」挙動、そしてシークレットをURLに直接置かずにワンタイムコードを安全に受け渡す方法です。
ディープリンクの種類と「アプリで開く」がうまくいかない理由
「アプリを開くリンク」は同じ振る舞いをするわけではありません。混同すると古典的な失敗に当たります:間違った場所を開く、ブラウザを開いてしまう、あるいは片方のプラットフォームでしか動かない。
よくある三つのカテゴリ:
- カスタムスキーム(例:myapp: のようなアプリ固有のスキーム)。設定は簡単だが、ブラウザや他アプリが慎重に扱うことがある。
- Universal Links(iOS)とApp Links(Android)。通常のWebリンクを使い、インストール済みならアプリを開き、未インストールならWebにフォールバックする。
- アプリ内ブラウザで開かれるリンク。メールアプリやメッセンジャーの組み込みブラウザで開かれるリンクは、SafariやChromeと異なる振る舞いをすることが多い。
「アプリで開く」は、タップされた場所によって意味が変わります。Safariでタップすると直接アプリにジャンプすることがありますが、同じリンクでもメールやメッセンジャー内だと組み込みのWebビューが開き、追加の「アプリで開く」ボタンを押さないと先に進まない場合があります。AndroidではChromeがApp Linksを尊重しても、あるソーシャルアプリ内ブラウザは無視することがあります。
さらに罠になるのがコールドスタートと既に動いている場合の違いです。
- コールドスタート: OSがアプリを起動し、アプリが初期化された後にディープリンクを受け取ります。スプラッシュや認証チェック、リモート設定読み込みがあると、リンクが失われないよう一時保存してアプリ準備後に再生する必要があります。
- 既に動いている場合: セッション中にリンクを受け取り、ナビゲーションスタックが既に存在するため、同じ目的地でも画面をプッシュするのかスタックをリセットするのかなど別扱いが必要です。
単純な例:Telegramからタップした招待リンクはしばしばアプリ内ブラウザで開かれます。OSが常に直接引き渡す前提で作っていると、ユーザーはWebページを見てリンクが壊れていると判断することがあります。こうした環境を事前に想定しておけば、後でプラットフォーム特有の接着コードを書く量を減らせます。
実装前にルートを計画する
多くのディープリンクバグはKotlinやSwiftUIの問題ではなく、計画不足です。リンクが一つの画面にきれいに対応していない、あるいは「かもしれない」オプションをたくさん抱えていると失敗します。
人がアプリをどう考えるかに合わせた一貫したルートパターンから始めてください:一覧、詳細、設定、チェックアウト、招待など。読みやすく安定したパターンにしておくと、メール、QRコード、Webページで再利用しやすくなります。
シンプルなルートセット例:
- Home
- Orders一覧 と Order詳細(orderId)
- アカウント設定
- 招待受諾(inviteId)
- 検索(query, tab)
次にパラメータを定義します:
- 単一オブジェクトにはIDを使う(orderId)。
- UI状態はオプションパラメータにする(tab、filter)。
- 各リンクに対して一番適切なデフォルトを決める。
また、リンクが間違っている場合の挙動も決めてください:データがない、IDが無効、ユーザーがアクセスできないコンテンツなど。安全な既定値は近い安定した画面(一覧など)を開き、短いメッセージを表示することです。白紙の画面や文脈のないログイン画面に放り込むのは避けてください。
最後に送り元ごとに計画します。QRコードは短いルートで高速に開ける必要があり、接続が不安定でも許容されるべきです。メールリンクはより長く詳細なコンテキストを含められます。Webページリンクはアプリ未インストール時でもうまく劣化(degrade)して、次に何をすればよいか説明できるべきです。
バックエンド駆動のアプローチ(例:APIエンドポイントや画面を生成するプラットフォームを使う)なら、このルート計画は共有の契約になります:アプリはどこに行くかを知り、バックエンドはどのIDや状態が有効かを知る。
URLにシークレットを置かない安全なトークン受け渡し
ディープリンクは安全な封筒のように扱われがちですが、実際はそうではありません。URL内の何かはブラウザ履歴、スクリーンショット、共有プレビュー、解析ログ、コピー先などに残り得ます。
URLにシークレットを置かないでください。長期有効なアクセストークン、リフレッシュトークン、パスワード、個人データなど、リンクが転送されたときにその人として振る舞えるものは入れてはいけません。
より安全なパターンは短時間有効なワンタイムコードです。リンクにはそのコードだけを入れ、アプリが開いた後に実際のセッションと引き換える方式にします。コードは盗まれても数分以内、あるいは一度使われたら無効になるべきです。
単純な受け渡しフロー:
- リンクにはワンタイムコードのみを含める(セッショントークンではない)。
- アプリが開き、バックエンドにそのコードを交換(redeem)するリクエストを送る。
- バックエンドは有効期限と未使用であることを検証し、使用済みにマークする。
- バックエンドは通常の認証セッションを返す。
- アプリはコードを引き換えたらメモリから消す。
引き換え後も、機微な操作の前には本人確認を行ってください。支払い承認、メール変更、データエクスポートなど重要な操作は、簡単な生体認証や再ログインを要求するべきです。
結果として得たセッションは安全に保存してください。iOSでは通常Keychain、AndroidではKeystore連携のストレージを使います。必要な情報だけを保存し、ログアウトやアカウント削除、疑わしい再利用検出時には消去してください。
具体例:ワークスペース招待リンクを送る場合、リンクは有効期限10分のワンタイムコードを持ち、アプリはそれを引き換えた後に「どのワークスペースに参加するか」を明確に表示してユーザーに確認させます。AppMasterを使っているなら、この動作はコードを引き換えるエンドポイントとモバイルUIの確認ステップに自然にマップできます。
認証と「続きから再開」
ディープリンクはしばしばプライベートデータを含む画面を指します。どれが誰でも開けるか(公開)で、どれがログイン必須か(保護済み)を先に決めてください。この一つの決定で多くの「テストでは動いた」の驚きを防げます。
単純なルール:まず安全なランディング状態を開き、ユーザー認証を確認してから保護された画面に移動するようにします。
公開と保護の決定
ディープリンクは誤って他人に転送され得ると考えてください。
- 公開: マーケティングページ、ヘルプ記事、パスワードリセット開始、招待受諾の開始(まだデータを表示しない)
- 保護: 注文詳細、メッセージ、アカウント設定、管理画面
- 混在: プレビュー画面は非機密のプレースホルダだけを表示し、ログイン後に完全な内容を見せる
ログイン後に元の場所に戻す
信頼できる方法は:リンクを解析して意図した目的地を保存し、認証状態に応じてルーティングすることです。
例:サポートチケットへの「アプリで開く」リンクをログアウト状態でタップした場合、アプリは中立的な画面を開いてログインを促し、ログイン後に自動的にそのチケットへ移動します。
確実にするために、小さな「戻り先」データ(ルート名とチケットIDなど)をローカルに短時間保存し、ログイン完了後に一度だけ読み出して遷移し、使ったら削除します。ログイン失敗や期限切れの場合は安全なホーム画面にフォールバックします。
次のようなエッジケースも丁寧に扱ってください:
- セッション期限切れ: 短いメッセージを出して再認証し、続行する。
- 権限剥奪: 目的地のシェルを開き「アクセス権がありません」と表示して次の安全なステップを案内する。
また、ロックスクリーンのプレビューやアプリスイッチャーのサムネイル、通知のプレビューにプライベートデータを表示しないでください。データ読み込みとセッション検証が終わるまでは機微な画面を空白にしておくと良いです。
カスタムなナビゲーションスパゲッティを避けるルーティング手法
各画面が独自にURLを解析し始めるとディープリンクはめんどくさくなります。パラメータの取り扱いや必須/オプションの判断がアプリ全体に分散してしまい、安全に変更できなくなります。
ルーティングを共通の配管(plumbing)と考え、1つのルートテーブルと1つのパーサーを用意し、UIにはクリーンな入力を渡すようにしましょう。
1つの共通ルートテーブルを使う
iOSとAndroidで同じ、人が読めるルートの一覧に合意してください。これを契約(contract)と見なします。
各ルートは次のものにマップされます:
- 画面、および
- 小さな入力モデル
例:「注文詳細」はOrder画面にマップされ、入力は OrderRouteInput(id) のようになります。参照元など追加値が必要なら、それらは入力モデルに含め、ビューコードに散らばらないようにします。
解析と検証を中央にまとめる
パース、デコード、検証は1箇所で行ってください。UIが「このトークンはあるか?」や「このIDは有効か?」と尋ねるべきではありません。UIは有効なルート入力か明確なエラーステートを受け取るべきです。
実用的なフロー:
- URLを受け取る(タップ、スキャン、共有シートなど)
- 既知のルートに解析する
- 必要なフィールドと形式を検証する
- 画面ターゲットと入力モデルを生成する
- 単一のエントリポイントからナビゲートする
「不明なリンク」用のフォールバック画面を用意してください。行き止まりにしないで、有用なものにします:開けなかった内容を示し、簡単な言葉で理由を説明し、Homeへ戻る、検索する、サインインするなどの次の行動を提示します。
ステップバイステップ:ディープリンクと「アプリで開く」挙動の設計
良いディープリンクは最高に退屈に感じられます。人がタップして常に正しい画面に着地するからです。
ステップ1:重要なエントリポイントを選ぶ
実際に使われる上位10個のリンクタイプをリストアップしてください:招待、パスワードリセット、注文レシート、チケット表示、プロモーションリンクなど。意図的に小さく絞ることが重要です。
ステップ2:パターンを契約として書く
各エントリポイントに対して、1つの正規パターンと正しい画面を開くための最小限のデータを定義します。名前ではなく安定したIDを優先し、必須とオプションを決めます。
役立つルール:
- ルートは1つの目的ごとに(invite, reset, receipt)。
- 必須パラメータは常に存在すること。オプションは安全なデフォルトを持つ。
- iOS(SwiftUI)とAndroid(Kotlin)で同じパターンを使う。
- 変更が予想されるなら簡単なバージョンプレフィックス(v1など)を用意する。
- パラメータが欠けたときの挙動を定義する(エラースクリーンを表示するなど)。
ステップ3:ログイン挙動とポストログインのターゲットを決める
リンクごとにログインが必要かどうかを書き出してください。必要な場合は遷移先を記録し、ログインしてから継続する設計にします。
例:レシートリンクはログインなしでプレビューを見せられるが、「請求書ダウンロード」ボタンはログインが必要で、ログイン後に正確なレシートに戻るべきです。
ステップ4:トークン受け渡しルールを決める(シークレットをURLに入れない)
リンクにワンタイムトークンが必要なら、有効期限と使用ルールを定義してください。
現実的な手法:URLには短時間有効で一度だけ使えるコードを入れ、アプリがサーバと交換する。
ステップ5:3つの実世界状態でテストする
ディープリンクは端で壊れます。各リンクタイプを次の状態でテストしてください:
- コールドスタート(アプリ閉じている)
- ウォームスタート(メモリにある)
- アプリ未インストール(Webやストアにフォールバック)
ルート、認証チェック、トークン交換ルールを一箇所に置けば、KotlinとSwiftUI画面にカスタムルーティングロジックが散らばるのを避けられます。
よくあるミスとその回避法
ディープリンクは地味な理由で失敗します:小さな想定、画面名変更、あるいは「一時的」としたトークンが至る所に残るなど。
現場でよく見る失敗と修正
-
アクセストークンをURLに入れてしまう(ログに漏れる):クエリ文字列はコピーされ、共有され、履歴や解析ログに残り得ます。修正:リンクには短いワンタイムコードだけを入れ、アプリ内で引き換えて速やかに期限切れにする。
-
アプリがインストールされている前提(フォールバックなし):リンクがエラーページを開くか何もしないとユーザーは諦めます。修正:インストール手順を案内するフォールバックWebページを用意する。単純な「続けるにはアプリを開いてください」ページでも無音よりは良い。
-
1つのデバイスで複数アカウントを扱わない:間違ったユーザーで正しい画面が開くと酷い体験になります。修正:リンク受信時にアクティブなアカウントを確認し、ユーザーに確認または切り替えを促してから続行する。特定のワークスペースが必要なら(ただしシークレットでなく)識別子を含めて検証する。
-
画面やルート名を変えてリンクを壊す:ルートがUI名に紐づいていると、タブ名を変えただけで古いリンクが死にます。修正:意図ベースで安定したルート(invite, ticket, order)を設計し、古いバージョンの互換性を保つ。
-
問題発生時の追跡がない:何が起きたか再現できないとサポートは推測しかできません。修正:非機密のリクエストIDをリンクに含め、サーバとアプリでログに残し、エラーメッセージにそのIDを表示する。
現実チェック:グループチャットで送られた招待リンクを想像してください。ある人が勤務用の端末で開き、デバイスに2つのアカウントがあり、タブレットにはアプリが入っておらず、リンクが同僚に転送される。リンクに招待コードだけが含まれていて、フォールバックがあり、アカウント確認を促し、リクエストIDをログに残す設計なら、そのリンクは多くのケースで安全に成功します。
例:いつでも正しい画面を開く招待リンク
招待は典型的なディープリンクです:チームメンバーがメッセンジャーでリンクを送り、受け取った人はワンタップで招待画面に着地することを期待します。
シナリオ:マネージャーが「Support Team」ワークスペースに新しいサポート担当を招待し、エージェントがTelegramで招待をタップした。
アプリがインストールされているなら、システムはアプリを開いて招待の詳細を渡すべきです。インストールされていないなら、招待が何のためかを説明してインストールを促すシンプルなWebページに着地させ、インストール後に招待フローを完了できるようにします。
アプリ内のフロー(KotlinでもSwiftUIでも同じ):
- 受信したリンクから招待コードを読む。
- ユーザーがログインしているか確認する。
- バックエンドで招待を検証し、正しい画面へルーティングする。
検証が重要です。リンクに長期有効なセッショントークンなどのシークレットを入れてはいけません。短時間有効な招待コードだけを含め、サーバ側で検証してから進めます。
ユーザー体験は予測可能であるべきです:
- 未ログイン: ログイン画面が表示され、ログイン後に招待受諾に戻る。
- ログイン済み: 「ワークスペースに参加しますか?」という確認が1回だけ表示され、完了後に正しいワークスペースに着地する。
招待が期限切れや既に使用済みなら、空白のエラーページに放り込まず、明確なメッセージと次の行動を示してください:新しい招待を依頼する、アカウントを切り替える、管理者に連絡するなど。「この招待は既に受諾されています」は「無効なトークン」より親切です。
クイックチェックリストと次のステップ
ディープリンクが「完了」したと感じられるのは、コールドスタートでもウォームスタートでも、さらにユーザーがすでにサインインしている場合でも同じように振る舞うときです。
クイックチェックリスト
出荷前に実機と実OSバージョンで次をテストしてください:
- リンクはコールドスタートとウォームスタートで正しい画面を開く。
- URLに機微な情報が入っていない。トークンが必要なら短時間で一度だけ使えるものにする。
- 不明、期限切れ、既使用のリンクは明快な画面にフォールバックし、次に取るべき行動を示す。
- メールアプリ、ブラウザ、QRスキャナー、メッセンジャープレビューなどから動作する(中には先にWebビューで開くものがある)。
- ログが何が起きたかを示す(受信したリンク、解析結果、認証要否、成功/失敗理由)。
ふさわしい検証方法は、重要なリンク(招待、パスワードリセット、注文詳細、サポートチケット、プロモ)をいくつか選び、同じテストフローで試すことです:メールからタップ、チャットからタップ、QRコードスキャン、再インストール後に開く。
次のステップ(保守性を保つために)
ディープリンク処理が画面中に広がり始めたら、ルーティングと認証を各画面のコードではなく共有の配管として扱ってください。解析ロジックを中央にまとめ、各目的地が生のURLではなくクリーンなパラメータを受け取るようにします。認証も同様に1つのゲートで「今続ける」か「先にサインインしてから続けるか」を決めさせます。
カスタム接着コードを減らしたければ、バックエンド、認証、モバイルアプリを一緒に設計すると楽です。AppMaster (appmaster.io) のようなノーコードプラットフォームは、生成されるプロダクション対応のバックエンドとネイティブアプリでルート名やワンタイムコード引き換えのエンドポイントを揃えやすくし、要件変更時の手作業を減らしてくれます。
来週やるべき一つのことがあるとしたら:代表的なルートと各失敗ケースの明確なフォールバックを紙に書き出し、それを単一のルーティングレイヤーで実装してください。
よくある質問
ディープリンクは、そのリンクが示す正確な画面を開くべきです。汎用のホームやダッシュボードではなく、アプリがインストールされていない場合でも、意味のある場所に遷移してインストール後に同じ目的地へ戻れるように案内するべきです。
Universal Links(iOS)やApp Links(Android)は通常のWeb URLを使い、インストール済みならアプリを開き、未インストールならGracefulにWebにフォールバックします。カスタムスキームは設定が容易ですが、ブラウザや他アプリによって扱いが異なることがあるため、二次的な選択肢として使うのが安全です。
多くのメールやメッセンジャーアプリは組み込みのブラウザでリンクを開き、OSに直接引き渡さないことがあります。これによりSafariやChromeでは動作するのに、メール内では動かない場合が出ます。ウェブのフォールバックをわかりやすくし、まずはWebページに導かれるケースを想定しておくと安心です。
コールドスタート時はスプラッシュや初期化、設定読み込みなどの処理があり、その間にリンク情報が失われることがあります。受信したリンクはすぐに保存し、初期化が終わった後に再生(replay)してナビゲーションするのが確実な対処法です。
長時間有効なアクセストークン、リフレッシュトークン、パスワードや個人情報などは絶対にURLに入れないでください。URLは履歴やスクリーンショット、ログに残り得ます。代わりに短時間で一度だけ使えるコードをURLに入れ、アプリ起動後にサーバで交換(redeem)する方式が安全です。
リンクを解析して意図した遷移先を保存し、認証状態に応じてルーティングするのが基本です。ログインは一度だけ行わせ、完了後に保存しておいた“戻り先”に移動します。保存する戻り先は小さく、期限を設け、使用後は消去してください。
ルートを共通の契約(contract)と見なし、解析と検証を中央にまとめて、画面にはクリーンな入力(生のURLではなく)を渡すことです。そうすれば各画面がパラメータの有無や形式を個別に判断する必要がなくなります。
まずアプリで有効なアカウントがどれかを確認し、リンクが示すワークスペースやテナントと一致するかを検証してから、ユーザーに確認やアカウント切替を促します。誤ったアカウントでプライベートな内容を開くより、短い確認ステップを挟む方が安全です。
近い安定した画面(例:一覧ページ)を開き、何が開けなかったのかを短く説明するメッセージを出すのが良い既定動作です。白紙ページや文脈のないログイン画面に放り込むのは避けてください。
主要なリンク種類を各状態でテストしてください:アプリが閉じている、既に動いている、インストールされていない状態から、それぞれメール、チャット、QRスキャンなど実際のソースから試します。AppMasterを使うと、バックエンドとネイティブアプリ間でルート名やワンタイムコードの交換エンドポイントを揃えやすくなり、カスタムの結びつけコードを減らせます。


