# iOS Universal Links 調査

Universal Linksは、AASAの取得からアプリ起動までの経路が見えにくい。 アプリが開くのは、インストール済みでAASAのパスが一致する場合だけで、それ以外はSafariでWebページが開く。

この記事では、流れを3つの図に分けて、MermaidとPlantUMLで書いた。

図 対象
技術フロー AASAの取得と関連付けから、リンクのタップ後の処理まで
呼び出しフロー 別アプリから UIApplication.open(url) で開く場合
関係図 Web Server、Apple CDN、iPhone、アプリの関係と分岐理由

# AASAを取得して関連付けてから、タップ時にパスを照合する

アプリの導入後や初回起動時に、iOSがAASA(apple-app-site-association)を取得する。 開発モードではWeb Serverから、本番ではApple CDNから取得する。 iOSはAASAを検証し、関連付けを登録する。

ユーザーがリンクをタップすると、iOSはAASAのドメインとパスを照合する。 一致すればアプリが NSUserActivity を受け取り、対象画面へ遷移する。 一致しなければSafariでWebページを開く。

flowchart TD
    A[アプリ導入後または初回起動] --> B{開発モードか}
    B -->|はい| C[Web Server から AASA を取得]
    B -->|いいえ| D[Apple CDN から AASA を取得]
    D --> E[必要に応じて CDN が Web Server から更新]
    C --> F[AASA を検証し関連付けを登録]
    D --> F
    F --> G[ユーザーが Universal Link をタップ]
    G --> H[AASA のドメインとパスを照合]
    H --> I{アプリがインストール済みで\nパスが一致するか}
    I -->|はい| J[アプリが NSUserActivity を受信]
    J --> K[DeepLinkRouter が URL を処理]
    K --> L[対象画面へ遷移]
    I -->|いいえ| M[Safari で Web ページを開く]
    M --> N[任意で Smart App Banner を表示]
    N --> O[ユーザー操作で App Store を開く]

PlantUMLの図には、AASA配信で守る条件も書いている。 HTTPSが必須で、リダイレクトせず200を直接返し、Content-Type は application/json にする。

@startuml
title Universal Links 技術フロー(関連付け~実行)

actor User
participant "Link Source\n(メッセージ/メール/SNS/ブラウザ等)" as Source
participant "iOS" as SWCD
participant "Apple CDN" as CDN
participant "Web Server\n(Origin)" as Web
participant "Safari"
participant "iOS App" as App
participant "App Store" as Store

== 初回の関連付け(アプリ導入直後/初回起動時など) ==
note right of App
Associated Domains に
applinks:example.com
(開発時は ?mode=developer 併用可)
end note

alt 開発モード(?mode=developer)
  SWCD -> Web: GET /.well-known/apple-app-site-association
  Web --> SWCD: 200 application/json(AASA)
else 本番(リリース)
  SWCD -> CDN: GET /a/v1/example.com
  CDN -> Web: (必要に応じてオリジン取得/更新)
  CDN --> SWCD: 200 application/json(AASA)
end
SWCD -> SWCD: AASA検証(appID / paths, components 等)
SWCD --> App: 関連付け結果を登録

== ユーザーがユニバーサルリンクをタップ ==
User -> Source: タップ https://example.com/...
Source -> SWCD: ユニバーサルリンク解決要求
SWCD -> SWCD: ドメイン/パスをAASAで照合

alt アプリがインストール済み かつ パス一致
  SWCD --> App: open via NSUserActivityTypeBrowsingWeb
  App -> App: DeepLinkRouter.handle(url)
  note right of App
  SceneDelegate / AppDelegate / SwiftUI の
  onContinueUserActivity で受信し画面遷移
  end note
else 未インストール または パス不一致
  SWCD --> Safari: https://example.com/... を開く
  opt Web側でSmart App Banner等を表示(任意)
    Safari -> Store: Appページを開く(ユーザー操作)
  end
end

== ユーザーの既定動作 ==
opt ユーザーが以前「Safariで開く」を選択
  note right of Safari
  同一リンクは以後Safari優先
  (長押しで「Appで開く」を再選択可)
  end note
end

== 厳守事項(AASA配信) ==
note over Web
・HTTPS必須、200直返し、拡張子なし
・Content-Type: application/json
・AASAパスでのリダイレクト禁止
end note
@enduml

# 呼び出しアプリは UIApplication.open(url) で受け入れアプリを開く

