UIFeedbackGenerator を使おう

## 概要

UIFeedbackGenerator は UI にフィードバックを追加するための、フィードバックジェネレーターの抽象クラスで、フィードバックの種類別に下記の３つの具象クラスが存在し、それぞれ iOS10 以降で使用することが可能です。

- [UIImpactFeedbackGenerator](https://developer.apple.com/documentation/uikit/uiimpactfeedbackgenerator)
- [UISelectionFeedbackGenerator](https://developer.apple.com/documentation/uikit/uiselectionfeedbackgenerator)
- [UINotificationFeedbackGenerator](https://developer.apple.com/documentation/uikit/uinotificationfeedbackgenerator)


基本的には、UIFeedbackGenerator を自身でインスタンス化することは禁止されていて、上記のサブラクスをインスタンス化してフィードバックをトリガーします。また、システムの設定やアプリケーションの状態、バッテリー残量など特定の要因によって Haptics が再生されないことがあります。下記に例を載せておきます。

- デバイスが Taptic Engine を搭載していない場合
- アプリがバックグラウンド状態の場合
-  システムの Haptics 設定が無効な場合

## Feedback の種類


### UIImpactFeedbackGenerator

最も基本的なフィードバックで、ユーザがボタンをタップしたりした時などに使用されます。強度別にスタイルが用意されており、iOS13 からはトリガーのタイミングで強度(itensity)を指定できるようになりました。詳しくは[こちら](https://developer.apple.com/documentation/uikit/uiimpactfeedbackgenerator/3183920-impactoccurred?changes=lat_11_3)をご覧ください。

```swift
    public enum FeedbackStyle : Int {

        case light

        case medium

        case heavy

        @available(iOS 13.0, *)
        case soft

        @available(iOS 13.0, *)
        case rigid
    }
```


### UISelectionFeedbackGenerator

スライダーでの値の変更など、連続したフィードバックをしたい時に使用します。


### UINotificationFeedbackGenerator

イベントの結果などによって、成功・警告・失敗など種類別にフィードバックを行いたい時に使用します。詳しくは[こちら](https://developer.apple.com/documentation/uikit/uinotificationfeedbackgenerator/feedbacktype)を参照してください。

```swift
    public enum FeedbackType : Int {

        
        case success

        case warning

        case error
    }
```


## 実際に使ってみる

3種類のフィードバックタイプをそれぞれ個別にインスタンス化して使用するのもいいですが、今回は全てのフィードバックを試してみたかったので下記のようなクラスを作成しました。

```swift
import UIKit

enum FeedbackGeneratorType {
    case impact(style: UIImpactFeedbackGenerator.FeedbackStyle)
    case notification
    case selection
    @available(iOS 13.0, *)
    case impactWithIntensity(intensity: CGFloat)
}

final class FeedbackGenerator {
    private var feedbackGenerator: UIFeedbackGenerator?
    private let type: FeedbackGeneratorType

    init(type: FeedbackGeneratorType) {
        self.type = type
    }

    // Please call this method a few seconds before triggering the feedback.
    func prepare() {
        switch type {
        case .impact(let style):
            feedbackGenerator = UIImpactFeedbackGenerator(style: style)
        case .notification:
            feedbackGenerator = UINotificationFeedbackGenerator()
        case .selection:
            feedbackGenerator = UISelectionFeedbackGenerator()
        case .impactWithIntensity:
            feedbackGenerator = UIImpactFeedbackGenerator()
        }
        feedbackGenerator?.prepare()
    }

    func releaseFeedbackEngine() {
        feedbackGenerator = nil
    }

    // MARK: - Excute Haptics methods.
    func excuteImpactFeedback(intensity: CGFloat? = nil) {
        let optionalIntensity = intensity
        guard let impactFeedbackGenerator = feedbackGenerator as? UIImpactFeedbackGenerator else { return }
        if case .impactWithIntensity(let intensity) = type, #available(iOS 13.0, *) {
            if let specificIntensity = optionalIntensity {
                impactFeedbackGenerator.impactOccurred(intensity: specificIntensity)
            } else {
                impactFeedbackGenerator.impactOccurred(intensity: intensity)
            }
        } else {
            impactFeedbackGenerator.impactOccurred()
        }
        releaseFeedbackEngine()
    }

    func excuteNotificationFeedback(notificationType: UINotificationFeedbackGenerator.FeedbackType) {
        guard let notificationFeedbackGenerator = feedbackGenerator as? UINotificationFeedbackGenerator else { return }
        notificationFeedbackGenerator.notificationOccurred(notificationType)
        releaseFeedbackEngine()
    }

    func excuteSelectionFeedback() {
        guard let selectionFeedbackGenerator = feedbackGenerator as? UISelectionFeedbackGenerator else { return }
        selectionFeedbackGenerator.selectionChanged()
        feedbackGenerator?.prepare()
    }
}
```

注目すべき点は２つあり、prepare() の呼び出しと、feedbackGenerator インスタンスの解放タイミングです。prepare() は Taptic Engine と呼ばれるフィードバックを再生するための振動モーターを準備中にするために呼び出します。これにより、フィードバックを再生する際にレイテンシをなくすことができます。２つ目の feedbackGenerator のインスタンス解放タイミングは、UIFeedbackGenerator がインスタンス化されると Taptic Engine が待機状態になり電力を消費するため、なるべくフィードバック毎にインスタンス を解放することが重要になってきます。また、UISelectionFeedbackGenerator に関しては連続してフィードバックを再生するのでインスタンス は解放せず、再生直後に prepare() を呼び出し、Taptic Engine を準備中にしていますので、任意のタイミングで feedbackGenerator を解放する必要があります。


### 使い方

UIButton の TouchDown イベントで prepare() して buttonTouchUpInside イベントでフィードバックを再生するサンプルです。

```swift
class ViewController: UIViewController {
    let feedbackGenerator1 = FeedbackGenerator(type: .impactWithIntensity(intensity: 10000))
    
    @IBAction func buttonTouchDown(_ sender: Any) {
        feedbackGenerator1.prepare()
    }
    
    @IBAction func buttonTouchUpInside(_ sender: Any) {
        feedbackGenerator1.excuteImpactFeedback()
    }
    
}
```