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

Let's Encrypt を使用してSSL対応しましたが、 Let's Encrypt はiOSに信頼されていませんでした。SSLの勉強になった以外の価値はなかったです。

2. アプリ: アプリを作成する環境を作る

まずはAppleのDeveloperCenterで作業

  1. 検証用のApp IDを作成する(org.ikenie3.linkstest というバンドルIDにしました)
  2. Identifiers > App IDs のApp ID一覧から作成したApp IDをクリックすると名前など詳細が表示されるので、 PrefixID をメモしておく(下の画像参考)
  3. 1で作成したApp IDに紐付いたDevelopmentとAd-Hoc用Provisioning Profileを作成する

あとiTunesConnectでアプリの入れ物を作った。理由はなるべく本番環境に近づけかたったことと、AppStoreに公開不可能な状態のアプリでUniversal Linksが機能するのか不安だったので。

作成したバンドルIDでプロジェクトを新規作成。

3-1. Associated Domainsの設定

プロジェクトの CapabilitiesAssociated Domains を設定する。

参考

設定のフォーマット

<service>:<fully qualified domain>[:port number]

設定したservice名

  1. applinks # universal links
  2. activitycontinuation # 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ファイルを署名します。

  1. KeychainAccess.appで iPhone Distribution の証明書をp12形式で書き出す
  2. 書き出したp12ファイルを元に openssl コマンドで証明書と秘密鍵を作成する
  3. 作成した証明書と秘密鍵を使用して 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 取得の流れ

  1. Associated Domainsが指定されたアプリがインストール(含アップデート)される
  2. アプリは指定されたドメインの 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ページ

動画とりました。動画は不慣れなのでうまく無い展開になりました(汗

ホーム画面 → ロック画面 → Handoffで起動 → Safariを起動 → Universal Linksでアプリを起動