ユニバーサルリンクの設定とリダイレクトに関するトラブルシューティング

## はじめに

最近、iOSアプリにユニバーサルリンクを設定しました。その中で、理解をブラッシュアップすることができたので、記事に残したいと思います。

## ユニバーサルリンクとは？

ユニバーサルリンクは、特定のURLにアクセスするとiOSアプリを直接開くことができる機能のようです。アプリがインストールされているかどうかに関係なく機能するという解説をよく見かけます。一見、開発者が意識することなく、そのような設定ができると思われますが、アプリのインストール状態に応じた仕掛けを作る必要があるようでした。

## 設定プロセス

ユニバーサルリンクの設定プロセスをざっと記載します。

### 1. アプリのAssociated Domains設定


Xcodeにて、Associated DomainsのCapabilitiesを有効にし、ユニバーサルリンクで使用するドメインを追加します。

例: applinks:mydomain.com

※もしかすると、Apple DeveloperのCertificates, Identifiers & Profilesの画面でもAssociated Domainsの有効化が必要かもしれません。

### 2. apple-app-site-associationファイルの作成 

ドメインのルートまたは.well-knownフォルダにapple-app-site-associationファイルを配置します。

ファイルの内容は以下のように設定します。

apple-app-site-association
```json
{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appIDs": ["<my-app-prefix>.<my-app-bundle-id>"],
        "components": [
          {
            "/": "/open-ios-app.php",
            "query": {
              "roomID": "*"
            }
          },
          {
            "/": "/room/*"
          }
        ]
      }
    ]
  }
}
```

* \<my-app-prefix\>には、Apple Developerで作成したアカウントのTeam IDを入力します
* \<my-app-bundle-id\>には、Apple DeveloperのCertificates, Identifiers & Profilesで作成したApp ID設定のうちのBundle IDを入力します
* componentsフィールドには、ユニバーサルリンクが適用されるパスの設定を行います
 
今回、アプリまたはWebサイトにリダイレクトさせるためのスクリプトとして「open-ios-app.php」を用意しました。また、アプリの「部屋」に直接入るための設定として、/room/*へのリンクもcomponentsフィールドに含めましたが、どちらかの設定は不要だったかもしれません。

ここにTeam IDやBundle IDを入力していることで、iOSのデバイスにアプリがインストールされている場合、それを認識することができるようです。（AppStoreがapple-app-site-associationを読みに言っているようです）

今回私は、
https://fun-planning-poker.net/.well-known/apple-app-site-association
という風にapple-app-site-associationファイルを設置しました。

### 3. サーバー側のスクリプト設定

ユーザーがユニバーサルリンクのURLにアクセスした後、適切にリダイレクトするためのPHPスクリプトを作成します。

例: open-ios-app.php
```PHP
<?php
if (!isset($_GET['roomID'])) {
    header("HTTP/1.1 400 Bad Request");
    echo 'Room ID is required';
    exit();
}

$roomID = htmlspecialchars($_GET['roomID']);
$redirectUrl = "https://fun-planning-poker.net/room/" . $roomID;

header("Location: " . $redirectUrl);
exit();
?>
```

今回私は、
https://fun-planning-poker.net/open-ios-app.php
という風にユニバーサルリンクを処理するためのスクリプトファイルを設置しました。

アプリの開発はFlutterで行っており、アプリの部屋番号に直接入室するためのQRコード形式のリンクとして、以下のようなコードを書きました。

room_screen.dart
```dart
QrImageView(
  data: 'https://fun-planning-poker.net/open-ios-app.php?roomID=${widget.roomID}',
  version: QrVersions.auto,
  size: 64.0,
),
```

## 問題と解決策

### 問題： アプリがインストールされていない場合のリダイレクト

アプリがインストールされていない場合、
https://fun-planning-poker.net/room/<roomID>
へのリクエストに対して、404エラーが返ってきました。

### 解決策


open-ios-app.phpを以下のように、2秒後にApp Storeへリダイレクトするように変更することで、解決しました。

アプリがインストールされていない場合の処理を開発者側で仕掛ける必要があるようでした。

open-ios-app.php
```PHP
<?php
if (!isset($_GET['roomID'])) {
	header("HTTP/1.1 400 Bad Request");
	echo 'Room ID is required';
	exit();
}

$roomID = htmlspecialchars($_GET['roomID']);

// App StoreのURL
$appStoreUrl = "https://apps.apple.com/jp/app/%E3%83%97%E3%83%A9%E3%83%B3%E3%83%8B%E3%83%B3%E3%82%B0%E3%83%9D%E3%83%BC%E3%82%AB%E3%83%BC/id6503992242";

// アプリのユニバーサルリンク
$redirectUrl = "https://fun-planning-poker.net/room/" . $roomID;

// JavaScriptによるリダイレクト
echo "
<!DOCTYPE html lang='ja'>
<html>
<head>
	<title>Redirecting...</title>
	<script type='text/javascript'>
		function openApp() {
			var iframe = document.createElement('iframe');
			iframe.style.display = 'none';
			iframe.src = '$redirectUrl';
			document.body.appendChild(iframe);

			setTimeout(function() {
				window.location = '$appStoreUrl';
			}, 2000);
		}
	</script>
</head>
<body onload='openApp()'>
	<p>Redirecting...</p>
</body>
</html>";
?>

```

### まとめ

ChatGPTやGitHub Copilotに頼って、腑に落ちていないまま理解を進めようとしたことにより、問題解決に時間がかかりました。

AIからの回答がどちらも、1つ目のopen-ios-app.phpのようなコードであったため、アプリがインストールされていない場合を開発者が意識しなくてもアプリインストール画面に遷移する仕組みだと理解してしまいました。

ユニバーサルリンクは、アプリがインストールされている場合、そうでない場合の両方に対応できるという理解は正しいと思いますが、そのためには、サーバー側のスクリプトによって両方の場合に対応したコードを書く必要があるようです。

この記事に対して、抜け漏れなどの指摘をいただけると嬉しいです。