Universal Linksを用いたiOSのDeep Link対応

iOS 13以降の Universal Links について紹介します。

# Universal Linksとは

iOS 9からサポートされて、Apple が正式に提供したディープリンク技術です。
簡単にいうと、通常の HTTP リンクで設定すると、その URL を踏んだ時に、端末のデフォルトブラウザで Web サイトを開く代わりに、アプリを起動してページへ遷移することができます。 
URL スキームと似ている機能ですが、主に下記の違いがあると思います。

* Custom URL スキームが不要です。 `hoge://` みたいなスキームではなく、 通常の HTTP リンクのスキーム `https://` でアプリが起動できるので、 Web サイトとアプリ両方同じURLで遷移できます。
* Web サーバーにアプリの起動条件を記載する apple-app-site-association を配置する必要がある。URL のパスやパラメータなど設定することで、アプリに遷移させたいリンクのみ指定可能です。
* 直接にブラウザにURLを入力した場合、アプリが起動しません。同一ドメイン内で Web 回遊中の時にも、アプリが起動しません。

# アプリ側の設定
## Associated Domainsの設定 

* Signing & Capabilities の `+ Capability` をクリックする
![スクリーンショット 2021-12-22 16.22.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45744/71ed9b81-41b0-caa3-facc-673b81dbd013.png)

* Associated Domainsを選択する
![スクリーンショット 2021-12-22 16.24.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45744/9cac3280-9a01-079e-c244-6e098ae98ada.png)

* Associated Domains という項目が追加されてここでドメインを追加する 
![スクリーンショット 2021-12-22 16.33.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45744/47dc8a24-85ed-8471-3e2e-18dfa4a2c0fd.png)

## ドメインを追加する
フォーマットは `applinks:<fully qualified domain>` になります。サブドメインで対応する場合、サブドメインを含めて設定する必要があります。 

## Alternate Mode
iOS 14から、OSは直接 Web サーバーからではなく、 Apple の CDN を経由して apple-app-site-association をダウンロードします。 CDNへの反映は最大24時間かかるらしいです。また、IP 制限がある開発環境で、CDN からのアクセスできない場合もあるので、開発の際、Alternate Modeを `developer` に設定すると、CDNを経由せず 直接 Web サーバーから apple-app-site-association をダウンロードできます。
先ほど追加したドメインにパラメータ　`mode=developer` を追加するだけで設定完了です。 `applinks:example.com?mode=developer` のようにになります。
この場合、テスト端末の `Associated Domains Development` を有効にする必要があります。
![IMG_740D72C118F8-1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45744/c6497dc4-0e4a-d6a1-42d5-87f359abd588.jpeg)


なお、 MDM アプリの場合、 `mode= managed` で設定すると、 Apple の CDN も回避できるようです。
詳細は [Associated Domains Entitlement](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_associated-domains)　ご確認ください。

## ipa で確認する
アプリを配布する際、 ipa の設定は大丈夫だろう？と思ったら ipa を展開して確認しましょう。

```shell
unzip -d work hoge.ipa
codesign -d --entitlements - work/Payload/hoge.app
```

entitlements で設定された値が確認できます。
![スクリーンショット 2021-12-22 19.58.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45744/fddccadb-7b28-dd1d-2645-dc79c06e10ea.png)

アプリ側の設定がこれで完了です。

# apple-app-site-association
iOS 13から apple-app-site-association のフォーマットが新しくなりました。これから紹介する apple-app-site-association は、 **iOS 12 以前では発動しないので、ご注意ください**。

