【iOS】入力文字数制限に近づいた際にHaptic feedback(触覚フィードバック)でユーザに注意喚起する

## はじめに

アプリ制作の現場，少なくとも業務においては，(ロジックはもちろん) 
UI 側に傾倒しがちで，3D Touch とか Haptic feedback とかの機能は
後回しかそれ以上に結構軽視されているように感じる。
最近の新しい iOS はカバーする端末が広いこともあり対応端末かそうでないかの場合分けが必要になるのも辛い。

Apple 純正アプリや海外のアプリだとさりげなく，
かつ適切に触覚フィードバックが使われていることが多い気がします。
例えば Twitter であれば 140 文字に近づくと120文字で一度，
140文字目で再度，150文字でまた一度といった感じです。
YouTube アプリではネットワークの状態変化で通知共にフィードバックがあります。

今回業務で入力フォームを多用する機能追加を行った際に，
ふと触覚フィードバックのことを思い出したので復習も兼ねて書きます。

## Haptic feedback について

触覚フィードバックという訳がついています。
多分ほとんどのユーザは知らないと思われ，ただ経験的に感覚は知っているくらいのものと思います。
注意を喚起時などに端末の Haptic Engine を使って物理的にフィードバックできます。

Human Interface Guidelines の Feedback の項[^1] に記載があります。
種類は大きく 3 種類あり，**Notification・Impact・Selection** です。

Notification は通知で成功(Success)・警告(Warning)・失敗(Failure) を
ユーザに通知する際に使います。
通信の成功可否などでアラートダイアログと共に使えると思います。

Impact は視覚体験を補完する物理的なメタファー(暗喩)とあります。
**Light・Medium・Heavy** の 3 種類あります。，
例えば iPhone の画面左側を押し込むとアプリスイッチャが開かれますが，
そのときの振動が該当しそうです。
開こうとすると弱い振動が，完全にスイッチャ画面になると強い振動がという感じです。
UISliderBar の限界値到達時に感じる触覚フィードバックもこれに該当すると思います。

Selection は選択が頻繁に変化している際にフィードバックします。
例としては，誕生日など日付を選択する際や都道府県などを選択する際に使うピッカーを操作しているときなどがあたります。

触覚的に体験はできませんが，どういう振動なのか雰囲気はわかる動画？が Haptic feedback の項に用意されているのでご覧ください。

注意点は下記の通りです。

* 慎重に Haptics を使う
* 一般にユーザが開始したアクションに応答して触覚フィードバックを提供
* フィードバックタイプを再定義しない
* 視覚体験とうまく絡めて使う
* 触覚フィードバックだけ使う機能はダメ(サポートしない端末もある)
* 視覚フィードバックが塞がれる可能性がある場合は触覚を使う
* 遅延を伴う場合があるので開始前に使用準備をしておく
* サウンドと共に使う場合は別に準備が必要

触覚フィードバックはあくまでも視覚のサポート的な存在で，多用したりメイン機能としてはならない。通知音などと一緒に使う場合は別に実装が必要で，起動まで遅延があるから先に準備するコード書いておく，という感じでしょうか。

## 今回実装するもの

10 文字以内で送信可能な入力フォームを用意し，
8 文字になった際に **Warning** の触覚フィードバックを，
10 文字，12 文字になった際に **Failure** の触覚フィードバックを返すようにする。

また，1 文字以上 10 文字以内で押下可能になる疑似送信ボタンを用意し，
押した際に成功・失敗の触覚フィードバックを返すようにする。
今回は通信せず，成功・失敗の状態は SegmentedControl で選択できるようにする。
一応視覚的にも成功 or 失敗の表示は行う。

Impact と Selection は今回は使いません。

画面的にはシンプルでこんな感じです。

