iOS Deferred Deep Link 遅延ディープリンク実装(Swift)
あらゆるシーンに対応し、中断しないApp遷移フローを自分で作る
[2022/07/22] iOS 16の今後の変更点の更新
iOS ≥ 16 から、ユーザーが自発的にペースト操作を行わない限り、Appが自らクリップボードを読み取ると確認ダイアログが表示され、ユーザーが許可を押さないとクリップボードの情報を取得できません。

UIPasteBoardのiOS 16におけるプライバシー変更
[2020/07/02] 更新
無関係
卒業して兵役を終えてからほぼ3年間、平凡に働いてきましたが、成長は鈍化し、快適ゾーンに入り始めていました。幸いにも思い切って退職を決意し、リセットして再出発しました。
自分の人生設計者になるを読んで人生設計を見直す中で、仕事や人生を振り返りました。技術力はあまり高くありませんが、Mediumで皆と共有することで「フロー状態」に入り、多くのエネルギーを得られます。ちょうど先日、友人からDeep Linkの質問があったので、自分の調査した方法を整理しつつ、エネルギーも補充しました!
シナリオ
まずは実際の利用シーンについて説明します。
- ユーザーがAPPをインストールしている場合、URLリンク(Google検索、Facebook投稿、Lineリンクなど)をクリックすると、直接APPが起動して目的の画面を表示します。インストールしていない場合はAPP Storeに遷移してAPPをインストールします。インストール後にAPPを開いたとき、以前に行きたかった画面を再現できること。
2.APPのダウンロードと起動データの追跡では、APPのプロモーションリンクからどれだけの人が実際にこの入口を経由してAPPをダウンロードし、起動したかを知りたいです。
- 特別なキャンペーン入口、例えば特定のURLからダウンロードして開くと報酬が得られる。
サポート状況:
iOS ≥ 9
Deferred Deep Link と Deep Link の違いとは?
純粋な Deep Link 自体:

iOS Deep Link自体の動作機構は、APPがインストールされているかどうかを判定するだけで、インストールされていればAPPを開き、されていなければ何もしません。
まずは「インストールされていなければAPP Storeへ遷移する」ことをユーザーに促す処理を追加します:
URL Scheme の部分はシステムによって制御されており、通常はアプリ内での呼び出しに使われ、あまり公開されていません。なぜなら、トリガーが自分で制御できない場所(例:Lineのリンク)にある場合、対応できないためです。
トリガーが自分のウェブページ上にある場合、いくつかの小技で対応可能です。詳細はこちらをご参照ください:
<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> 内に以下を追加できます:
<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 内で処理できます:
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 内で処理されます:
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 に変換するのに便利です。
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
}

了解しました!翻訳したいMarkdown段落を送ってください。
それで何が足りないのか?
現時点ではほぼ完璧に見えます。すべての想定される状況に対応しましたが、まだ何か足りないものはありますか?

図のように、未インストール -> 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のインストール完了後に読み取って使用する仕組みです。

「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 Scheme方式でパッケージ化することをおすすめします。これにより識別やデータの逆解析が容易になります:
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のページに遷移します。
ここでは、開発を加速するための2つのラップされたJavaScriptのCookie処理メソッドを提供します:
/// 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 を初期化し、その View を sharedCookieView に追加します。これにより実際には表示・読み込みされていますが、画面外にあるためユーザーには見えません🌝。
SFSafariViewController の URL はどこを指すべきか?
Web側の共有ページと同様に、Cookieを読み取るためのページをもう一つ作成し、クロスドメインのCookie問題を避けるために両方のページを同じドメイン内に配置します。ページの内容は後ほど添付します。
@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) // SharedCookieViewに追加
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) {
// SFSafariViewControllerの初期読み込みが完了したときに呼ばれるメソッド
方法内で読み込み完了イベントをキャッチする。
ここまで来たら、次は didCompleteInitialLoad でウェブ内のCookieを読み取るだけで完了です!
ここではSFSafariViewControllerのCookieを読み取る方法が見つかりませんでした。ネット上の方法を試してもすべて空でした。
場合によってはJavaScriptを使ってページ内容とやり取りし、JavaScriptにCookieを読み取らせてUIViewControllerに返す必要があります。
TrickyなURL Scheme方法
iOS が共有された Cookie を取得できないので、「Cookie を読むページ」に直接「Cookie を読んでもらう」ようにします。
前文で紹介したJavaScriptのCookie処理メソッドgetCookie()はここで使います。私たちの「Cookieを読み取るページ」の内容は空白ページです(ユーザーは見えません)が、JavaScript部分ではbodyのonload後にCookieを読み取ります:
<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 は ZMediumToMarkdown によって Medium から変換されました。



コメント