URLスキーム・独自ディープリンク実装に代わる、Universal Links(iOS 9で導入)でより良いUXを実現


iOS 9で、`Universal Links`という仕組みが導入されます。

WWDC資料:
[Seamless Linking to Your App - WWDC 2015 - Videos - Apple Developer](https://developer.apple.com/videos/wwdc/2015/?id=509)

これまでのURLスキーム・独自ディープリンク実装と機能的に似ていますが、それがAPIとして公式にサポートされ、より良いUXが提供出来るようになる模様です。

Search API(特にWeb Markup API)と同時にiOS 9対応に向けてWeb側で是非対応検討しておいた方が良い事項だと思っています:
[searchapi - iOS 9の「Search API Best Practices and FAQs」が公開されたので読み解いてみた - Qiita](http://qiita.com/mono0926/items/27af791065ed919255e0)

# Universal Linksとは？

ディープリンク系の技術としてAppleから正式に提供されたもの、という理解で良さそうです。

## 通常のHTTPのリンク経由でアプリを起動出来る

Web・iOSアプリともに対応している状態で、Webの特定コンテンツのURLにアクセスすると、こういう挙動となります。

- アプリがインストール済み: 直接アプリに飛んでコンテンツが表示される
- アプリが未インストール: 通常通りWebページが開く

iOS 8までのカスタムURLスキーム(`player-ookami://some-content`など)のようなアプリ起動専用のURLが不要で、そのコンテンツのWeb URL1つに統一されます。

例えば、`http://some-site/some-content` はある特定コンテンツを一意に指すもので、Webサイトが開かれるかもしれないしアプリが開かれるかもしれない、みたいなイメージです。
まさに`Universal Links`という名前通りですね。

## UX上の大きな違い

これまでのカスタムURLスキームの仕組みを使ったディープリンク実装では、一旦コンテンツのWebページに飛んでからアプリに自動遷移 or 「アプリで開く」ボタンなどで開くという挙動でした。
(自動遷移は、せっかく読もうとしたところで画面が慌ただしく動く感じで好きじゃ無い。お行儀良くないと思っている。)

一方、`Universal Links`の場合、Webサイトのコンテンツにアクセスしようとすると即座にアプリにシームレスに遷移し、その間にWebページなどが挟まれません。

例えば、WWDCサイトを見ていて`Universal Links`対応のサイトのリンクをタップすると、以下のような挙動になります。

- もしWWDCアプリがインストールされていたら即座にアプリに移り、そのコンテンツに対応する画面が表示される
- アプリがインストールされていなければ、普通にWebサイト上でそのコンテンツのページに遷移する

![Artboard 1.png](https://qiita-image-store.s3.amazonaws.com/0/19398/ab11a7ff-224f-e2ef-8901-77e5ad1f365f.png "Artboard 1.png")



勝手にアプリに遷移すると煩わしいのでは？と思うかもしれませんが、iOS 9ではアプリ間の遷移がPUSH遷移ぽくなっていて、またステータスバー左に戻るボタンが付く、という対応とセットなので問題無いと思っています。
「Webで直接開いた方が良いのに」と思われないように、スムーズにコンテンツを表示することがアプリには求められますが(　´･‿･｀)

## カスタムURLスキームと違って一意性が担保できる

カスタムURLスキームは、アプリ同士で重複することがシステム上許されてしまっていて、また被った時にどちらが呼び出されるか不定となっています。(後勝ち説などありますが、仕様的には不定)

例えば、[今開発中のPlayer!](https://itunes.apple.com/jp/app/id897872395)は、本当は`player://`にしたいところですが、衝突を避けて`player-ookami://`にしています。これも衝突を避けやすくはなりますが、確率の問題で、これでも衝突し得ます。

例えば、LINEのURLスキームは`line://`ですが、これをカスタムURLスキームとして宣言したアプリをリリースしてLINEヘの遷移を奪うことも出来ちゃいます(有名なのでもしかしたらこれは例外的にレビューで弾かれるかもしれないけど)。

一方、`Universal Links`はhttpリンクがキーとなっていて、それはドメインで一意性が担保されるので重複が起こりえなくなります。

# `Universal Links`対応手順

さっくり以下の対応が必要となります。

- Webサイト
	- 独自ドメインに配置
	- SSL対応
	- 署名したJSONファイル配置
- iOSアプリ
	- iOS 9以上
	- Xcode 7・iOS 9 SDKでビルド
	- Capabilities設定
	- AppDelegateでハンドリング

## 1. iOSアプリでCapabilitiesの設定

Capabilitiesの設定で、`Associated Domains’のDomainsに設定します。

![Screen Shot 2015-07-06 at 5.37.01 AM.png](https://qiita-image-store.s3.amazonaws.com/0/19398/69000857-9a79-44fd-3112-dcb67808ac5f.png "Screen Shot 2015-07-06 at 5.37.01 AM.png")

ドメインに`applinks:`の接頭辞を付けたものを記述していきます。

例:
https://www.facebook.com/ -> `applinks:facebook.com`

サブドメイン、`www`の有無などの揺れも漏れなく記載する必要があるので注意です。

ここに記述したサイトの`apple-app-site-association`ファイル(次の手順で説明)が、アプリ初回起動時にダウンロードされるようです。

## 2. `apple-app-site-association`というファイルをWebサイトに配置

Webサイトは、SSL対応してある必要があります。
Qiitaはこのままじゃダメっぽいですね(　´･‿･｀)

まず以下のようなJSONファイルを用意します。

```json
{
  "applinks": {
    "apps": [],
    "details": {
      "TBEJCS6FFP.com.domain.App": {
        "paths":[ "*" ]
      }
    }
  }
}
```

`paths`には、対応しているパスを記述しましょう。
全指定は、`"*"`です。
部分指定したい場合は、`["/wwdc/news/", /videos/wwdc/2015/*]`のように指定しましょう。

`TBEJCS6FFP.com.domain.App`部分は、`Team ID`と`Bundle Identifier`を連結したものです。


`apps`には何を指定すれば良いのか謎です(　´･‿･｀)

### 署名して配置

JSONファイルをSSL証明書で署名する必要があります。

これで、署名された`apple-app-site-association`が生成されるのでWebサイトのルートに配置しましょう。

```
openssl smime \
    -sign \
    -nodetach \
    -in "unsigned.json" \
    -out "apple-app-site-association" \
    -outform DER \
    -inkey "private-key.pem" \
    -signer "certificate.pem"
```
> HTTPSプロトコルでapple-app-site-associationファイルを供給する場合、MIME型がapplication/jsonであるプレーンテキストファイルであってもよく、その場合署名は必要ありません。


## 3. iOSアプリでのハンドリング

こんな感じです。

```swift
import UIKit
 
extension AppDelegate {
    func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool {
        // typeをチェック
        if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
            let webpageURL = userActivity.webpageURL!
            if !handleUniversalLink(URL: webpageURL) {
                // コンテンツをアプリで開けない場合は、Safariに戻す
                UIApplication.sharedApplication().openURL(webpageURL)
                return false
            }
            // 多分処理後にここかどこか適切な場所で呼ぶ必要あり
            restorationHandler()
        }
        return true
    }
    
    private func handleUniversalLink(URL url: NSURL) -> Bool {
        if let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: true), let host = components.host, let pathComponents = components.path?.pathComponents {
            switch host {
            case "my-domain.com":
                // ここでハンドリング
                return true
            default:
                return false
            }
            
        }
        return false
    }
}
```

- `func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool`はHandoff・Search APIでも使われるメソッドなのでtypeチェック必要
- URLパースは、[NSURLComponents](https://developer.apple.com/library/prerelease/ios//documentation/Foundation/Reference/NSURLComponents_class/index.html)を使うと便利
	- [便利クラス - NSURLComponentsとNSURLQueryItem](http://sonson.jp/blog/2015/02/12/URLComponent/)
- どうしても開けない例外系として、その場合はSafariに飛ぶようにすることが推奨
	- 無限ループにならないか気になるけど、多分遷移元見てどうにかやってくれるはず

というわけで、手順は以上です。
Web側で現状要件満たしていない場合は追加対応がちょっと面倒そうですが、それ以外はわりとすんなり組めそうですね。

# 「アプリで開く」ボタンに相当するものは？

よく「アプリで開く」ボタンのようなものが、Webのコンテンツページに配置されていますが、`Universal Links`はそのあたりの面倒は見てくれません。

このあたりは基本的にこれまでと変わらず、独自URLスキーム・ディープリンク実装か、[Smart App Banner](https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/PromotingAppswithAppBanners/PromotingAppswithAppBanners.html)を使う必要があります。

Appleは[Smart App Banner](https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/PromotingAppswithAppBanners/PromotingAppswithAppBanners.html)推しの模様です。

## Smart App Bannerのcontentのapp-argument指定が大事になってくるかも

今回この`Universal Links`や`Search API`の`Web Markup`などが出てきたことと合わせてか`app-argument`に、サイトのURLを渡すとそのURLがインデックスされるとのことです。

例:

```html
<head>
<meta name="apple-itunes-app" 
      content="app-id=640199958, app-argument=https://developer.apple.com/wwdc/schedule, affiliate-data=optionalAffiliateData">
</head>
```

`Universal Links`使っていれば、そのURLのパースと`Smart App Banner`の`app-argument`のURLパースが全く同じロジックで出来て良いですね。

Search APIについてはこちら:
[searchapi - iOS 9の「Search API Best Practices and FAQs」が公開されたので読み解いてみた - Qiita](http://qiita.com/mono0926/items/27af791065ed919255e0)

# 問題点

## iOS 9でしか使えない

これが一番悩ましいですね。
iOS 8以下にも対応する場合は、URLスキーム・独自ディープリンクと両対応することになるかと思います。

[iOS9でカスタムURLスキームの遷移に失敗するときの注意点 - Qiita](http://qiita.com/Night___/items/60355d21951cffbef51b)と合わせて、ちょっと色々考えることがありそうです。

## Webサイトに要件がある

[Universal Links対応手順](http://qiita.com/mono0926/items/2bf651246714f20df626#universal-links対応手順)に書いた通りですが、現時点で要件満たさない場合は、対応作業が必要となります。

## 新しい種類のページを作ってその時点でリリースしているiOSアプリが開けないパターンがあり得そう

非対応ページは`apple-app-site-association` で除外すれば良いですが、その最新設定ファイルをiOSアプリがまだ取得出来ていない場合、Webでそのページにアクセスされると意図せずアプリに遷移してしまい、ハンドリング出来ないことがありそうです(実装例の通りSafariに飛ばすことになる)。
設定ファイルの更新タイミングは初回起動時と[Breaking down iOS 9 Universal Links](http://blog.hokolinks.stfi.re/how-to-implement-apple-universal-links-on-ios-9/?sf=kjlnv%29)には書いてありますが、インストール直後の1回だけなのか起動する度になのか更新タイミングが気になります。
WWDC見直したり、検証してみたいところです。

# 参考資料

- [Seamless Linking to Your App - WWDC 2015 - Videos - Apple Developer](https://developer.apple.com/videos/wwdc/2015/?id=509)
- [Breaking down iOS 9 Universal Links](http://blog.hokolinks.stfi.re/how-to-implement-apple-universal-links-on-ios-9/?sf=kjlnv%29)
	- 分かりやすい！
- [HOKO - Universal Links](http://support.hokolinks.com/ios/universal-links/)
	- ディープリンクサービスサイトの`Universal Links`解説記事
- [Promoting Apps with Smart App Banners](https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/PromotingAppswithAppBanners/PromotingAppswithAppBanners.html)