Skip to content

Instantly share code, notes, and snippets.

@Jiropole
Created June 21, 2022 18:40
Show Gist options
  • Select an option

  • Save Jiropole/51a62b7f8dbfa115128203203a4268b2 to your computer and use it in GitHub Desktop.

Select an option

Save Jiropole/51a62b7f8dbfa115128203203a4268b2 to your computer and use it in GitHub Desktop.
A convenient wrapper exposing the full range of Taptic Engine functions. Helps with the guesswork around timing and energy conservation.
import Foundation
import UIKit
import Combine
/// Wraps the Taptic Engine patterns.
/// For most accurate timing, call `prepare()` within a short interval before `trigger()`.
/// If more than 5 seconds pass after either call, the engine is released to conserve energy.
class HapticImpact {
/// Specifies feedback type, used with the `prepare()` function.
enum ImpactType {
// uses UISelectionFeedbackGenerator
case selectionChanged
// uses UINotificationFeedbackGenerator
case success
case warning
case error
// uses UIImpactFeedbackGenerator
case heavy
case light
case medium
case rigid
case soft
case custom(intensity: CGFloat)
}
private var generator: UIFeedbackGenerator?
private var impactType: ImpactType = .light
private var expiryCancellable: AnyCancellable?
/// Should be called slightly prior to triggering, to avoid any delay starting the Taptic engine. Once prepared,
/// the haptic engine remains engaged for 5 seconds. If no `trigger()`, occurs within that time, the engine
/// is released to conserve energy.
func prepare(impactType: ImpactType) {
switch impactType {
case .selectionChanged:
generator = UISelectionFeedbackGenerator()
case .success, .warning, .error:
generator = UINotificationFeedbackGenerator()
default:
generator = UIImpactFeedbackGenerator(style: impactType.feedbackType)
}
generator?.prepare()
self.impactType = impactType
restartTimeout()
}
/// Trigger the feedback. Ideally `trigger()` is called at some short delay after `prepare()`. Once triggered,
/// the haptic engine remains engaged for 5 seconds. If no additional `trigger()`, occurs within that time, the engine
/// is released to conserve energy.
func trigger() {
if generator == nil {
prepare(impactType: impactType)
}
switch impactType {
case .selectionChanged:
(generator as? UISelectionFeedbackGenerator)?.selectionChanged()
case .success, .warning, .error:
(generator as? UINotificationFeedbackGenerator)?.notificationOccurred(impactType.notificationType)
case .custom(let intensity):
(generator as? UIImpactFeedbackGenerator)?.impactOccurred(intensity: intensity)
default:
(generator as? UIImpactFeedbackGenerator)?.impactOccurred()
}
restartTimeout()
}
}
private extension HapticImpact {
private func restartTimeout() {
expiryCancellable = Timer.publish(every: 5.0, on: RunLoop.main, in: .tracking)
.autoconnect()
.sink { [weak self] _ in
self?.generator = nil
self?.expiryCancellable?.cancel()
}
}
}
private extension HapticImpact.ImpactType {
var notificationType: UINotificationFeedbackGenerator.FeedbackType {
switch self {
case .success:
return UINotificationFeedbackGenerator.FeedbackType.success
case .warning:
return UINotificationFeedbackGenerator.FeedbackType.warning
default:
return UINotificationFeedbackGenerator.FeedbackType.error
}
}
var feedbackType: UIImpactFeedbackGenerator.FeedbackStyle {
switch self {
case .heavy:
return .heavy
case .light:
return .light
case .medium:
return .medium
case .rigid:
return .rigid
default:
return .soft
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment