Universal Links の新情報
iOS 13、iOS 14 Universal Links の新機能とローカルテスト環境の構築

Photo by NASA
はじめに
ウェブサイトとアプリの両方を持つサービスにとって、Universal Linksの機能はユーザー体験において非常に重要であり、Webとアプリ間のシームレスな連携を実現します。しかし、これまで簡単な設定だけであまり深く掘り下げられていませんでした。先日、時間をかけて調査する機会があり、興味深い点をいくつか記録しました。
よくある考慮事項
担当したサービスでは、Universal Linksの実装に関して、APP側に完全なウェブ機能が実装されていないことが多いです。Universal Linksはドメイン名を認識するため、ドメイン名が一致すればAPPが起動します。この問題については、APPに対応機能がないURLをNOTで除外することが可能です。もしウェブサービスのURLが極端に複雑であれば、Universal Links用に新しいサブドメインを作成するのが良いでしょう。
apple-app-site-association はいつ更新される?
-
iOS < 14では、アプリは初回インストールやアップデート時にUniversal Linksのウェブサイトのapple-app-site-associationを取得します。
-
iOS ≥ 14では、Apple CDNがUniversal Linksサイトのapple-app-site-associationをキャッシュし、定期的に更新します。アプリは初回インストールやアップデート時にApple CDNから取得しますが、ここで問題があります。Apple CDNのapple-app-site-associationがまだ古い場合があるのです。
Apple CDNの更新メカニズムについて調べましたが、ドキュメントには記載がありませんでした。ディスカッションを確認したところ、公式は「定期的に更新する」とだけ回答し、詳細は後日ドキュメントで公開するとしていますが、いまだに公開されていません。
私の感覚では遅くとも48時間以内には更新されると思います。。。ですので、次回apple-app-site-associationを変更する場合は、APPのリリースやアップデートの数日前にapple-app-site-associationを先に修正して公開しておくことをおすすめします。
apple-app-site-association Apple CDN 確認:
Headers: HOST=app-site-association.cdn-apple.com
GET https://app-site-association.cdn-apple.com/a/v1/あなたのドメイン

