GameControllerを使ってDualshock4でUIViewを操作してみる

iOS13から、iPhoneでPS4やXboxのコントローラーが利用可能になりました。
コントローラーに対応したアプリで利用することができます。

どんなふうに使うのか試しに触ってみました。

### コントローラーと接続する
コントローラーの接続は以下の2つのNotificationでハンドリングすることができます。
[GCControllerDidConnect - NSNotification.Name | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/nsnotification/name/1458856-gccontrollerdidconnect)
[GCControllerDidDisconnect - NSNotification.Name | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/nsnotification/name/1458875-gccontrollerdiddisconnect)

これらを使うには `GameController` をimportする必要があります。

```
import GameController
```

NotificationCenterを登録します。

```
NotificationCenter.default.addObserver(self,
                                       selector: #selector(handleControllerDidConnect),
                                       name: .GCControllerDidConnect,
                                       object: nil)
NotificationCenter.default.addObserver(self,
                                       selector: #selector(handleControllerDidConnect),
                                       name: .GCControllerDidDisconnect,
                                       object: nil)
```
これでコントローラーとの接続が確立したときと、切断されたときにそれぞれ通知を受け取ることができます。

### コントローラーの入力をハンドリングする

コントローラの入力のハンドリングは [GCExtendedGamepad - Game Controller | Apple Developer Documentation](https://developer.apple.com/documentation/gamecontroller/gcextendedgamepad) を使います。
これは `Notification#object` から取得することができます。

```
@objc func handleControllerDidConnect(notification: Notification) {
    guard let controller = notification.object as? GCController,
        let gamepad = controller.extendedGamepad else {
        return
    }
    ・・・
}
```

`GCExtendedGamepad` はコントローラーの入力値や入力に対するハンドラを持っており、それぞれ以下のプロパティでアクセスできます。
※一部ボタンは省略しています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14751/2c428f65-891f-fc66-35f2-8aef38f1d513.png" width=600>


現在値を取得する場合は

```
// ×ボタンが押されているかどうか
let isButtonAPressed = extendedGamepad.buttonA.isPressed
// 右スティックの水平方向の傾き（-1~1）
let rightThumbstickX = extendedGamepad.rightThumbstick.xAxis.value
// 右スティックが上方向に傾けられているかどうか
let isRightThumbstickUpPressed = extendedGamepad.rightThumbstick.up.isPressed
```

入力を受け取ったタイミングでハンドリングしたい場合は

```
// ○ボタンが押された
gamepad.buttonB.pressedChangedHandler = { (input, value, isPressed) in
    ・・・
}
// OPTIONSボタンが押された
gamepad.buttonMenu.pressedChangedHandler = { (input, value, isPressed) in
    ・・・   
}
// 右スティックが傾けられた
gamepad.rightThumbstick.valueChangedHandler = { (pad, xAxis, yAxis) in
    ・・・      
}
```

というように使います。


### UIViewを操作してみる
以下のようなサンプルを作ってみました。

- ランダムな `UIColor` のリストを表示し、セルをタップするとその色のHEX値をアラートで表示
    - 左スティックでTableViewをスクロール
    - 右スティックでポインターを動かす
    - ○ボタンでセルを選択
- OPTIONSボタンで配列の中身を新しくする

https://github.com/yoshimin/DualshockSample

![dualshock.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14751/776821f4-e163-7050-fa47-d294da4e82de.gif)

コントローラーとiPhoneの接続は以下のように行います。

1. iPhoneのBluetoothをONにしておく
2. SHAREボタンを長押ししながら、ライトバーが点滅するまでPSボタンを長押しする
3. Bluetoothの設定画面上にDUALSHOCKが表示されるのでタップするとペアリングされます

この状態でアプリを起動するとすぐに `GCControllerDidConnect` が呼ばれ入力を受け取ることができます。

簡単ですが実装してみた所感を以下にまとめます。

##### スクロールやポインターの移動など継続的な動きは現在値を使う
最初右スティックを使ってポインターを動かすのに `rightThumbstick.valueChangedHandler` を使ってみたのですが、これは値が変化したときにのみ発火するものなので、傾けたままの場合、最初に傾けたあとポインタが動かず止まってしまいました。
なので、 `GCControllerDidConnect` の通知で受け取った `GCExtendedGamepad` を保持しておき `CADisplayLink` を使ってフレームごとに `leftThumbstick.xAxis` `leftThumbstick.yAxis` を見てポインタを動かすようにしました。

逆に、ボタンの入力などその時々で処理したいイベントは `pressedChangedHandler` などのハンドラーを使います。

##### ボタンの押下イベントは `isPressed` をみる
closure の引数に `isPressed` があるので容易に想像できるかも知れませんが、ボタンの入力は押したときと離したときの２回呼ばれます。
[GCControllerButtonValueChangedHandler](https://developer.apple.com/documentation/gamecontroller/gccontrollerbuttonvaluechangedhandler)

最初なにも気にせずに `pressedChangedHandler` の中に処理を書いていたら、処理が2回呼ばれていました...
ボタンが押し込まれたときに処理したい場合は `isPressed == true` であることを確認する必要があります。

### まとめ
ゲームに使うものと思っていましたが、UIKit製のアプリでも普通に使えるのだなと思いました。
コントローラーの入力が受け取れるだけなので、それを受け取ってどうするかは実装次第で、何でもできます。
とはいえゲーム以外の需要はほとんどないと思いますが、普段PS4でAmazonビデオやNetflixで見ている私としてはiPadで動画アプリで視聴しているときに再生/一時停止/早送り/巻き戻しとかをコントローラーで操作できるのもありな気がしました。