UniversalLinksを実装した
仕事でUniversal Linksを実装することになったので検証環境を用意した。検証に使用したドメインは www.ikenie3.org
。Universal Links単体の実装というよりはHandoffとUniversal Linksの設定を同時に行っていく流れになりました。雑なので他の人が見ても意味がわからないかもしれないレベルのメモです。
作業内容
1. サーバ: https化
まず http://www.ikenie3.org をhttps化した。
が、必須ではなかったらしい。
URLがhttpsに対応している、もしくはUniversal Linksで使用するJSONファイルが署名されていることが条件ということでした。
証明書に関してはHandoffProgrammingGuideに指定の記載があります。
その際、iOSが信頼する認証局 (http://support.apple.com/kb/ht5012に列挙されている認証局)が発行した証明書とキーを、IDとして 指定します
You can perform this task with Terminal commands such as those shown in Listing 2-9, removing the white space from the text for ease of manipulation, and using the openssl command with the certificate and key for an identity issued by a certificate authority trusted by iOS
- iOS で利用できる信頼されたルート証明書の一覧
- iOS 9 で利用できる信頼されたルート証明書の一覧
- iOS 8 で利用できる信頼されたルート証明書の一覧
- iOS 7 で利用できる信頼されたルート証明書の一覧
Let's Encrypt
を使用してSSL対応しましたが、 Let's Encrypt
はiOSに信頼されていませんでした。SSLの勉強になった以外の価値はなかったです。
2. アプリ: アプリを作成する環境を作る
まずはAppleのDeveloperCenterで作業
- 検証用のApp IDを作成する(
org.ikenie3.linkstest
というバンドルIDにしました) Identifiers > App IDs
のApp ID一覧から作成したApp IDをクリックすると名前など詳細が表示されるので、Prefix
とID
をメモしておく(下の画像参考)- 1で作成したApp IDに紐付いたDevelopmentとAd-Hoc用Provisioning Profileを作成する
あとiTunesConnectでアプリの入れ物を作った。理由はなるべく本番環境に近づけかたったことと、AppStoreに公開不可能な状態のアプリでUniversal Linksが機能するのか不安だったので。
3. アプリ: UniversalLinks対応アプリのプロジェクトを作成
作成したバンドルIDでプロジェクトを新規作成。
3-1. Associated Domainsの設定
プロジェクトの Capabilities
で Associated Domains
を設定する。
設定のフォーマット
<service>:<fully qualified domain>[:port number]
設定したservice名
applinks
# universal linksactivitycontinuation
# handoff
このアプリでは www.ikenie3.org
を使用することにしていましたが、なんとなく www
サブドメインの無い ikenie3.org
ドメインも許可しました。
※ webcredentials
が必要という記事を見かけましたが確認した結果不要でした。
3-2. Info.plistの設定
下記を追記しました。
アプリをサービスに紐づけするための設定
だいたいコピペなので全部の意味はわかっていないです
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>NSRTFDPboardType</string>
<key>LSHandlerRank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>org.ikenie3.linkstest.rtfd</string>
</array>
<key>NSUbiquitousDocumentUserActivityType</key>
<string>org.ikenie3.linkstest.MyActivityType</string>
</dict>
</array>
URLスキームで起動できるように設定
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
<string>org.ikenie3.linkstest</string>
<key>CFBundleURLSchemes</key>
<array>
<string>org.ikenie3.linkstest</string>
</array>
</dict>
</array>
canOpenURL
対応
<key>LSApplicationQueriesSchemes</key>
<array>
<string>org.ikenie3.linkstest://</string>
</array>
Handoff対応?
Safariから起動させるときには NSUserActivityTypeBrowsingWeb
の設定が必要らしい
<key>NSUserActivityTypes</key>
<array>
<string>NSUserActivityTypeBrowsingWeb</string>
<string>org.ikenie3.linkstest</string>
</array>
最終的にInfo.plistはこうなりました。
3-3. AppDelegateでDelegateメソッドを実装
使いそうなメソッドは必須でなくても実装
//================================
// MARK: --- Handoff
//================================
func application(application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool {
print("time: \(NSDate()), function: \(#function), file: \(#file), line: \(#line)")
return true
}
func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool {
print("time: \(NSDate()), function: \(#function), file: \(#file), line: \(#line)")
if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
let webURL = userActivity.webpageURL!
print(webURL)
}
return true
}
func application(application: UIApplication, didFailToContinueUserActivityWithType userActivityType: String, error: NSError) {
print("time: \(NSDate()), function: \(#function), file: \(#file), line: \(#line)")
}
func application(application: UIApplication, didUpdateUserActivity userActivity: NSUserActivity) {
print("time: \(NSDate()), function: \(#function), file: \(#file), line: \(#line)")
}
4. サーバ: JSONファイル
Universal LinksとHandoffに対応したWebサイトには、apple-app-site-association
というJSONの設置が必須です。
4−1. apple-app-site-associationを書く
中身はこうなりました。正直なところ、色々と調べていたら何が正解なのかわからなくてHandoffとUniversalLinksで使われていた設定を全部書いたというところなので、あとはもうちょっと設定を絞って動作確認が必要です。
apple-app-site-association.json
というファイルで作成しました。
{
"activitycontinuation": {
"apps": [
"6Z392279MA.org.ikenie3.linkstest"
]
},
"applinks": {
"apps": [],
"details": [
{
"appID": "6Z392279MA.org.ikenie3.linkstest",
"paths": [
"/app/*",
"/links/foo"
]
}
]
}
}
6Z392279MA.org.ikenie3.linkstest
これは<TeamID>.<App Bundle Identifier>
みたいに書かれていますが、TeamIDというか[アプリ: アプリを作成する環境を作る]の箇所で作成したApp IDのPrefixが入ります。
4−2. apple-app-site-associationを署名する
Appleを認証局として作られた証明書を使用してJSONファイルを署名します。
- KeychainAccess.appで
iPhone Distribution
の証明書をp12形式で書き出す - 書き出したp12ファイルを元に
openssl
コマンドで証明書と秘密鍵を作成する - 作成した証明書と秘密鍵を使用して
apple-app-site-association.json
を署名する
p12ファイルから証明書と秘密鍵を作成して、JSONファイルを署名する部分はスクリプトにしました。何度もコマンド叩くことになりそうだったので。
#!/bin/sh
#
# Handoffで使用するapple-app-site-associationを作成するためのスクリプト
#
JSON="apple-app-site-association.json"
ASSOCIATION="apple-app-site-association"
NAME="ikenie3.org.dist" # p12ファイルの'.p12'を除いた部分
P12=$NAME.p12
CERT=$NAME.cert
KEY=$NAME.key
# INTERMEDIATE=$NAME-intermediate.cert
# 証明書の書き出し
openssl pkcs12 -in $P12 -clcerts -nokeys -out $CERT
# 秘密鍵の書き出し
openssl pkcs12 -in $P12 -nocerts -nodes -out $KEY
# 中間証明書の書き出し
# openssl pkcs12 -in $P12 -cacerts -nokeys -out $INTERMEDIATE
# AppleのHandoffのドキュメントにコマンドが書かれている
# https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/Handoff/AdoptingHandoff/AdoptingHandoff.html#//apple_ref/doc/uid/TP40014338-CH2-SW13
#
cat $JSON | openssl smime -sign -inkey $KEY \
-signer $CERT \
-certfile $CERT \
-noattr -nodetach \
-outform DER > $ASSOCIATION
apple-app-site-association 取得の流れ
apple-app-site-association 取得の流れ
- Associated Domainsが指定されたアプリがインストール(含アップデート)される
- アプリは指定されたドメインの apple-app-site-association を読みに行く
apple-app-site-associationの配置場所はルート( /
)またはルート直下の /.well-known
配下ということでした。nginxのログを見ると先に /.well-known/apple-app-site-association
見つからない場合は /apple-app-site-association
にリクエストする流れでした。
124.219.180.132 - - [05/Sep/2016:13:30:38 +0900] "GET /.well-known/apple-app-site-association HTTP/1.1" 404 169 "-" "swcd (unknown version) CFNetwork/758.5.3 Darwin/15.6.0"
124.219.180.132 - - [05/Sep/2016:13:30:38 +0900] "GET /apple-app-site-association HTTP/1.1" 200 3765 "-" "swcd (unknown version) CFNetwork/758.5.3 Darwin/15.6.0"
なのでnginx側の設定は /.well-known/apple-app-site-association
だけあれば無駄なエラーログも出なくて済みそうです(両方書いてもいいけど。
Nginxの設定はこんな感じです。今回はJSONを署名しているので MIMEをapplication/pkcs7-mime
に指定しています。
# Handoff/UniversalLinks
location /.well-known/apple-app-site-association {
default_type application/pkcs7-mime;
# default_type application/json;
alias /path/to/apple-app-site-association;
}
あとはAppleが提供するバリデーションツールでバリデーションをしてあげてもいいのですが、ここはキャッシュがあったりするので最終的には使用しませんでした。
これでHandoffとUniversal Linksの対応ができました。
Handoff対応をした場合の懸念点
1つ気になる記事がありました。
クックパッドのエンジニアブログです。
Handoffの問題点
アプリがリリースされた翌日、12/05 の 0:00 に サーバーへのアクセスが急増し、捌き切れない状態になりました。 調査したところ、その1秒くらいの間に "/apple-app-site-association" というファイルに対して、普段の40倍くらいのアクセスがありました。
とのことです。ここだけはサーバサイドのエンジニアなどと協議なりする必要がありそうです。
その他全体的に参考にしたWebページ
- http://tech.pepabo.com/2015/12/06/apple-app-site-association-for-universal-links/
- http://techlife.cookpad.com/entry/2015/10/21/120000
- http://techlife.cookpad.com/entry/2015/06/05/110000
- http://techdev.seesaa.net/article/438847064.html
動画とりました。動画は不慣れなのでうまく無い展開になりました(汗
ホーム画面 → ロック画面 → Handoffで起動 → Safariを起動 → Universal Linksでアプリを起動