概要
UIFeedbackGenerator は UI にフィードバックを追加するための、フィードバックジェネレーターの抽象クラスで、フィードバックの種類別に下記の3つの具象クラスが存在し、それぞれ iOS10 以降で使用することが可能です。
基本的には、UIFeedbackGenerator を自身でインスタンス化することは禁止されていて、上記のサブラクスをインスタンス化してフィードバックをトリガーします。また、システムの設定やアプリケーションの状態、バッテリー残量など特定の要因によって Haptics が再生されないことがあります。下記に例を載せておきます。
- デバイスが Taptic Engine を搭載していない場合
- アプリがバックグラウンド状態の場合
- システムの Haptics 設定が無効な場合
Feedback の種類
UIImpactFeedbackGenerator
最も基本的なフィードバックで、ユーザがボタンをタップしたりした時などに使用されます。強度別にスタイルが用意されており、iOS13 からはトリガーのタイミングで強度(itensity)を指定できるようになりました。詳しくはこちらをご覧ください。
public enum FeedbackStyle : Int {
case light
case medium
case heavy
@available(iOS 13.0, *)
case soft
@available(iOS 13.0, *)
case rigid
}
UISelectionFeedbackGenerator
スライダーでの値の変更など、連続したフィードバックをしたい時に使用します。
UINotificationFeedbackGenerator
イベントの結果などによって、成功・警告・失敗など種類別にフィードバックを行いたい時に使用します。詳しくはこちらを参照してください。
public enum FeedbackType : Int {
case success
case warning
case error
}
実際に使ってみる
3種類のフィードバックタイプをそれぞれ個別にインスタンス化して使用するのもいいですが、今回は全てのフィードバックを試してみたかったので下記のようなクラスを作成しました。
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()
}
}
注目すべき点は2つあり、prepare() の呼び出しと、feedbackGenerator インスタンスの解放タイミングです。prepare() は Taptic Engine と呼ばれるフィードバックを再生するための振動モーターを準備中にするために呼び出します。これにより、フィードバックを再生する際にレイテンシをなくすことができます。2つ目の feedbackGenerator のインスタンス解放タイミングは、UIFeedbackGenerator がインスタンス化されると Taptic Engine が待機状態になり電力を消費するため、なるべくフィードバック毎にインスタンス を解放することが重要になってきます。また、UISelectionFeedbackGenerator に関しては連続してフィードバックを再生するのでインスタンス は解放せず、再生直後に prepare() を呼び出し、Taptic Engine を準備中にしていますので、任意のタイミングで feedbackGenerator を解放する必要があります。
使い方
UIButton の TouchDown イベントで prepare() して buttonTouchUpInside イベントでフィードバックを再生するサンプルです。
class ViewController: UIViewController {
let feedbackGenerator1 = FeedbackGenerator(type: .impactWithIntensity(intensity: 10000))
@IBAction func buttonTouchDown(_ sender: Any) {
feedbackGenerator1.prepare()
}
@IBAction func buttonTouchUpInside(_ sender: Any) {
feedbackGenerator1.excuteImpactFeedback()
}
}