Merge pull request #151 from akatreyt/fix/issue-150
fix: issue #150 - add haptic feedback option
This commit is contained in:
@@ -422,6 +422,7 @@ extension AnalyticsManager {
|
||||
// MARK: Settings
|
||||
case settingsClosed
|
||||
case deleteToggleChanged(enabled: Bool)
|
||||
case hapticFeedbackToggled(enabled: Bool)
|
||||
case exportTapped
|
||||
case dataExported(format: String, count: Int)
|
||||
case importTapped
|
||||
@@ -555,6 +556,8 @@ extension AnalyticsManager {
|
||||
return ("settings_closed", nil)
|
||||
case .deleteToggleChanged(let enabled):
|
||||
return ("delete_toggle_changed", ["enabled": enabled])
|
||||
case .hapticFeedbackToggled(let enabled):
|
||||
return ("haptic_feedback_toggled", ["enabled": enabled])
|
||||
case .exportTapped:
|
||||
return ("export_tapped", nil)
|
||||
case .dataExported(let format, let count):
|
||||
|
||||
@@ -206,6 +206,7 @@ class UserDefaultsStore {
|
||||
case paywallStyle
|
||||
case lockScreenStyle
|
||||
case celebrationAnimation
|
||||
case hapticFeedbackEnabled
|
||||
|
||||
case contentViewCurrentSelectedHeaderViewBackDays
|
||||
case contentViewHeaderTag
|
||||
|
||||
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])
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ struct AddMoodHeaderView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||
@AppStorage(UserDefaultsStore.Keys.votingLayoutStyle.rawValue, store: GroupUserDefaults.groupDefaults) private var votingLayoutStyle: Int = 0
|
||||
@AppStorage(UserDefaultsStore.Keys.celebrationAnimation.rawValue, store: GroupUserDefaults.groupDefaults) private var celebrationAnimationIndex: Int = 0
|
||||
@AppStorage(UserDefaultsStore.Keys.hapticFeedbackEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var hapticFeedbackEnabled = true
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
@@ -90,14 +91,16 @@ struct AddMoodHeaderView: View {
|
||||
}
|
||||
|
||||
private func addItem(withMood mood: Mood) {
|
||||
let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
|
||||
impactFeedback.impactOccurred()
|
||||
|
||||
// Store mood, date, and use saved animation preference
|
||||
celebrationMood = mood
|
||||
celebrationDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboardingData)
|
||||
celebrationAnimation = CelebrationAnimationType.fromIndex(celebrationAnimationIndex)
|
||||
|
||||
// Play haptic feedback matching the selected animation
|
||||
if hapticFeedbackEnabled {
|
||||
HapticFeedbackManager.shared.play(for: celebrationAnimation)
|
||||
}
|
||||
|
||||
// Show celebration - mood will be saved when animation completes
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
showCelebration = true
|
||||
|
||||
@@ -38,6 +38,7 @@ struct SettingsContentView: View {
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.deleteEnable.rawValue, store: GroupUserDefaults.groupDefaults) private var deleteEnabled = true
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.hapticFeedbackEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var hapticFeedbackEnabled = true
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
@@ -53,6 +54,7 @@ struct SettingsContentView: View {
|
||||
// Settings section
|
||||
settingsSectionHeader
|
||||
reminderTimeButton
|
||||
hapticFeedbackToggle
|
||||
canDelete
|
||||
showOnboardingButton
|
||||
|
||||
@@ -1011,6 +1013,38 @@ struct SettingsContentView: View {
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
private var hapticFeedbackToggle: some View {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "waveform")
|
||||
.font(.title2)
|
||||
.foregroundColor(.purple)
|
||||
.frame(width: 32)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(String(localized: "Haptic Feedback"))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(String(localized: "Vibrate when logging mood"))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Toggle("", isOn: $hapticFeedbackEnabled)
|
||||
.labelsHidden()
|
||||
.onChange(of: hapticFeedbackEnabled) { _, newValue in
|
||||
AnalyticsManager.shared.track(.hapticFeedbackToggled(enabled: newValue))
|
||||
}
|
||||
.accessibilityLabel(String(localized: "Haptic Feedback"))
|
||||
.accessibilityHint(String(localized: "Toggle vibration feedback when voting"))
|
||||
}
|
||||
.padding()
|
||||
.background(theme.currentTheme.secondaryBGColor)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
private var canDelete: some View {
|
||||
VStack {
|
||||
Toggle(String(localized: "settings_use_delete_enable"),
|
||||
@@ -1205,6 +1239,7 @@ struct SettingsView: View {
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults) private var firstLaunchDate = Date()
|
||||
@AppStorage(UserDefaultsStore.Keys.privacyLockEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var privacyLockEnabled = false
|
||||
@AppStorage(UserDefaultsStore.Keys.hapticFeedbackEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var hapticFeedbackEnabled = true
|
||||
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
|
||||
@@ -1226,6 +1261,7 @@ struct SettingsView: View {
|
||||
|
||||
// Settings section
|
||||
settingsSectionHeader
|
||||
hapticFeedbackToggle
|
||||
canDelete
|
||||
showOnboardingButton
|
||||
|
||||
@@ -1823,6 +1859,38 @@ struct SettingsView: View {
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
private var hapticFeedbackToggle: some View {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "waveform")
|
||||
.font(.title2)
|
||||
.foregroundColor(.purple)
|
||||
.frame(width: 32)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(String(localized: "Haptic Feedback"))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(String(localized: "Vibrate when logging mood"))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Toggle("", isOn: $hapticFeedbackEnabled)
|
||||
.labelsHidden()
|
||||
.onChange(of: hapticFeedbackEnabled) { _, newValue in
|
||||
AnalyticsManager.shared.track(.hapticFeedbackToggled(enabled: newValue))
|
||||
}
|
||||
.accessibilityLabel(String(localized: "Haptic Feedback"))
|
||||
.accessibilityHint(String(localized: "Toggle vibration feedback when voting"))
|
||||
}
|
||||
.padding()
|
||||
.background(theme.currentTheme.secondaryBGColor)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
private var canDelete: some View {
|
||||
VStack {
|
||||
Toggle(String(localized: "settings_use_delete_enable"),
|
||||
|
||||
Reference in New Issue
Block a user