fix: issue #150 - add haptic feedback option
Automated fix by Tony CI v3. Refs #150 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
590
Shared/Services/HapticFeedbackManager.swift
Normal file
590
Shared/Services/HapticFeedbackManager.swift
Normal file
@@ -0,0 +1,590 @@
|
||||
//
|
||||
// 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])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user