タップ一つで「目的の画面」まで運ぶ
プッシュ通知を送ったのに、開いてみるとホーム画面に戻されてしまう。せっかく「今日の壁紙が届きました」と知らせても、ユーザーがその壁紙までたどり着く前に離脱してしまう — これは私自身が壁紙アプリの運営で何度も経験した、もったいない取りこぼしでした。
ディープリンクは、この距離をゼロにする仕組みです。myapp://wallpaper/sky-blue のような URL をタップすると、アプリが起動して、その壁紙の詳細画面が直接開きます。通知・Web サイト・他アプリ・広告計測のリンク、いずれの入口からでも「目的の一画面」までまっすぐ運べるようになります。
ここからは Rork Max(Expo ベース)で動くアプリに、URL Scheme と Universal Links の両方を実装する手順と、実機で初めて気づくつまずきどころを順に見ていきます。
URL Scheme と Universal Links、どちらをいつ使うか
二つの方式は役割が違います。URL Scheme(myapp://path)は実装が最も手軽で、アプリ内テストや他アプリからの呼び出しに向いています。ただしアプリが未インストールだとタップしても何も起きず、ユーザーには「壊れたリンク」に見えてしまいます。
Universal Links(iOS)と App Links(Android)は、どちらも https://myapp.com/path という通常の Web URL を使います。アプリが入っていれば直接アプリが開き、入っていなければそのまま Web ページが開く — この「切り替えのシームレスさ」が最大の利点で、Apple も公式に推奨している方式です。マーケティングや通知から外部に出す URL は、原則 Universal Links / App Links を使うのが安全だと考えています。
実務での使い分けはシンプルで、アプリ内部やデバッグでは URL Scheme、ユーザーに渡すリンクは Universal Links、と二段構えにしておくと迷いません。
app.config でスキームと関連ドメインを設定する
Rork Max が生成したプロジェクトは Expo 構成なので、app.json(または app.config.ts)に入口を宣言します。
{
"expo": {
"scheme": "myapp",
"ios": {
"associatedDomains": ["applinks:myapp.com"]
},
"android": {
"intentFilters": [
{
"action": "VIEW",
"autoVerify": true,
"data": [
{ "scheme": "https", "host": "myapp.com", "pathPrefix": "/wallpaper" }
],
"category": ["BROWSABLE", "DEFAULT"]
}
]
}
}
}scheme が URL Scheme(myapp://)の登録、associatedDomains と intentFilters が Universal Links / App Links の宣言です。ここまでは設定ファイルだけの話で、つまずく人はほとんどいません。問題は次のサーバー側にあります。
Universal Links が「効かない」ときに最初に疑う場所
「設定は全部入れたのに、リンクをタップすると Safari が開いてしまう」— Universal Links で最もよく聞く症状で、原因のほとんどはアプリ側ではなくサーバー側の検証ファイルにあります。
iOS は、宣言したドメインの https://myapp.com/.well-known/apple-app-site-association(AASA)というファイルを取りに行き、内容が一致して初めてアプリを開きます。このファイルでつまずく典型が三つあります。配信パスが /.well-known/ 配下になっていない、Content-Type が application/json でない、そして途中でリダイレクト(301/302)が挟まっている、の三つです。いずれか一つでも該当すると iOS は黙ってあきらめ、ただの Web リンクとして Safari を開きます。
正しい AASA は、リダイレクトなしの 200 で次の JSON を返す状態です。
{
"applinks": {
"details": [
{
"appIDs": ["YOUR_TEAM_ID.com.example.myapp"],
"components": [
{ "/": "/wallpaper/*", "comment": "壁紙詳細へのリンク" }
]
}
]
}
}Android 側は https://myapp.com/.well-known/assetlinks.json を見ます。
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.myapp",
"sha256_cert_fingerprints": ["YOUR_SHA256_FINGERPRINT"]
}
}
]ここで私がはまったのは sha256_cert_fingerprints でした。手元のデバッグ証明書のフィンガープリントを入れたまま本番 AAB を出すと、Play 署名鍵と一致せず App Links が検証されません。本番では Play Console の「アプリの署名」に表示される SHA-256 を使う、というのが地味ですが大事なポイントです。AASA / assetlinks は配置したら、ブラウザで実際にその URL を開いて、リダイレクトなしの素の JSON が返ることを必ず目視確認するようにしています。
React Navigation との統合とコールド起動の取りこぼし
入口が通ったら、URL をどの画面に割り当てるかを定義します。Rork Max には自然言語でそのまま頼めます。
「アプリに次のディープリンクを追加してください:
- myapp://home → ホーム画面
- myapp://wallpaper/:id → 壁紙詳細画面
- myapp://profile → プロフィール画面
React Navigation の linking 設定を使って実装してください。」
生成される linking 設定はおおむね次の形になります。
const linking = {
prefixes: ['myapp://', 'https://myapp.com'],
config: {
screens: {
Home: 'home',
WallpaperDetail: 'wallpaper/:id',
Profile: 'profile',
},
},
};
<NavigationContainer linking={linking}>
{/* ... */}
</NavigationContainer>ここで実機でしか気づけない落とし穴があります。リンクの受け取りには二つの経路があり、アプリが起動中(ウォーム) のときは Linking.addEventListener('url', ...) が発火しますが、アプリが完全に終了した状態(コールド) から起動したときは、最初の URL を Linking.getInitialURL() で拾わないと取りこぼします。
2014年から個人開発で壁紙アプリを作り続けてきましたが、私自身がこれに遭遇したのもその一つでした。自前で addEventListener だけを書いていたため、通知から「アプリが落ちている状態」でタップすると、毎回ホーム画面に着地してしまう。期待した壁紙画面には飛ばないのです。
// Before: ウォーム起動しか拾えず、コールド起動を取りこぼす
import * as Linking from 'expo-linking';
useEffect(() => {
const sub = Linking.addEventListener('url', ({ url }) => handleUrl(url));
return () => sub.remove();
}, []);// After: 起動時の初期 URL も明示的に拾う
import * as Linking from 'expo-linking';
useEffect(() => {
// コールド起動: アプリを開いた URL を取得
Linking.getInitialURL().then((url) => {
if (url) handleUrl(url);
});
// ウォーム起動: 実行中の遷移
const sub = Linking.addEventListener('url', ({ url }) => handleUrl(url));
return () => sub.remove();
}, []);幸い、React Navigation の linking 設定を使う場合は、内部で getInitialURL を読んでくれるため、この二経路は自動的に処理されます。逆に言えば、linking 設定に任せず自前でハンドリングするときだけ、コールド起動を自分で拾う必要がある、と覚えておくと混乱しません。
プッシュ通知から特定の画面へ運ぶ
最後に、冒頭の「通知を開いたらホームに戻される」を解消します。通知のデータ部にディープリンク URL を載せ、タップ時にそれを開くだけです。
// 送信側(サーバー): 通知データに遷移先 URL を含める
const notification = {
title: "今日の壁紙が届きました",
body: "新しい空のグラデーションを追加しました",
data: { url: "myapp://wallpaper/sky-blue" }
};
// アプリ側: タップ時に URL を開く
import * as Notifications from 'expo-notifications';
Notifications.addNotificationResponseReceivedListener((response) => {
const url = response.notification.request.content.data?.url;
if (url) Linking.openURL(url);
});Linking.openURL を呼べば、先ほど定義した linking 設定が URL を解釈して、そのまま壁紙詳細画面まで運んでくれます。通知の開封率そのものより、開いた後にどこへ着地するかのほうがリテンションに効きます。AdMob の広告収益を支えに個人開発を続けてきた立場としても、離脱を一つ減らすこの設計は、地味ながら効果が大きいと感じています。
次の一歩
まずは URL Scheme だけを設定し、シミュレータのターミナルから npx uri-scheme open "myapp://wallpaper/sky-blue" --ios を実行して、狙った画面に着地するかを確認してみてください。ここが通れば、あとは AASA / assetlinks を整えるだけで Universal Links まで地続きです。実装の参考になれば幸いです。