リモコンのイベント取得その1 クリックとスワイプ


tvOSアドベントカレンダー1日目担当の @toshi0383 です。2日目も担当だし3日目も担当です。頑張ってやっていきます💪

tvOSの特徴といえばまずはあのリモコンですよね。
![tv.png](https://qiita-image-store.s3.amazonaws.com/0/35008/aac9413f-6f44-4df2-5e5b-b3180c6104c5.png "tv.png")


今回は入門向けにリモコンのイベント取得方法についておさらいします。
初めに言っておくとSiriのAPIはアプリ側に解放されておらずイベントが取れません。
tvOSのUXについては以下の記事がわかりやすいので参考にしてみてください。
http://kudakurage.hatenadiary.com/entry/2017/11/16/113657

# クリック系
「クリック」系は以下の2パターンで実装できます。

## UIPressのレスポンダチェーン
UIPressのレスポンダチェーンが反応するので、これを利用することもできます。
あまり使っているのを見ないかも。

## UIPressType
UIGestureRecognizerに`allowedPressTypes`を登録して使います。
登録できるイベントの種類は以下の通りです。

```swift
@available(tvOS 9.0, *)
public enum UIPressType : Int {
    case upArrow
    case downArrow
    case leftArrow
    case rightArrow

    case select
    case menu
    case playPause
}
```

それぞれ以下の通りに対応します。

- `upArrow`  タッチサーフェスの上端をクリック
- `downArrow` タッチサーフェスの下端をクリック
- `leftArrow` タッチサーフェスの左端をクリック
- `rightArrow` タッチサーフェスの右端をクリック
- `select` タッチサーフェスの真ん中あたりをクリック
- `menu` MENUボタン
- `playPause` Play/Pauseボタン

### セルをクリックしたら再生する
例えばセルをクリックしたら再生するのは以下のように実装すればよいということになります。
`allowedPressTypes`がなぜか`[NSNumber]`なので若干書き方にコツがいります。

```swift
class CollectionViewCell: UICollectionViewCell {
    private var reuseDisposeBag = DisposeBag()
    func configure(_ model: Model) {
        reuseDisposeBag = DisposeBag()
        let playPauseGesture = UITapGestureRecognizer()
        playPauseGesture.allowedPressTypes = [UIPressType.playPause.rawValue as NSNumber]
        self.addGestureRecognizer(playPauseGesture)
        playPauseGesture.rx.event
            .subscribe(onNext: { _ in
                PlayerAction.shared.play(model)
            })
            .disposed(by: reuseDisposeBag)
    }
}
```

# スワイプ
タッチサーフェスはスワイプに反応します。

## UISwipeGestureRecognizer
単発のスワイプなら、UISwipeGestureRecognizerを使えます。
スワイプは上下左右を指定することができます。

```swift
        let gr = UISwipeGestureRecognizer(target: self, action: #selector(gesture))
        gr.direction = .up
        view.addGestureRecognizer(gr)
```

## UITouchのレスポンダチェーンやUIKit.UIGestureRecognizerSubclass
UITouchのレスポンダチェーンを使ったりUIGestureRecognizerをサブクラスすれば、どの方向にどれくらいスワイプしたかやbegan,changed,endedなどの詳細なイベントを取得することができます。

```swift
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        print(touches.first?.location(in: view))
        super.touchesBegan(touches, with: event)
    }
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesMoved(touches, with: event)
    }
```

# タッチ
基本的にタッチサーフェスはちょっと触れただけで反応します。リモコンの上下左右の端をタッチすると、方向キーの役割をして、右端なら右隣の要素へフォーカスが移動します。

## UITouchのレスポンダチェーン
単純にどこでもいいからタッチされたとかタッチアップとかを検知するなら先ほども言及したUITouchのレスポンダチェーンで十分です。

```swift
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        print(touches.first?.location(in: view))
        super.touchesBegan(touches, with: event)
    }
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)
    }
```

## 「タッチサーフェス上の端っこ」の検知

上述の通り、

> リモコンの上下左右の端をタッチすると、方向キーの役割をして、右端なら右隣の要素へフォーカスが移動します。

ということになっているんですが、「タッチサーフェス上の端っこ」の検知は、UIKitにはAPIが提供されていません。
UITouchのレスポンダチェーンに通知される座標は、最初に触り始めたところが `(0, 0)` になってしまうので、タッチサーフェス上の座標が取れないのです。
これの解決方法については別途記事にします。

# まとめ
以上、AppleTVのリモコンのイベント取得方法その1でした。
ここまではiOSと大体一緒ですね。リモコンのイベント取得その2,その3ではより実践的な内容を予定しています。
tvOSアドベントカレンダー、明日は引き続き入門ネタでParallaxエフェクトとはというテーマで書きます。

# 参考
- https://developer.apple.com/tvos/human-interface-guidelines/overview/themes/
- http://kudakurage.hatenadiary.com/entry/2017/11/16/113657