iOS9から使える3D Touch・Peek and Popを実装する

iPhone 6s、iPhone6s Plusから使えるようになった3D Touch。
メールアプリやSafariなどで使うことができるPeek and Popを実装してみたいと思います。

3D Touchの機能のクイックアクションについてはこちらに書いていますので、あわせて読んでいただけると嬉しいです。

・[iOS9から使える3DTouch・クイックアクションを実装する](http://qiita.com/takashings/items/067f6a2f9910b4bd9aac)

# Peek and Popとは？
先述にありますが、iOS9から使える3D Touchの機能の1つです。
しかし、使うためにはiPhone 6s、iPhone 6s Plusなどの3D Touchに対応している機種が限定されています。

Peek and Popとは、Appleの公式のページから説明を借りると、

> PeekとPopを使うと、あらゆる種類のコンテンツを実際に開くことなくプレビューでき、さらにそこから操作もできます。例えば、受信ボックスにあるEメールを軽くプレスすると、それぞれのEメールをPeekで覗いてみることができます。そのEメールを開きたい時は、少し深めにプレスしましょう。Popでパッと開きます。
[iPhone 6s - 3D Touch](http://www.apple.com/jp/iphone-6s/3d-touch/)

イメージとしてはこんな感じです。
iPhone 6s、iPhone 6s Plusをお持ちの方は一度はやってみたことがあると思います。

<img width="742" alt="peek_and_pop_1.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/40009/eb706844-1478-49b5-63fd-03a41a8b29fc.jpeg">
画像引用：[iPhone 6s - 3D Touch](http://www.apple.com/jp/iphone-6s/3d-touch/)

メールやSafariなどのアプリで使うことができる機能です。

## Peek and Popの使いどころ
実際にアプリで実装する場合は実装前に使いどころを検討する必要があります。

まず、第一の（ほとんどの場合）必須条件としては、**Peekで表示時には次に画面（UIViewControllerクラス）遷移があることが大前提**となります。
そのため、必然的に**元の画面に戻ることができる画面設計**が大前提となります。

また、表示される画面が**多数の情報が用意されているもの**は避けた方がいいと思います。
Peek and Popはのぞき見のようなものを想定しているので、次の画面に複数の情報がちりばめられているものは好ましくないと思います。
メールアプリを例にすると、トップの「メールボックス」でのPeek and Popは使用できません。これは次に表示される画面が（複数のメールの内容で構成されている）メールの一覧画面というのが理由にあるかと思います。のぞき見の内容が大量の情報である場合は、好ましくないかと思います。

ちなみに、メールアプリで使えるPeek時の左右スワイプは今のところサードパーティアプリでの実装はできないようです。
（iOS9の次のメジャーバージョンでは使えるんでしょうかね・・・？）

## Peek and Popを実装する
では、実装してみたいと思います。
まずは``UIViewControllerPreviewingDelegate``を追加する必要があります。
そして、Peek and Popを有効にするビューを指定します。

```swift:swift
class ViewController: UIViewController, UIViewControllerPreviewingDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()

        // 3D Touchが使える端末か確認
        if self.traitCollection.forceTouchCapability == UIForceTouchCapability.Available {
            // どのビューをPeek and Popの対象にするか指定
            registerForPreviewingWithDelegate(self, sourceView: view)
        }
    }
}
```

以下のコードはUITableViewのセルを押し込むと、本来はPushで表示される次の画面を表示するというものを実装しています。

```swift:swift
    func previewingContext(previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {

        // 3D Touchの対象がUITableViewかどうかを判別（UITableViewでの位置を取得）
        guard let cellPosition : CGPoint = tableView.convertPoint(location, fromView: view) else {
            return nil
        }
        
        // 3D Touchされた場所が存在するかどうか判定
        // Peekを表示させたくない、表示すべきではない場合は"nil"を返す
        guard let indexPath : NSIndexPath = self.tableView.indexPathForRowAtPoint(cellPosition) else {
            return nil
        }

        // Peekで表示させる画面のインスタンス生成
        let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
        let ctrl : NextViewController = storyboard.instantiateViewControllerWithIdentifier("NextViewController") as! NextViewController

        // Peekで表示させるプレビュー画面の大きさを指定
        // 基本的にwidthの数値は無視される
        ctrl.preferredContentSize = CGSize(width: 0.0, height: UIScreen.mainScreen().bounds.size.height * 0.5)

        let cell : UITableViewCell = self.tableView.cellForRowAtIndexPath(indexPath)!

        // 3D Touchではっきりと表示させる部分を指定（どの部分をぼかして、どの部分をPeekしているかを設定）
        previewingContext.sourceRect = view.convertRect(cell.frame, fromView: tableView)
        
        // 次の画面のインスタンスを返す
        return ctrl
    }

    // Popする直前に呼ばれる処理（通常は次の画面を表示させる）
    // UINavigationControllerでのpushでの遷移は"showViewController:sender:"をコールする
    // Modalでの遷移の場合は"presentViewController:animated:completion:"をコールする
    func previewingContext(previewingContext: UIViewControllerPreviewing, commitViewController viewControllerToCommit: UIViewController) {
        showViewController(viewControllerToCommit, sender: self)
    }
```

次の画面へデータを受け渡す場合は``previewingContext(_:viewControllerForLocation:)``でデータを設定する必要があります。

たったこれだけでPeek and Popの実装ができました。

注意点としては以下の通りです。これらを考慮する必要があります。

+ Peekで表示させる画面への遷移前にアラートなどを挟むことはできない
+ PeekとPopで違う画面を表示することはできない（のぞき見の意味がなくなる）
+ Peek時には``viewDidAppear(animated: Bool)``などの次の画面のライフサイクルイベントが呼ばれる

# Peek quick actions
Peekの画面ではその場でショートカット操作を行える**Peek quick actions**というものがあります。

Peek画面で上へスワイプすると表示されるこの画面です。

<img width="414" alt="IMG_8102.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/40009/3a2889f5-d3e9-7efb-1151-8acc62c11ebf.jpeg">

Peekで表示した画面の上部分にナビゲーションのようなものがある場合、Peek quick actionsが使える、ということになります。

<img width="414" alt="IMG_8101.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/40009/764f8abc-ca26-7354-53f0-c3705270ba29.jpeg">

逆にこの表示がない場合は使えない、ということですね。

## Peek quick actionsの使いどころ
**Peek quick actions**はPeekで表示されている画面で操作できるショートカット操作です。

例えば、上記の画像のメールアプリで言えば、

+ 返信
+ 転送
+ マーク
+ 自分に通知
+ メッセージを移動

の5つの操作が可能です。
この操作は全てツールバーで表示されているこの部分のボタンからできることになります。

<img width="414" alt="IMG_8100.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/40009/071ac653-b17e-112f-39ee-ba06da6e6d86.jpeg">

Peek quick actionsで追加を検討するものとしては以下のものとなります。

+ Peekで表示される画面先で操作できるもの
+ よく使う、使われると思われる操作
+ ショートカット一覧に載せてその操作が何ができるかわかるもの

逆に追加すべきではない、ふさわしくない（と思われる）アクションをメールアプリを例にすると

+ **前後のメールへ移動**：Peekで表示される画面に対しての操作ではない
+ **差出人、宛先の詳細を確認する**：使用頻度が他のアクションに比べて低い
+ **新規メール作成**：ツールバーにボタンがあるが、ショートカット操作ではない
+ **Peek quick actionsでのみ使用できる操作**：非対応端末では使えないため

以上のアクションは追加検討には外れるものとなります。

ちなみに3D Touchに関する**iOS Human Interface Guidelines**もあるので、そちらも一読することをおすすめします。
どういう場面でPeek and Popを使うべきかのガイドラインが記載されています。

[iOS Human Interface Guidelines : 3D Touch](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/3DTouch.html)

## Peek quick actionsを実装する
実装してみます。

Peek quick actionsの実装はPeekで表示される画面のクラス内で実装を行います。

```swift:swift
override func previewActionItems() -> [UIPreviewActionItem] {

        // "UIPreviewAction"で処理を登録
        let action1 : UIPreviewAction = UIPreviewAction(title: "アクション1", style: UIPreviewActionStyle.Default) { (action, previewViewController) -> Void in
            // ここに処理を書く
        }
        
        let action2 : UIPreviewAction = UIPreviewAction(title: "アクション2", style: UIPreviewActionStyle.Destructive) { (action, previewViewController) -> Void in
            // ここに処理を書く
        }
        
        // ボタンを押した後にさらに処理を分岐したい場合は"UIPreviewActionGroup"として登録
        let subAction1 : UIPreviewAction = UIPreviewAction(title: "サブアクション1", style: UIPreviewActionStyle.Default) { (action, previewViewController) -> Void in
            // ここに処理を書く
        }
        
        let subAction2 : UIPreviewAction = UIPreviewAction(title: "サブアクション2", style: UIPreviewActionStyle.Destructive) { (action, previewViewController) -> Void in
            // ここに処理を書く
        }
        
        let groupAction : UIPreviewActionGroup = UIPreviewActionGroup(title: "グループアクション", style: UIPreviewActionStyle.Default, actions: [subAction1, subAction2])
        
        return [action1, action2, groupAction]
    }
```

実装したものを表示すると、こうなります。

<img width="414" alt="IMG_8103.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/40009/48cca73b-4504-62d1-ae20-24b342b0072b.jpeg">

"グループアクション"を選択した場合はさらに選択肢を選択できるようになります。
メールアプリを例にした場合、「マーク」「自分に通知」「メッセージを移動」を選んだときのアクションですね。

<img width="414" alt="IMG_8104.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/40009/ee3bdec0-520b-4d41-7eb3-0e50af4d8cca.jpeg">

Peek quick actionsは必ずしも実装する必要はないですが、Peekした際にその場で操作ができたら便利かも、と思った場合は実装をした方がいいですね。

以上です。
Peek and Popは現時点（記事執筆時）は対応端末の数が少なく、API側でもできることが少ないですが、今後リリースされるiOSではできることが多くなったり、より良いユーザー体験を提供できることが増えるかもしれません。

少しでも参考になれば、嬉しいです。

**参考ページ**

+ [Adopting 3D Touch on iPhone : 3D Touch APIs](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/Adopting3DTouchOniPhone/3DTouchAPIs.html)
+ [iOS Human Interface Guidelines : 3D Touch](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/3DTouch.html)
+ [UIViewControllerPreviewingDelegate Protocol Reference](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewControllerPreviewingDelegate_Protocol/index.html)