iOS Deferred Deep Link|Swiftで実現する遅延深度連結の完全ガイド
iOSアプリの遅延深度連結をSwiftで実装し、ユーザーの離脱を防ぐシームレスな遷移を実現。全シナリオ対応の転送フロー設計でコンバージョン率を最大化します。
本記事は AI による翻訳をもとに作成されています。表現が不自然な箇所がありましたら、ぜひコメントでお知らせください。
記事一覧
iOS Deferred Deep Link 遅延ディープリンク実装(Swift)
あらゆるシーンに対応し、中断しないApp遷移フローを自分で作る
[2022/07/22] iOS 16の今後の変更点の更新
iOS ≥ 16 から、ユーザーが自発的に貼り付け操作を行わない限り、アプリが剪貼簿を読み取ると確認ダイアログが表示され、ユーザーが許可しないとアプリは剪貼簿の情報を取得できません。
UIPasteBoardのiOS 16におけるプライバシー変更
[2020/07/02] 更新
無関係
卒業して兵役を終えてから、なんとなく働き続けてもうすぐ3年。成長は頭打ちになり、居心地の良い環境に入ってしまった。幸いにも思い切って退職を決め、気持ちを整理して再スタートを切った。
做自己的生命設計師を読んで自分の人生設計を見直す中で、仕事や人生を振り返りました。技術力はあまり高くないですが、Mediumで皆と共有することで「フロー状態」に入り、多くのエネルギーを得られます。ちょうど最近、友人からDeep Linkの質問があったので、私が調べた方法を整理しつつ、自分のエネルギーも補充しました!
シナリオ
まずは実際の利用シーンについて説明します。
- ユーザーがアプリをインストールしている場合、URLリンク(Google検索、Facebook投稿、LINEリンクなど)をクリックすると、直接アプリが起動して目的の画面を表示します。インストールしていない場合はApp Storeに遷移してアプリをインストールします。インストール後にアプリを開いた際、以前にアクセスしようとした画面を再現できるようにします。
2.APPのダウンロードと起動データ追跡では、APPのプロモーションリンクから実際に何人がこの入口を通じてAPPをダウンロードし起動したかを知りたいです。
- 特別なイベント入口、例えば特定のURLからダウンロードして開くと報酬がもらえる場合。
サポート状況:
iOS ≥ 9
Deferred Deep Link と Deep Link の違いとは?
純粋な Deep Link 自体:
iOS Deep Linkの仕組みは、アプリがインストールされているかどうかを判定し、インストールされていればアプリを開き、されていなければ何もしないという動作のみです。
まず「未インストールの場合はApp Storeへ遷移する」ことをユーザーに促す処理を追加します:
URL Scheme の部分はシステムが制御しており、通常はアプリ内での呼び出しに使われ、あまり公開されていません。なぜなら、トリガーが自分で制御できない場所(例:Lineのリンク)にある場合、対応できないからです。
自身のウェブページでトリガーがある場合は、いくつかの小技を使って対応できます。詳しくはこちらを参照してください:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<html>
<head>
<title>リダイレクト中...</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script>
var appurl = 'marry://open';
var appstore = 'https://apps.apple.com/tw/app/%E7%B5%90%E5%A9%9A%E5%90%A7-%E6%9C%80%E5%A4%A7%E5%A9%9A%E7%A6%AE%E7%B1%8C%E5%82%99app/id1356057329';
var timeout;
function start() {
window.location = appurl;
timeout = setTimeout(function(){
if(confirm('結婚吧アプリをすぐにインストールしますか?')){
document.location = appstore;
}
}, 1000);
}
window.onload = function() {
start()
}
</script>
</head>
<body>
</body>
</html>
大まかなロジックは 同じくURL Schemeを呼び出し、タイムアウトを設定し、時間内に遷移しなければインストールされていないと判断してSchemeが呼べなかったものとして、代わりにApp Storeのページへ遷移させる というものです(ただし体験はあまり良くなく、URLエラーの表示が出ることがありますが、自動リダイレクトが追加されます)。
Universal Link 自体が独自のウェブページであり、遷移がなければデフォルトでウェブブラウザで表示されます。ここでウェブサービスがあれば直接ウェブに遷移し、なければ直接App Storeのページに誘導します。
ウェブサービスのサイトは <head></head> 内に以下を追加できます:
1
<meta name="apple-itunes-app" content="app-id=APPID, app-argument=ページパラメータ">
iPhoneのSafariでウェブ版を閲覧すると、上部にAPPインストールの案内や「APPでこのページを開く」ボタンが表示されます。パラメータ app-argument はページの値を渡し、APPに伝達するために使われます。
「無ければAPP Storeに遷移する」フローチャート
Deep LinkのAPP側処理の改善:
私たちが求めているのはもちろん「ユーザーがアプリをインストールしていればアプリを開く」だけではありません。ソース情報をアプリと連携させ、アプリ起動後に自動的に目的のページを表示させることも必要です。
URL Scheme方式はAppDelegateのfunc application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool内で処理できます:
1
2
3
4
5
6
7
8
9
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
if url.scheme == "marry",let params = url.queryParameters {
if params["type"] == "topic" {
let VC = TopicViewController(topicID:params["id"])
UIApplication.shared.keyWindow?.rootViewController?.present(VC,animated: true) // ビューコントローラーを表示する
}
}
return true
}
Universal Link は AppDelegate の func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool 内で処理します:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
extension URL {
/// test=1&a=b&c=d => ["test":"1","a":"b","c":"d"]
/// URLのクエリを解析して[String: String]の辞書に変換する
public var queryParameters: [String: String]? {
guard let components = URLComponents(url: self, resolvingAgainstBaseURL: true), let queryItems = components.queryItems else {
return nil
}
var parameters = [String: String]()
for item in queryItems {
parameters[item.name] = item.value
}
return parameters
}
}
まず、URLの拡張メソッド queryParameters を示します。これにより、URLのクエリをSwiftのDictionaryに簡単に変換できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb, webpageURL = userActivity.webpageURL {
/// universal link URLの場合...
let params = webpageURL.queryParameters
if params["type"] == "topic" {
let VC = TopicViewController(topicID:params["id"])
UIApplication.shared.keyWindow?.rootViewController?.present(VC,animated: true)
}
}
return true
}
完了!
それで何が足りない?
現時点でほぼ完璧に仕上がっており、発生しうるすべての状況に対応していますが、まだ何が不足しているのでしょうか?
図のように、未インストール → App Storeでインストール → App Storeから起動の場合、元の情報が途切れてしまい、アプリは起動元を認識できずトップページのみ表示されます。ユーザーは再度前のページに戻ってもう一度開く操作をしなければ、アプリはページ遷移を起動しません。
この方法でも不可能ではありませんが、離脱率を考えるとステップが増えるごとに離脱も増え、ユーザー体験もスムーズではありません。ましてやユーザーが必ずしも賢いとは限りません。
本文のポイントへ進む
何謂 Deferred Deep Link?遅延ディープリンクとは、Deep LinkをApp Storeでのインストール後も元の情報を保持できるようにする仕組みです。
Androidエンジニアによると、Android自体にはこの機能がありますが、iOSではこの設定がサポートされておらず、この機能を実現する方法もあまり使いやすくありません。続きをご覧ください。
Deferred Deep Link(ディファードディープリンク)
もし自分で作る時間をかけたくない場合は、直接 branch.io や Firebase Dynamic Links を利用できます。この記事で紹介した方法はFirebaseの使い方です。
Deferred Deep Linkの効果を実現する方法は、ネット上で主に2つあります:
一つはユーザーのデバイス、IP、環境などのパラメータを使ってハッシュ値を計算し、ウェブ側でサーバーにデータを保存します。アプリがインストールされて起動した際に同じ方法で計算し、値が一致すればデータを取得して復元します(branch.ioの方法)。
もう一つは、本記事で紹介する方法で、Firebaseの方法と同様に、iPhoneのクリップボードとSafariとAPPのCookie共有機能を利用する方法です。データをクリップボードやCookieに保存し、APPインストール後に読み取って使用します。
1
2
「Open」をクリックすると、JavaScriptが自動的にクリップボードを上書きして、遷移に必要な情報をコピーします:https://XXX.app.goo.gl/?link=https://XXX.net/topicID=1&type=topic
Firebase Dynamic Links を使ったことがある方なら、この開くための遷移ページに馴染みがあるはずです。仕組みを理解すれば、このページがプロセスから外せないことがわかります!
また、Firebaseはスタイルの変更を提供していません。
サポート状況
まずは落とし穴について、対応状況の問題です。前述のように「不親切」な点です!
もしAPPがiOS 10以上のみ対応であれば、はるかに簡単です。APPはクリップボードのアクセスを実装し、WebはJavaScriptで情報をクリップボードに上書きし、その後APP Storeへ遷移してダウンロードさせるだけで済みます。
iOS = 9 はJavaScriptによる自動クリップボード操作をサポートしていませんが、SafariとAPPのSFSafariViewController間での「Cookie共有技術」をサポートしています。
また、APPはバックグラウンドでこっそりSFSafariViewControllerを追加してWebを読み込み、先ほどリンクをクリックしたときに保存したCookie情報をWebから取得する必要があります。
手順が複雑で、リンクのクリックはSafariブラウザのみ対応。
公式ドキュメントによると、iOS 11以降はユーザーのSafari Cookieを取得できません。この場合はSFAuthenticationSessionを使用できますが、この方法はバックグラウンドでの実行ができず、読み込みのたびに以下の確認ダイアログが表示されます:
SFAuthenticationSession の認証ダイアログ
また、APP審査ではSFSafariViewControllerをユーザーに見えない場所に配置することは許可されていません。(プログラムでトリガーしてからaddSubviewするのは発見されにくいですが)
実践してみよう
まず簡単に、iOS 10以上のユーザーのみを対象に、iPhoneのクリップボードを使って情報を転送する方法について説明します。
Web 側:
Firebase Dynamic Linksを参考にして独自のページをカスタマイズし、clipboard.jsというライブラリを使って、ユーザーが「今すぐ移動」をクリックしたときに、APPに渡す情報(marry://topicID=1&type=topic)をまずクリップボードにコピーし、その後location.hrefでApp Storeのページに遷移させています。
APP 側:
AppDelegate またはメインの UIViewController でクリップボードの値を読み取る:
let pasteData = UIPasteboard.general.string // クリップボードの文字列を取得する
こちらでは情報をURLスキーム方式で包むことを推奨します。識別やデータの復号が容易になるためです:
1
2
3
4
5
6
if let pasteData = UIPasteboard.general.string,let url = URL(string: pasteData),url.scheme == "marry",let params = url.queryParameters {
if params["type"] == "topic" {
let VC = TopicViewController(topicID:params["id"])
UIApplication.shared.keyWindow?.rootViewController?.present(VC,animated: true)
}
}
最後に処理が完了したら、UIPasteboard.general.string = “” を使ってクリップボードの情報をクリアします。
実践 — iOS 9 バージョン対応
面倒なことに、iOS 9をサポートする場合、前述の通りクリップボードが使えないため、Cookie共有の方法を使う必要があります。
Web側:
web側も扱いやすく、ユーザーが「今すぐ行く」をクリックしたときに、APPに渡したい情報をCookieに保存します(marry://topicID=1&type=topic)。その後、location.hrefでApp Storeのページに遷移します。
ここでは、Cookieを処理するための2つのパッケージ化されたJavaScriptメソッドを提供し、開発を加速します:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// name: Cookie 名称
/// val: Cookie 値
/// day: Cookie 有効期限、デフォルトは1日
/// EX1: setcookie("iosDeepLinkData","marry://topicID=1&type=topic")
/// EX2: setcookie("hey","hi",365) = 1年間有効
function setcookie(name, val, day) {
var exdate = new Date();
day = day \\|\\| 1;
exdate.setDate(exdate.getDate() + day);
document.cookie = "" + name + "=" + val + ";expires=" + exdate.toGMTString();
}
/// getCookie("iosDeepLinkData") => marry://topicID=1&type=topic
function getCookie(name) {
var arr = document.cookie.match(new RegExp("(^\\| )" + name + "=([^;]*)(;\\|$)"));
if (arr != null) return decodeURI(arr[2]);
return null;
}
APP 側:
本文で最も厄介な部分がやってきました。
前述の原理に触れましたが、メインページのUIViewControllerでプログラムを使ってこっそりSFSafariViewControllerを背景に読み込み、ユーザーに気付かれないようにします。
もう一つの落とし穴: こっそり読み込む場合、iOS ≥ 10 の SFSafariViewController は、Viewのサイズが1未満、透明度が0.05未満、または isHidden に設定されていると、SFSafariViewController は 読み込まれません 。
p.s iOS = 10 は Cookie とクリップボードの両方をサポートしています。
こちらの方法は、メインの UIViewController の上部に UIView を設置し、適当な高さを設定しますが、その下端をメイン UIView の上端に合わせます。そして IBOutlet(sharedCookieView)をクラスに接続します。viewDidLoad() 内で SFSafariViewController を初期化し、そのビューを sharedCookieView に追加します。これにより、実際には表示されて読み込まれていますが、画面には表示されずユーザーは見えません🌝。
SFSafariViewController の URL はどこを指すべきか?
Web側の共有ページと同様に、Cookieを読み取るためのページをもう一つ作成し、クロスドメインのCookie問題を避けるために両方のページを同じドメインに配置します。ページの内容は後ほど添付します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@IBOutlet weak var SharedCookieView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string:"http://app.marry.com.tw/loadCookie.html")
let sharedCookieViewController = SFSafariViewController(url: url)
VC.view.frame = CGRect(x: 0, y: 0, width: 200, height: 200) // ビューのフレームを設定
sharedCookieViewController.delegate = self // デリゲートを設定
self.addChildViewController(sharedCookieViewController) // 子ビューコントローラを追加
self.SharedCookieView.addSubview(sharedCookieViewController.view) // サブビューとして追加
sharedCookieViewController.beginAppearanceTransition(true, animated: false) // 表示開始のトランジション
sharedCookieViewController.didMove(toParentViewController: self) // 親ビューコントローラへの移動完了
sharedCookieViewController.endAppearanceTransition() // 表示終了のトランジション
}
sharedCookieViewController.delegate = self // デリゲートを自分自身に設定する
class HomeViewController: UIViewController, SFSafariViewControllerDelegate
この Delegate を追加することで、読み込み完了後のコールバック処理をキャッチできます。
私たちは以下の場所でできます:
func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) {
方法内で読み込み完了イベントをキャッチする。
ここまで来たら、次は didCompleteInitialLoad 内でウェブページの Cookie を読み取るだけで完了です!
ここではSFSafariViewControllerのCookieを読み取る方法が見つかりませんでした。ネット上の方法を試してもすべて空でした。
場合によっては、JavaScriptとページ内容を連携させ、JavaScriptにCookieを読み取らせてUIViewControllerに返す必要があります。
TrickyなURL Schemeの方法
iOSが共有されたCookieを取得できないので、「Cookieを読み取るページ」に直接「Cookieの読み取り」を任せましょう。
前文で紹介したJavaScriptのCookie処理メソッドgetCookie()はここで使います。私たちの「Cookieを読み取るページ」の内容は空白ページです(ユーザーは見えません)が、JavaScript部分ではbodyのonload後にCookieを読み取ります:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<html>
<head>
<title>iOSディープリンク保存クッキーを読み込み中...</title>
<script>
function checkCookie() {
var iOSDeepLinkData = getCookie("iOSDeepLinkData");
if (iOSDeepLinkData && iOSDeepLinkData != '') {
setcookie("iOSDeepLinkData", "", -1);
window.location.href = iOSDeepLinkData; /// marry://topicID=1&type=topic
}
}
</script>
</head>
<body onload="checkCookie();">
</body>
</html>
実際の原理のまとめは次の通りです:HomeViewController viewDidLoad 時に SFSafariViewController を使って loadCookie.html ページをこっそり読み込みます。loadCookie.html ページは事前に保存されたCookieを読み取り確認し、あれば読み出して削除します。その後、window.location.href を使って呼び出し、URL Scheme をトリガーします。
なので、その後のコールバック処理は AppDelegate 内の func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool に戻って処理されます。
完了!まとめ:
面倒だと感じる場合は、直接 branch.io や Firebase Dynamic を使うのが良いでしょう。無駄に一から作る必要はありません。ここではインターフェースのカスタマイズや複雑な要件があるため、自作しています。
iOS=9のユーザーは非常に少なく、特に必要でなければ無視しても問題ありません。クリップボードを使う方法は速くて効率的であり、クリップボードを使うことでリンクを必ずSafariで開く必要もありません!
ご質問やご意見がございましたら、お問い合わせ ください。
Post は Medium から ZMediumToMarkdown によって変換されました。
本記事は Medium にて初公開されました(こちらからオリジナル版を確認)。ZMediumToMarkdown による自動変換・同期技術を使用しています。

{:target="_blank"}](/assets/b08ef940c196/0*E8h6Fy0H9_5jxhjV.webp)








{:target="_blank"}](/assets/b08ef940c196/1*tPXHlrQE3MdrjMzFbnS_4w.webp)


