Files
Reflect/Shared/Services/HapticFeedbackManager.swift
treyt 1303cb8cbc fix: issue #150 - add haptic feedback option
Automated fix by Tony CI v3.
Refs #150

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-10 16:13:08 -05:00

591 lines
20 KiB
Swift

//
// HapticFeedbackManager.swift
// Reflect
//
// Haptic feedback patterns that match celebration animations.
//
import UIKit
import CoreHaptics
@MainActor
final class HapticFeedbackManager {
static let shared = HapticFeedbackManager()
private var engine: CHHapticEngine?
private init() {
prepareEngine()
}
private func prepareEngine() {
guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }
do {
engine = try CHHapticEngine()
engine?.resetHandler = { [weak self] in
try? self?.engine?.start()
}
try engine?.start()
} catch {
engine = nil
}
}
/// Play a haptic pattern that matches the given celebration animation type.
func play(for animationType: CelebrationAnimationType) {
guard CHHapticEngine.capabilitiesForHardware().supportsHaptics,
let engine else {
// Fall back to basic haptic on devices without CoreHaptics
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred()
return
}
do {
let pattern = try hapticPattern(for: animationType)
let player = try engine.makePlayer(with: pattern)
try engine.start()
try player.start(atTime: CHHapticTimeImmediate)
} catch {
// Fallback to simple impact
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred()
}
}
// MARK: - Pattern Definitions
private func hapticPattern(for animationType: CelebrationAnimationType) throws -> CHHapticPattern {
switch animationType {
case .confettiCannon:
return try confettiPattern()
case .vortexCheckmark:
return try vortexPattern()
case .explosionReveal:
return try explosionPattern()
case .flipReveal:
return try flipPattern()
case .shatterReform:
return try shatterPattern()
case .pulseWave:
return try pulseWavePattern()
case .fireworks:
return try fireworksPattern()
case .morphBlob:
return try morphPattern()
case .zoomTunnel:
return try tunnelPattern()
case .gravityDrop:
return try gravityPattern()
}
}
// Confetti: burst upward, then scattered taps falling down
private func confettiPattern() throws -> CHHapticPattern {
var events: [CHHapticEvent] = []
// Initial burst
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8)
],
relativeTime: 0.0
))
// Glow swell
events.append(CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
],
relativeTime: 0.1,
duration: 0.3
))
// Scattered confetti taps falling
let confettiTimes: [Double] = [0.5, 0.6, 0.65, 0.75, 0.85, 0.95, 1.1, 1.2]
for (i, time) in confettiTimes.enumerated() {
let intensity = Float(0.6 - Double(i) * 0.05)
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: max(intensity, 0.15)),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4)
],
relativeTime: time
))
}
// Final checkmark thud
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
],
relativeTime: 1.4
))
return try CHHapticPattern(events: events, parameters: [])
}
// Vortex: spinning wind-up then a satisfying snap
private func vortexPattern() throws -> CHHapticPattern {
var events: [CHHapticEvent] = []
// Spiral wind-up (continuous, rising intensity)
events.append(CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.2),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
],
relativeTime: 0.0,
duration: 0.5
))
// Ring snap
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.9),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7)
],
relativeTime: 0.5
))
// Checkmark bounce
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.6)
],
relativeTime: 0.7
))
// Settle
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4)
],
relativeTime: 0.9
))
let curve = CHHapticParameterCurve(
parameterID: .hapticIntensityControl,
controlPoints: [
CHHapticParameterCurve.ControlPoint(relativeTime: 0.0, value: 0.2),
CHHapticParameterCurve.ControlPoint(relativeTime: 0.4, value: 0.7),
CHHapticParameterCurve.ControlPoint(relativeTime: 0.5, value: 1.0)
],
relativeTime: 0.0
)
return try CHHapticPattern(events: events, parameterCurves: [curve])
}
// Explosion: big bang then particles scatter
private func explosionPattern() throws -> CHHapticPattern {
var events: [CHHapticEvent] = []
// Big explosion impact
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
],
relativeTime: 0.0
))
// Debris rumble
events.append(CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8)
],
relativeTime: 0.05,
duration: 0.4
))
// Glow pulse
events.append(CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2)
],
relativeTime: 0.3,
duration: 0.3
))
// Checkmark landing
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
],
relativeTime: 0.6
))
return try CHHapticPattern(events: events, parameters: [])
}
// Flip: a quick flip-over thud
private func flipPattern() throws -> CHHapticPattern {
var events: [CHHapticEvent] = []
// Card lift
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
],
relativeTime: 0.0
))
// Mid-flip whoosh
events.append(CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4)
],
relativeTime: 0.2,
duration: 0.2
))
// Card lands
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.9),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.6)
],
relativeTime: 0.4
))
// Shimmer glide
events.append(CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.2),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7)
],
relativeTime: 0.5,
duration: 0.4
))
return try CHHapticPattern(events: events, parameters: [])
}
// Shatter: crack apart then reform
private func shatterPattern() throws -> CHHapticPattern {
var events: [CHHapticEvent] = []
// Shatter crack
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
],
relativeTime: 0.0
))
// Flying shards
let shardTimes: [Double] = [0.05, 0.1, 0.15, 0.2, 0.3]
for (i, time) in shardTimes.enumerated() {
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(0.7 - Double(i) * 0.1)),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8)
],
relativeTime: time
))
}
// Reform convergence
events.append(CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
],
relativeTime: 0.6,
duration: 0.4
))
// Checkmark snap
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
],
relativeTime: 1.1
))
return try CHHapticPattern(events: events, parameters: [])
}
// Pulse Wave: expanding rings
private func pulseWavePattern() throws -> CHHapticPattern {
var events: [CHHapticEvent] = []
// Three expanding wave pulses
let waveTimes: [Double] = [0.0, 0.1, 0.2]
for (i, time) in waveTimes.enumerated() {
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(0.8 - Double(i) * 0.15)),
CHHapticEventParameter(parameterID: .hapticSharpness, value: Float(0.6 - Double(i) * 0.1))
],
relativeTime: time
))
}
// Stamp down (checkmark)
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7)
],
relativeTime: 0.3
))
// Stamp settle
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4)
],
relativeTime: 0.45
))
return try CHHapticPattern(events: events, parameters: [])
}
// Fireworks: multiple bursts at staggered intervals
private func fireworksPattern() throws -> CHHapticPattern {
var events: [CHHapticEvent] = []
// 4 firework bursts, each staggered
let burstTimes: [Double] = [0.0, 0.15, 0.3, 0.45]
for time in burstTimes {
// Launch whoosh
events.append(CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
],
relativeTime: time,
duration: 0.1
))
// Burst pop
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.9),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8)
],
relativeTime: time + 0.1
))
}
// Final checkmark
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
],
relativeTime: 0.7
))
return try CHHapticPattern(events: events, parameters: [])
}
// Morph: flowing organic movement
private func morphPattern() throws -> CHHapticPattern {
var events: [CHHapticEvent] = []
// Expand outward
events.append(CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2)
],
relativeTime: 0.0,
duration: 0.4
))
// Morph shift
events.append(CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
],
relativeTime: 0.4,
duration: 0.5
))
// Converge
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4)
],
relativeTime: 0.9
))
// Checkmark reveal
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
],
relativeTime: 1.2
))
return try CHHapticPattern(events: events, parameters: [])
}
// Tunnel: rushing zoom then arrival
private func tunnelPattern() throws -> CHHapticPattern {
var events: [CHHapticEvent] = []
// Rings rushing inward (6 quick taps, increasing intensity)
for i in 0..<6 {
let time = Double(i) * 0.08
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(0.2 + Double(i) * 0.12)),
CHHapticEventParameter(parameterID: .hapticSharpness, value: Float(0.3 + Double(i) * 0.1))
],
relativeTime: time
))
}
// Warp speed continuous
events.append(CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.6)
],
relativeTime: 0.3,
duration: 0.2
))
// Arrival impact
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7)
],
relativeTime: 0.5
))
// Settle bounce
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4)
],
relativeTime: 0.7
))
return try CHHapticPattern(events: events, parameters: [])
}
// Gravity: fall, bounce, bounce, settle
private func gravityPattern() throws -> CHHapticPattern {
var events: [CHHapticEvent] = []
// Falling whoosh
events.append(CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
],
relativeTime: 0.0,
duration: 0.35
))
// First impact (heaviest)
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8)
],
relativeTime: 0.4
))
// First bounce up & down
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.6)
],
relativeTime: 0.6
))
// Second bounce (smaller)
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.35),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
],
relativeTime: 0.75
))
// Final settle
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.2),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
],
relativeTime: 0.85
))
let curve = CHHapticParameterCurve(
parameterID: .hapticIntensityControl,
controlPoints: [
CHHapticParameterCurve.ControlPoint(relativeTime: 0.0, value: 0.1),
CHHapticParameterCurve.ControlPoint(relativeTime: 0.35, value: 0.8)
],
relativeTime: 0.0
)
return try CHHapticPattern(events: events, parameterCurves: [curve])
}
}