現在のApple CDN上のバージョンを取得できます。(リクエストヘッダーに Host=https://app-site-association.cdn-apple.com/ を必ず付けてください)
iOS ≥ 14 デバッグ
前述のCDN問題のため、開発段階ではどのようにデバッグすればよいのでしょうか?
この部分はAppleが解決方法を提供してくれてよかったです。さもなければ即時更新ができず、本当に困ってしまいます。applinks:domain.com に ?mode=developer を付けるだけで済みます。さらに、managed(企業内APP用) や developer+managed モードも設定可能です。

mode=developer を付けると、APP はシミュレーター上で毎回 Build & Run するたびに直接サイトから最新の app-site-association を取得して使用します。
実機でビルド&実行する場合は、まず「設定」->「開発者」->「Associated Domains Development」をオンにしてください。

⚠️ ここに落とし穴があります。apple-app-site-association はサイトのルートディレクトリか
./.well-knownディレクトリに置けますが、mode=developer では./.well-known/apple-app-site-associationのみを参照するため、効果がないと思ってしまいました。
開発テスト
iOS 14未満の場合、app-site-associationを変更したら削除してから再度ビルド&実行しないと最新のものを取得しません。iOS 14以上は前述の方法に加えてmode=developerを使用してください。
app-site-association の内容の変更は、できればサーバー上のファイルを直接修正するのが望ましいですが、サーバー側にアクセスできない場合、Universal Links のテストは非常に面倒になります。何度もバックエンドの同僚に頼まなければならず、app-site-association の内容を確定してから一度にリリースしなければならず、何度も修正すると同僚を困らせてしまいます。
ローカルでシミュレーション環境を構築する
上記の問題を解決するために、ローカルで小さなサービスを立ち上げることができます。
まずは Mac に nginx をインストールします:
brew install nginx
もしまだ brew をインストールしていない場合は、先にインストールしてください:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
(コメントなし、コードはそのままです)
nginxをインストールした後、/usr/local/etc/nginx/ に移動して nginx.conf ファイルを開いて編集します:
...略
server {
listen 8080;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root /Users/zhgchgli/Documents;
index index.html index.htm;
}
...略
大体44行目あたりの location / 内の root を、任意のディレクトリパスに変更してください(ここでは Documents を例としています)。
ポート8080でリッスンします。競合がなければ変更は不要です。
変更を保存したら、以下のコマンドでnginxを起動します:
nginx
停止したい場合は、以下を入力してください:
nginx -s stop
(コメントなし、コマンドはそのままです)
了解しました。翻訳作業を停止しました。
もし nginx.conf を変更した場合は、必ず以下のコマンドを実行してください:
nginx -s reload
サービスを再起動する。
新しく設定した root ディレクトリ内に ./.well-known フォルダを作成し、apple-app-site-association ファイルを ./.well-known に配置します。
⚠️
.well-knownを作成した後に消えた場合は、Macで「隠しフォルダを表示」機能をオンにしてください:
ターミナルで:
defaults write com.apple.finder AppleShowAllFiles TRUE
(コメントなし、コードはそのまま保持)
killall finder を実行すると、すべての Finder が再起動します。

⚠️
apple-app-site-associationは拡張子がないように見えますが、実際には.json拡張子があります:
ファイルを右クリック -> 「情報を見る Get Info」-> 「名前と拡張子 Name & Extension」-> 拡張子があるか確認し、同時に「拡張子を隠す Hide extension」のチェックを外すことができます

問題なければ、ブラウザを開いて以下のリンクが正常にapple-app-site-associationをダウンロードできるか確認してください:
http://localhost:8080/.well-known/apple-app-site-association
正常にダウンロードできれば、ローカル環境のシミュレーションが成功したことを意味します!
もし404や403エラーが発生した場合は、ルートディレクトリが正しいか、ディレクトリやファイルが正しく配置されているか、apple-app-site-associationに誤って拡張子(.json)が付いていないかを確認してください。
登録&ダウンロード Ngrok


ngrok 実行ファイルの解凍

Dashboard ページ にアクセスして、設定を行います。
./ngrok authtoken あなたのTOKEN
設定が完了したら、以下を実行してください:
./ngrok http 8080
私たちの nginx はポート8080で動作しています。
サービスを起動する。

この時、サービス起動状態のウィンドウが表示され、Forwardingから今回割り当てられた公開URLを確認できます。
⚠️ 起動するたびに割り当てられるURLが変わるため、開発テスト用としてのみ使用してください。
こちらでは今回割り当てられたURL
https://ec87f78bec0f.ngrok.io/を例として使用します
ブラウザに戻って https://ec87f78bec0f.ngrok.io/.well-known/apple-app-site-association を入力し、apple-app-site-associationファイルが正常にダウンロード・閲覧できるか確認してください。問題なければ次のステップに進めます。
ngrokで割り当てられたURLをAssociated Domainsのapplinks:設定に入力します。

テストを便利にするために、必ず ?mode=developer を付けてください。
再ビルド&実行:

ブラウザを開き、対応するUniversal LinksのテストURL(例: https://ec87f78bec0f.ngrok.io/buy/123)を入力して動作を確認してください。
ページで404が表示されても無視してください。実際にはそのページは存在しません。iOSのURLマッチング機能が期待通りに動作するかをテストしているだけです。上部に「Open」が表示されればマッチ成功を意味します。また、NOT(否定)マッチの状況もテスト可能です。
「Open」をクリックしてアプリを開く -> テスト成功!
開発段階でテストがすべて完了したら、修正した apple-app-site-association ファイルをバックエンドに渡してサーバーにアップロードすれば、問題なく動作することが保証されます〜
最後にAssociated Domainsのapplinks:を正式なテスト用URLに変更してください。
また、ngrokの実行状況ウィンドウから、APPのビルド&実行時にapple-app-site-associationファイルが要求されているかどうかを確認することもできます:

Applinks 設定内容
iOS < 13 以前:
設定ファイルはより簡単で、以下の内容のみ設定可能です:
{
"applinks": {
"apps": [],
"details": [
{
"appID" : "TeamID.BundleID",
"paths": [
"NOT /help/",
"*"
]
}
]
}
}
TeamID.BundleId をあなたのプロジェクト設定に置き換えてください(例:TeamID = ABCD 、BundleID = li.zhgchg.demoapp => ABCD.li.zhgchg.demoapp)。
複数の appID がある場合は、複数回追加する必要があります。
paths 部分はマッチングルールで、以下のいくつかの構文をサポートしています:
-
*:0文字以上の任意の文字にマッチ、例:/home/*(home/alan…) -
?:1文字にマッチ、例:201?(2010〜2019) -
?*:1文字以上の任意の文字にマッチ、例:/?*(/test、/home..) -
NOT:除外指定、例:NOT /help(/help以外のすべてのURL)
より多くの組み合わせは実際の状況に応じて自由に決められます。詳細は公式ドキュメントを参照してください。
- ご注意ください、これは正規表現ではなく、正規表現の記述はサポートされていません。
- 古いバージョンはQuery (?name=123)やAnchor (#title)をサポートしていません。
- 中国語のURLはまずASCIIに変換してからpathsに入れる必要があります(すべてのURL文字はASCIIである必要があります)。
iOS ≥ 13 以降:
設定ファイルの内容が強化され、Query/Anchor、文字セット、エンコード処理のサポートが追加されました。
"applinks": {
"details": [
{
"appIDs": [ "TeamID.BundleID" ],
"components": [
{
"#": "no_universal_links",
"exclude": true,
"comment": "フラグメントが no_universal_links と等しい任意のURLにマッチし、システムにユニバーサルリンクとして開かないよう指示します"
},
{
"/": "/buy/*",
"comment": "/buy/ で始まるパスを持つ任意のURLにマッチします"
},
{
"/": "/help/website/*",
"exclude": true,
"comment": "/help/website/ で始まるパスを持つ任意のURLにマッチし、システムにユニバーサルリンクとして開かないよう指示します"
},
{
"/": "/help/*",
"?": { "articleNumber": "????" },
"comment": "/help/ で始まるパスを持ち、クエリ項目名が 'articleNumber' で値がちょうど4文字の任意のURLにマッチします"
}
]
}
]
}
公式ドキュメントからの転載で、フォーマットが変更されていることがわかります。
appIDs は配列で、複数の appID を入れることができます。これにより、以前のようにブロック全体を繰り返し入力する必要がなくなります。
WWDCでは旧バージョンとの互換性について触れており、iOS 13以降では新しいフォーマットを読み取ると古いpathsを無視します。
マッチングルールは components に移動され、3種類のタイプをサポートしています:
-
/:URL -
?:クエリ、例:?name=123&place=tw -
#:アンカー、例:#title
そして組み合わせて使用することも可能です。例えば、/user/?id=100#detail の場合のみAPPに遷移させたい場合は、以下のように記述します:
{
"/": "/user/*",
"?": { "id": "*" },
"#": "detail"
}
マッチング構文は元の構文と同様で、*、?、?*もサポートしています。
comment コメント欄を追加し、識別しやすいようにコメントを入力できます。(ただし、これは公開されるため、他の人も見ることができます)
逆排除は exclude: true を指定します。
caseSensitive 指定機能を追加しました。マッチングルールが大文字小文字を区別するかどうかを指定できます。デフォルト:true。これにより、多くのルールを省略できます。
percentEncoded の追加について前述しましたが、旧バージョンではURLをASCIIに変換してpathsに入れる必要がありました(中国語などは見た目が悪く判別しにくくなります)。このパラメータは自動的にエンコードするかどうかを指定するもので、デフォルトは true です。
中国語のURLでも直接入れることができます(例: /客服中心)。
詳細な公式ドキュメントはこちらをご参照ください。
デフォルトの文字セット:
これは今回のアップデートで重要な機能の一つであり、文字セットのサポートが追加されました。
システムが定義した文字セット:
-
$(alpha):A-Z と a-z -
$(upper):A-Z -
$(lower):a-z -
$(alnum):A-Z と a-z と 0–9 -
$(digit):0–9 -
$(xdigit):16進数の文字、0–9 と a,b,c,d,e,f,A,B,C,D,E,F -
$(region):ISO地域コード isoRegionCodes 、例:TW -
$(lang):ISO言語コード isoLanguageCodes 、例:zh
もし私たちのURLが多言語対応している場合、Universal Linksをサポートするには、以下のように設定できます:
"components": [
{ "/" : "/$(lang)-$(region)/$(food)/home" }
]
これにより、/zh-TW/home や /en-US/home の両方をサポートでき、とても便利で、一連のルールを自分で書く必要がありません!
カスタム文字セット:
デフォルトの文字セットに加えて、カスタム文字セットを作成して、設定ファイルの再利用性と可読性を向上させることもできます。
applinks に substitutionVariables を追加するだけです:
{
"applinks": {
"substitutionVariables": {
"food": [ "burrito", "pizza", "sushi", "samosa" ]
},
"details": [{
"appIDs": [ ... ],
"components": [
{ "/" : "/$(food)/" }
]
}]
}
}
例では food というカスタム文字セットを定義し、その後の components で使用しています。
以上の例は /burrito 、/pizza 、/sushi 、/samosa にマッチします。
詳細はこちらの公式ドキュメントをご参照ください。
アイデアが浮かばない?
設定ファイルの内容にアイデアがない場合は、他のサイトの内容をこっそり参考にしても構いません。サービスサイトのホームページのURLに /app-site-association または /.well-known/app-site-association を付け加えるだけで、その設定を読むことができます。
例えば: https://www.netflix.com/apple-app-site-association
補足
SceneDelegate を使用している場合、Universal Link の起点は SceneDelegate 内にあります:
func scene(_ scene: UIScene, continue userActivity: NSUserActivity)
AppDelegateではなく:
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool
関連記事
参考資料
Post は ZMediumToMarkdown によって Medium から変換されました。



コメント