Merge pull request #151 from akatreyt/fix/issue-150

fix: issue #150 - add haptic feedback option
This commit is contained in:
akatreyt
2026-03-10 16:16:06 -05:00
committed by GitHub
5 changed files with 669 additions and 4 deletions

View File

@@ -422,6 +422,7 @@ extension AnalyticsManager {
// MARK: Settings // MARK: Settings
case settingsClosed case settingsClosed
case deleteToggleChanged(enabled: Bool) case deleteToggleChanged(enabled: Bool)
case hapticFeedbackToggled(enabled: Bool)
case exportTapped case exportTapped
case dataExported(format: String, count: Int) case dataExported(format: String, count: Int)
case importTapped case importTapped
@@ -555,6 +556,8 @@ extension AnalyticsManager {
return ("settings_closed", nil) return ("settings_closed", nil)
case .deleteToggleChanged(let enabled): case .deleteToggleChanged(let enabled):
return ("delete_toggle_changed", ["enabled": enabled]) return ("delete_toggle_changed", ["enabled": enabled])
case .hapticFeedbackToggled(let enabled):
return ("haptic_feedback_toggled", ["enabled": enabled])
case .exportTapped: case .exportTapped:
return ("export_tapped", nil) return ("export_tapped", nil)
case .dataExported(let format, let count): case .dataExported(let format, let count):

View File

@@ -206,6 +206,7 @@ class UserDefaultsStore {
case paywallStyle case paywallStyle
case lockScreenStyle case lockScreenStyle
case celebrationAnimation case celebrationAnimation
case hapticFeedbackEnabled
case contentViewCurrentSelectedHeaderViewBackDays case contentViewCurrentSelectedHeaderViewBackDays
case contentViewHeaderTag case contentViewHeaderTag

View 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])
}
}

View File

@@ -15,6 +15,7 @@ struct AddMoodHeaderView: View {
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default @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.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.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 } private var textColor: Color { theme.currentTheme.labelColor }
@@ -90,14 +91,16 @@ struct AddMoodHeaderView: View {
} }
private func addItem(withMood mood: Mood) { private func addItem(withMood mood: Mood) {
let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
impactFeedback.impactOccurred()
// Store mood, date, and use saved animation preference // Store mood, date, and use saved animation preference
celebrationMood = mood celebrationMood = mood
celebrationDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboardingData) celebrationDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboardingData)
celebrationAnimation = CelebrationAnimationType.fromIndex(celebrationAnimationIndex) 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 // Show celebration - mood will be saved when animation completes
withAnimation(.easeInOut(duration: 0.3)) { withAnimation(.easeInOut(duration: 0.3)) {
showCelebration = true showCelebration = true

View File

@@ -38,6 +38,7 @@ struct SettingsContentView: View {
@AppStorage(UserDefaultsStore.Keys.deleteEnable.rawValue, store: GroupUserDefaults.groupDefaults) private var deleteEnabled = true @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.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 } private var textColor: Color { theme.currentTheme.labelColor }
@@ -53,6 +54,7 @@ struct SettingsContentView: View {
// Settings section // Settings section
settingsSectionHeader settingsSectionHeader
reminderTimeButton reminderTimeButton
hapticFeedbackToggle
canDelete canDelete
showOnboardingButton showOnboardingButton
@@ -1011,6 +1013,38 @@ struct SettingsContentView: View {
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) .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 { private var canDelete: some View {
VStack { VStack {
Toggle(String(localized: "settings_use_delete_enable"), 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.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.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults) private var firstLaunchDate = Date()
@AppStorage(UserDefaultsStore.Keys.privacyLockEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var privacyLockEnabled = false @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 } private var textColor: Color { theme.currentTheme.labelColor }
@@ -1226,6 +1261,7 @@ struct SettingsView: View {
// Settings section // Settings section
settingsSectionHeader settingsSectionHeader
hapticFeedbackToggle
canDelete canDelete
showOnboardingButton showOnboardingButton
@@ -1823,6 +1859,38 @@ struct SettingsView: View {
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) .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 { private var canDelete: some View {
VStack { VStack {
Toggle(String(localized: "settings_use_delete_enable"), Toggle(String(localized: "settings_use_delete_enable"),
@@ -1837,7 +1905,7 @@ struct SettingsView: View {
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
} }
private var exportData: some View { private var exportData: some View {
Button(action: { Button(action: {
showingExporter.toggle() showingExporter.toggle()