iOS13から追加されるContext Menusについて

WWDC19で発表された**Context Menus**という新しいUIについて紹介します。
なお、下記の情報は19.6.24時点のbeta2時点のものであり、iOS13正式リリースまでに変更の可能性があります。

## Context Menusとは？
![](https://user-images.githubusercontent.com/5630896/59643647-71b71a00-91a4-11e9-9e5d-d8c894ca5b53.png)

iOS13~登場する3D Touch、ロングプレスなどのジェスチャーで表示できるメニューのことです。
今まで3D Touchで表示できたPeek and Popのメニューと似ていますが、大きく2つの違いがあります。

### Peek and Popとの2つの違い
1. iOS13~対応の端末であれば3D Touchに対応していない端末でも利用できる。Peek and Popは3D Touchのサポートが必要
2. コンテキストに関連するメニューがすぐ表示される(上の画像参照)Peek and Popはスワイプアップが必要

### メニューをネストできる
例えば上の画像のように写真をロングプレスしたメニューの場合、下記のように、最初の表示はシェアと編集と削除ボタンだけを出しておいて、編集ボタンを押したときにコピー・複製のサブメニューを表示する。などといったことができます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30423/e97a05c1-bc12-79bc-ea39-1e855ef7d102.png)

### ジェスチャに対する一貫した動作が担保される
下記のジェスチャで一貫してメニューが表示されます

* 3D Touch
* Haptic Touch
* Long press
* Secondary click 

## 実装について
GitHubにデモアプリのソースをUPしたので、Xcode11(beta2~)でビルドして挙動を確認してみてください。
https://github.com/hirothings/Context-Menus-Demo

実装方法については大きく、UIViewにインタラクションを追加した場合と、TableView・CollectionViewのDelegateを使う2パターンがあります。

## TableView・CollectionViewのDelegateを使うパターン
iOS13~利用できる新しいDelegateメソッドを1つ呼ぶだけで実装できます。

CollectionViewの場合、
`collectionView(_:contextMenuConfigurationForItemAt:point:)`

``` swift
extension CollectionViewController: UICollectionViewDataSource, UICollectionViewDelegate {

// <中略>

func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt
    indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
    
    // ①プレビューの定義
    let previewProvider: () -> PreviewViewController? = { [unowned self] in
        return PreviewViewController(image: self.images[indexPath.row])
    }
    
    // ②メニューの定義
    let actionProvider: ([UIMenuElement]) -> UIMenu? = { _ in
        let share = UIAction(__title: "Share", image: UIImage(systemName: "square.and.arrow.up")) { _ in
            // some action
        }
        let editMenu: UIMenu = {
            let copy = UIAction(__title: "Copy", image: nil) { _ in
                // some action
            }
            let delete = UIAction(__title: "Delete", image: UIImage(systemName: "trash"), options: [.destructive]) { _ in
                // some action
            }
            return UIMenu(__title: "Edit..", image: nil, identifier: nil, children: [copy, delete])
        }()
        
        return UIMenu(__title: "Edit..", image: nil, identifier: nil, children: [share, editMenu])
    }
    
    return UIContextMenuConfiguration(identifier: nil,
                                      previewProvider: previewProvider,
                                      actionProvider: actionProvider)
}

// <中略>
}
```

### 補足

**①プレビューの定義**
`UIContextMenuContentPreviewProvider` (実態はクロージャのtypealias)を指定。
プレビュー表示用に準備したViewControllerを返している
ちなみにプレビュー用のViewControllerにボタンを置いて反応するか試したが、ボタンのアクションは制御されていた

**②メニューの定義**
`UIContextMenuActionProvider` (実態はクロージャのtypealias)を指定。
UIMenuをネストして返している

## UIViewにインタラクションを追加するパターン

1.UIViewに `addInteraction`

``` swift
imageView.isUserInteractionEnabled = true
let interaction = UIContextMenuInteraction(delegate: self)
imageView.addInteraction(interaction)
```

2.`UIContextMenuInteractionDelegate` に準拠し、Delegateメソッド内でContextMenuの設定をする

``` swift
extension SampleViewController: UIContextMenuInteractionDelegate {
  func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
    // 先述のCollectionViewのDelegate内の実装と一緒
  }
}
```

## Human Interface Guidelines抜粋
Human Interface Guidelinesを読むと下記のようなことが書いてありました。

* 一貫してコンテキストメニューを採用すること
* もっとも一般的に使用されるコマンドだけメニューに含めること
* サブメニューを活用してメニューの複雑さを管理すること
* サブメニューの階層は１レベルにすること

詳しくは、[Context MenusのHIG](https://developer.apple.com/design/human-interface-guidelines/ios/controls/context-menus/)を読んでください


## 参考URL
official video [Modernizing Your UI for iOS 13]
https://developer.apple.com/videos/play/wwdc2019/224/

Human Interface Guidelines
https://developer.apple.com/design/human-interface-guidelines/ios/controls/context-menus/

実装してみたデモアプリ
https://github.com/hirothings/Context-Menus-Demo