![hapticfeedback_01.png](https://qiita-image-store.s3.amazonaws.com/0/88266/ac7c617a-b61f-e730-8846-29bb5be2cdd3.png "hapticfeedback_01.png")

## 環境

* macOS 10.13.6
* iOS 10 以上
* iPhone 7 以上
* Xcode 10

今回は `UIImpactFeedbackGenerator`[^2] を用いて実装します。
UIkit を import します。 
要件は iOS 10 以上で iOS 10未満 の端末，iPad や iPhone 6s 以下は
対応しないので(クラッシュはないがフィードバックがない)，
代わりに `AudioToolbox`[^3] を import して同等の実装ができます[^4]。
端末によって場合分けとなりますが，主機能ではありませんので思い切って対応しないのもいいかもしれないと思います。

今回実装したものは GitHub にあげましたので気になる方がいらっしゃいましたらご覧ください。
間違い・コード改善指摘をいただけると嬉しいです。

[https://github.com/MilanistaDev/HapticFeedbackTest](https://github.com/MilanistaDev/HapticFeedbackTest)

## 実装

`UIImpactFeedbackGenerator` の使い方ですが，とても簡単です。
遅延があるので準備をしてから使うと先ほどありましたが，prepare() 関数をコールしておくという意味だと思われます。Preparing the Generator の項に記述があります。発電機の例わかりやすいです。

> Preparing the generator can reduce latency when triggering feedback. This is particularly important when trying to match feedback to sound or visual cues.
Calling the generator’s prepare() method puts the Taptic Engine in a prepared state. To preserve power, the Taptic Engine stays in this state for only a short period of time (on the order of seconds), or until you next trigger feedback.
Think about when and where you can best prepare your generators. If you call prepare() and then immediately trigger feedback, the system won’t have enough time to get the Taptic Engine into the prepared state, and you may not see a reduction in latency. On the other hand, if you call prepare() too early, the Taptic Engine may become idle again before you trigger feedback.

今回は下記のように実装しましたが，適切なライフサイクルで管理すればいいと思います。

```swift:ViewController.swift
// プロパティを用意
var feedbackGenerator : UINotificationFeedbackGenerator? = nil

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    
    // インスタンスを生成し prepare() をコール
    self.feedbackGenerator = UINotificationFeedbackGenerator()
    self.feedbackGenerator?.prepare()
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    // 一応 nil にしておく
    self.feedbackGenerator = nil
}
```

あとは必要な箇所で下記のコードを書くだけです。

```swift:ViewController.swift
// Success
self.feedbackGenerator?.notificationOccurred(.success)

// Warning
self.feedbackGenerator?.notificationOccurred(.warning)

// Failure
self.feedbackGenerator?.notificationOccurred(.error)
```

例としては下記です。
8 文字になった際に Warning の触覚フィードバックを，
10 文字，12 文字になった際に Failure の触覚フィードバックを返す

```swift
// 8 characters: Warning, 10 Characters: Failure, 12 Characters: Failure again
if self.textView.text.count == 8 {
    self.feedbackGenerator?.notificationOccurred(.warning)
} else if self.textView.text.count == 10 || self.textView.text.count == 12 {
    self.feedbackGenerator?.notificationOccurred(.error)
}
```

SegmentedControl の値を見て，送信ボタン押した際に成功・失敗の触覚フィードバックを返す。

```swift:ViewController.swift
@IBAction func sendAction(_ sender: Any) {
    switch self.statusSegmentedControl.selectedSegmentIndex {
    case 0:
        self.feedbackGenerator?.notificationOccurred(.success)
    case 1:
        self.feedbackGenerator?.notificationOccurred(.error)
    }
}
```

## 動作確認

書く前からわかってたことだけど
やっぱり肝心の触覚フィードバックが GIF アニメで伝わらない・・・
実際に実装してみたり，GitHub のプロジェクトを実機で試してみたりしてみてください🙇‍♀️

![hapticfeedback.gif](https://qiita-image-store.s3.amazonaws.com/0/88266/2c4acd40-cc89-7691-e20c-093c573c5121.gif "hapticfeedback.gif")

たくさん入力フォームがある画面での UIAlertController の多用はユーザ的に鬱陶しく感じることあるのでサラッと静かにエラーのフィードバックさせてもいいかもしれない。

## おわりに

今回は，入力制限のあるフォームを例に触覚フィードバックを返す例について書きました。
実装自体はとても簡単だし，フィードバックさせるトリガーも入力フォームや通信可否以外にも
色々考えられますのでアプリを見直すときに導入するか考えてみようかなと思ってます。
あくまでも視覚の補助的な立ち位置なので触覚フィードバックは容量用法を守って適切に使わないといけないです。

乱文となりましたが，ご覧いただきありがとうございました。


[^1]: [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/ios/user-interaction/feedback/)

[^2]: [UIFeedbackGenerator Reference](https://developer.apple.com/documentation/uikit/uifeedbackgenerator)

[^3]: [Audiotoolbox](https://developer.apple.com/documentation/audiotoolbox)

[^4]: [iOS Haptic Feedback for iPhone 6s, 7 (or newer).](https://medium.com/nerdmade/ios-haptic-feedback-for-iphone-7-and-6s-1bc6e7f1c285)