呼び出しアプリは、Universal LinkのURLを生成して UIApplication.open(url) を呼ぶ。 iOSはキャッシュ済みのAASAを照合し、必要に応じてAASAを取得する。 以降の分岐は、ユーザーがリンクをタップした場合と同じである。

flowchart TD
    A[ユーザーが呼び出しアプリのボタンをタップ] --> B[呼び出しアプリが Universal Link URL を生成]
    B --> C[UIApplication.open(url) を呼び出す]
    C --> D[iOS がキャッシュ済み AASA を照合]
    D --> E[必要に応じて AASA を取得]
    E --> F{受け入れアプリがインストール済みで\nパスが一致するか}
    D --> F
    F -->|はい| G[受け入れアプリが NSUserActivity を受信]
    G --> H[DeepLinkRouter が URL を解析]
    H --> I[対象画面へ遷移]
    F -->|いいえ| J[Safari で Web ページを開く]
    J --> K[任意で App Store のページを表示]
@startuml
title 呼び出しアプリ→ユニバーサルリンク→受け入れアプリ 呼び出しフロー

actor User
participant "呼び出しアプリ" as B
participant "iOS (System / swcd)" as iOS
participant "AASA配信\n(Apple CDN / Web)" as AASA
participant "受け入れアプリ" as A
participant "Safari" as Safari
participant "App Store" as Store

User -> B: ボタンタップ「受け入れアプリを開く」
B -> B: ユニバーサルリンクURL生成(https://example.com/...)
note right of B #FFEECC
A.要アプリ実装(ボタン処理とURL生成)
end note
B -> iOS: UIApplication.open(url)
note right of B #FFEECC
B.要アプリ実装(open呼び出しとフォールバック)
end note

iOS -> iOS: AASA照合(キャッシュ)
iOS -> AASA: ※必要時のみAASA取得(簡易)
AASA --> iOS: 200 JSON(簡易)
note over AASA #FFDDE0
F.要サーバー実装(AASAをHTTPS/200 JSONで配信)
end note

alt 受け入れアプリがインストール済み + パス一致
  iOS --> A: NSUserActivityTypeBrowsingWeb(url)
  note right of A #FFEECC
  C.要アプリ実装(URL受信と保留)
  end note

  A -> A: DeepLinkRouter.handle(url)
  note right of A #FFEECC
  D.要アプリ実装(URL解析とルーティング)
  end note

  A --> User: 対象画面を表示
  note right of A #FFEECC
  E.要アプリ実装(UI遷移の実行)
  end note
else 未インストール or パス不一致(またはSafari優先)
  iOS --> Safari: https://example.com/... を開く
  opt Web側でApp誘導(任意)
    Safari -> Store: Appページ表示(ユーザー操作)
  end
end

@enduml

# アプリが開かずWebサイトが開く理由

Webサイトが開くのは、アプリが未インストール、AASAが不一致、Safariが優先される場合などである。 ユーザーが以前「Safariで開く」を選んだリンクは、以後Safariが優先される。 長押しで「Appで開く」を選び直せる。

flowchart LR
    A[Web Server に AASA を配置] --> B[Apple CDN が AASA を取得・配信]
    C[ユーザーがリンクをタップ] --> D[iPhone が AASA を参照]
    B --> D
    D --> E{アプリがインストール済みで\nAASA が一致するか}
    E -->|はい| F[受け入れアプリを起動]
    E -->|いいえ: 未インストール・不一致・Safari 優先など| G[Web サイトを開く]
@startuml
title Universal Links 関係図(起点修正+分岐理由)

left to right direction

actor "User" as USER
rectangle "Web Server" as WEB
node "Apple CDN" as CDN
node "iPhone" as PHONE
cloud "Webサイト" as SITE
component "iPhone(受け入れアプリ)" as APP

' 事前フロー(番号外)
note left of WEB
事前:AASA配置
end note

' 実行フロー(起点)
USER --> PHONE : ① リンクをタップ

' AASA参照(キャッシュ/必要時取得)
WEB --> CDN : ②
CDN --> PHONE : ③

' 開き先(④の分岐理由を注記)
PHONE --> APP : ④
note on link #FFEECC
条件:アプリインストール済み +
AASA一致(ドメイン/パス等)
→ 受け入れアプリ起動
end note

PHONE --> SITE : ④
note on link #FFEECC
条件:未インストール/AASA不一致/
ユーザーがSafari優先を選択/
.open(universalLinksOnly)失敗時
→ Webサイトを開く
end note

@enduml

# 調査で参照した記事