## apple-app-site-association の書き方
Apple の公式ドキュメント [applinks](https://developer.apple.com/documentation/bundleresources/applinks) のサンプル json を見てみましょう。

```json
{
    "applinks":
    {
        "details":
        [
            {
                "appIDs":
                [
                    "ABCDE12345.com.example.app",
                    "ABCDE12345.com.example.app2"
                ],
                "components":
                [
                    {
                        "#": "no_universal_links",
                        "comment": "フラグメント#no_universal_linksが付いている場合Universal Links発動しない",
                        "exclude": true
                    },
                    {
                        "/": "/buy/*",
                        "comment": "パスが/buy/からスタートしたURLはUniversal Links発動する"
                    },
                    {
                        "/": "/help/website/*",
                        "comment": "パスが/help/website/からスタートしたURLはUniversal Links発動しない",
                        "exclude": true
                    },
                    {
                        "/": "/help/*",
                        "?":
                        {
                            "articleNumber": "????"
                        },
                        "comment": "パスが/help/からスタートしたURL、かつパラメータarticleNumberが４桁の文字列の場合Universal Links発動する"
                    }
                ]
            }
        ]
    }
}
```

### 全体の説明
|  項目名  |  説明  |
| ---- | ---- |
|  applinks  |  固定オブジェクト  |
|  applinks.details  |  Universal Linksのアプリ情報と発動条件の配列  |
|  applinks.details.appIDs  |  対象アプリの情報の配列　　 <br> フォーマットは `<team id>.<bundle id>` になる |
|  applinks.details.components  |  Universal Linksの発動条件の配列　 <br> 優先順位は上から適用される |

### components の説明
|  項目名  |  説明  |
| ---- | ---- |
|  "#": ""  |  フラグメントの条件  |
|  "/": ""  |  URLパスの条件  |
|  "?": { "<key>": "<value>" }  |  パラメータの条件  |
|  "exclude": true  |  trueで設定すると該当条件は発動しない  |
|  "comment": ""  |  コメント <br> 必須ではない  |

components　の各種条件の組み合わせで細かく発動条件が設定できます。

## SubstitutionVariables
**iOS 13.5** から [SubstitutionVariables](https://developer.apple.com/documentation/bundleresources/applinks/substitutionvariables) という、代替変数の機能が追加されました。
`$(alpha)` 、 `$(digit)` など固定の変数があり、こちらのように自前の定義を追加することも可能です。

```json
{
  "applinks": {
    "substitutionVariables": {
      "food": [ "burrito", "pizza", "sushi", "samosa" ]
    },
    "details": [{
      "appIDs": [ ... ],
      "components": [
        { "/" : "/$(lang)_$(region)/$(food)/" }
      ]
    }]
  }
}
```

# サーバー側の設定

apple-app-site-associationを作成したらそれを Web サーバーに配置します。ここではいくつのポイントがあります。

* apple-app-site-association ファイルは拡張子なしで、 MIME タイプが **application/json** になっていること
* **.well-known** ディレクタリー直下に apple-app-site-association ファイルが配置されていること
* **リダイレクトなしで**、 `https://<fully qualified domain>/.well-known/apple-app-site-association` へアクセスできること

## 疎通確認
apple-app-site-association の設置が終わったら、正常にアクセスできるか確認しましょう。[App Search API Validation Tool](https://search.developer.apple.com/appsearch-validation-tool) から確認も可能ですが、こちらの curl を叩くと CDN への反映状況などより詳細な情報を入手できます。

```shell
curl -v https://app-site-association.cdn-apple.com/a/v1/example.com
```

キャッシュの生存時間があとどのくらいあるかなどの情報も確認できるので、疎通確認の際非常に役立ちます。

![スクリーンショット 2021-12-22 20.16.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45744/d70062f4-8ad7-60af-fab1-630cce0e81ac.png)

疎通確認が成功したら、設定したURLを踏むとアプリが起動できると思います。
ただし、 iOS では、アプリの**新規インストール**、**アップデート**、または**システムのタイミング（平均的に１週間１回らしい）**でapple-app-site-associationが更新されるので、端末に最新のapple-app-site-associationが反映されるまでタイムラックがあります。

# アプリの実装
最後に、アプリ側で受け取ったURLを処理します。
`AppDelegate` の `application(_:continue:restorationHandler:)` が最初の入り口になります。

```swift
    func application(_ application: UIApplication, continue continueUserActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
        // appLinksの場合、activityTypeがNSUserActivityTypeBrowsingWebになる
        guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL else { return false }

        if canHandle(url) {
            // do something
        } else {
            // 処理できないURLはブラウザで開く
            UIApplication.shared.open(url, options: [:], completionHandler: nil)
        }

        return true
    }
```

以上、 Universal Links の紹介でした。