From 9eec9c47ed387b8a401f54296ff5c5f4a0eb0beb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Nov 2025 17:06:59 +0000 Subject: [PATCH 1/3] feat: add haptic feedback to additional sheet presentations Extend haptic feedback system to provide tactile responses for more sheet and modal presentations throughout the app. Changes: - UserProfileBottomSheetView: Add light impact on open/close in NationalRankingView, TeamRankingView, and GroupDetailView - BadgeDetailView: Add light impact on open/close for all three badge view types (BadgeItemView, BadgeCardButton, Simple3DBadgeView) - MissionSection: Add light impact when opening/closing mission detail sheet - GroupDetailView: Add light impact when opening/closing group detail sheet All feedback follows the same pattern: light impact on open and light impact on close, providing consistent tactile response across the app. --- .../Components/Badge/BadgeComponents.swift | 21 +++++++++++++++++++ .../Group/GroupBottomSheetView.swift | 7 +++++++ .../Components/Group/GroupDetailView.swift | 6 ++++++ .../Components/Home/MissionSection.swift | 7 +++++++ .../Ranking/NationalRankingView.swift | 7 +++++++ .../Components/Ranking/TeamRankingView.swift | 7 +++++++ 6 files changed, 55 insertions(+) diff --git a/ios/escape/escape/Views/Components/Badge/BadgeComponents.swift b/ios/escape/escape/Views/Components/Badge/BadgeComponents.swift index ddd9786..00ededc 100644 --- a/ios/escape/escape/Views/Components/Badge/BadgeComponents.swift +++ b/ios/escape/escape/Views/Components/Badge/BadgeComponents.swift @@ -307,6 +307,7 @@ struct BadgeItemView: View { .simultaneousGesture( TapGesture() .onEnded { _ in + HapticFeedback.shared.lightImpact() showingDetail = true } ) @@ -322,6 +323,12 @@ struct BadgeItemView: View { .sheet(isPresented: $showingDetail) { BadgeDetailView(badge: badge) } + .onChange(of: showingDetail) { oldValue, newValue in + // Haptic feedback when sheet is dismissed + if oldValue && !newValue { + HapticFeedback.shared.lightImpact() + } + } } // Fallback view when no image is available @@ -396,6 +403,7 @@ struct BadgeCardButton: View { var body: some View { Button(action: { + HapticFeedback.shared.lightImpact() showingDetail = true }) { VStack(spacing: 8) { @@ -475,6 +483,12 @@ struct BadgeCardButton: View { .sheet(isPresented: $showingDetail) { BadgeDetailView(badge: badge) } + .onChange(of: showingDetail) { oldValue, newValue in + // Haptic feedback when sheet is dismissed + if oldValue && !newValue { + HapticFeedback.shared.lightImpact() + } + } } } @@ -486,6 +500,7 @@ struct Simple3DBadgeView: View { var body: some View { Button(action: { + HapticFeedback.shared.lightImpact() showingDetail = true }) { VStack(spacing: 4) { @@ -570,6 +585,12 @@ struct Simple3DBadgeView: View { .sheet(isPresented: $showingDetail) { BadgeDetailView(badge: badge) } + .onChange(of: showingDetail) { oldValue, newValue in + // Haptic feedback when sheet is dismissed + if oldValue && !newValue { + HapticFeedback.shared.lightImpact() + } + } } // Fallback view when no image is available diff --git a/ios/escape/escape/Views/Components/Group/GroupBottomSheetView.swift b/ios/escape/escape/Views/Components/Group/GroupBottomSheetView.swift index 68b2c87..28216bc 100644 --- a/ios/escape/escape/Views/Components/Group/GroupBottomSheetView.swift +++ b/ios/escape/escape/Views/Components/Group/GroupBottomSheetView.swift @@ -265,6 +265,7 @@ struct GroupCardView: View { Button(action: { Task { await groupViewModel.selectGroup(group) + HapticFeedback.shared.lightImpact() showingGroupDetail = true } }) { @@ -324,6 +325,12 @@ struct GroupCardView: View { .sheet(isPresented: $showingGroupDetail) { GroupDetailView(groupViewModel: groupViewModel) } + .onChange(of: showingGroupDetail) { oldValue, newValue in + // Haptic feedback when sheet is dismissed + if oldValue && !newValue { + HapticFeedback.shared.lightImpact() + } + } } } diff --git a/ios/escape/escape/Views/Components/Group/GroupDetailView.swift b/ios/escape/escape/Views/Components/Group/GroupDetailView.swift index 3a1d106..f5ea729 100644 --- a/ios/escape/escape/Views/Components/Group/GroupDetailView.swift +++ b/ios/escape/escape/Views/Components/Group/GroupDetailView.swift @@ -358,6 +358,12 @@ struct MemberRowView: View { UserProfileBottomSheetView(userId: member.user.id) .presentationDetents([.medium, .large]) } + .onChange(of: showUserProfile) { oldValue, newValue in + // Haptic feedback when sheet is dismissed + if oldValue && !newValue { + HapticFeedback.shared.lightImpact() + } + } } } diff --git a/ios/escape/escape/Views/Components/Home/MissionSection.swift b/ios/escape/escape/Views/Components/Home/MissionSection.swift index ebfa656..c27751f 100644 --- a/ios/escape/escape/Views/Components/Home/MissionSection.swift +++ b/ios/escape/escape/Views/Components/Home/MissionSection.swift @@ -41,6 +41,7 @@ struct MissionSection: View { .padding(.horizontal) } else { MissionCardView(mission: missionViewModel.todaysMission) { + HapticFeedback.shared.lightImpact() showingMissionDetail = true } .padding(.horizontal) @@ -60,6 +61,12 @@ struct MissionSection: View { isPresented: $showingMissionDetail ) } + .onChange(of: showingMissionDetail) { oldValue, newValue in + // Haptic feedback when sheet is dismissed + if oldValue && !newValue { + HapticFeedback.shared.lightImpact() + } + } } private func loadCurrentMission() { diff --git a/ios/escape/escape/Views/Components/Ranking/NationalRankingView.swift b/ios/escape/escape/Views/Components/Ranking/NationalRankingView.swift index 68a4170..6ed923a 100644 --- a/ios/escape/escape/Views/Components/Ranking/NationalRankingView.swift +++ b/ios/escape/escape/Views/Components/Ranking/NationalRankingView.swift @@ -89,6 +89,12 @@ struct NationalRankingView: View { UserProfileBottomSheetView(userId: identifiableUserId.id) .presentationDetents([.medium, .large]) } + .onChange(of: selectedUserId) { oldValue, newValue in + // Haptic feedback when sheet is dismissed + if oldValue != nil && newValue == nil { + HapticFeedback.shared.lightImpact() + } + } .task { await loadRankings() startAnimations() @@ -96,6 +102,7 @@ struct NationalRankingView: View { } private func handleUserTap(userId: UUID) { + HapticFeedback.shared.lightImpact() selectedUserId = IdentifiableUUID(id: userId) } diff --git a/ios/escape/escape/Views/Components/Ranking/TeamRankingView.swift b/ios/escape/escape/Views/Components/Ranking/TeamRankingView.swift index da498e4..3608448 100644 --- a/ios/escape/escape/Views/Components/Ranking/TeamRankingView.swift +++ b/ios/escape/escape/Views/Components/Ranking/TeamRankingView.swift @@ -90,6 +90,12 @@ struct TeamRankingView: View { UserProfileBottomSheetView(userId: identifiableUserId.id) .presentationDetents([.medium, .large]) } + .onChange(of: selectedUserId) { oldValue, newValue in + // Haptic feedback when sheet is dismissed + if oldValue != nil && newValue == nil { + HapticFeedback.shared.lightImpact() + } + } .task { await loadTeamRankings() startAnimations() @@ -97,6 +103,7 @@ struct TeamRankingView: View { } private func handleUserTap(userId: UUID) { + HapticFeedback.shared.lightImpact() selectedUserId = IdentifiableUUID(id: userId) } From 0181bfef0d68f84d790db8e407d4acabcffbeef7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Nov 2025 17:08:00 +0000 Subject: [PATCH 2/3] feat: add medium impact haptic feedback to badge flip animation Add tactile feedback when users flip the badge to see the back side in BadgeDetailView's RotatableBadgeView component. Changes: - RotatableBadgeView: Medium impact haptic when badge is flipped, providing stronger feedback for this more significant 3D interaction --- ios/escape/escape/Views/Components/Badge/BadgeDetailView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/ios/escape/escape/Views/Components/Badge/BadgeDetailView.swift b/ios/escape/escape/Views/Components/Badge/BadgeDetailView.swift index 7ec0f00..22eb1bf 100644 --- a/ios/escape/escape/Views/Components/Badge/BadgeDetailView.swift +++ b/ios/escape/escape/Views/Components/Badge/BadgeDetailView.swift @@ -162,6 +162,7 @@ struct RotatableBadgeView: View { } .rotation3DEffect(.degrees(rotationAngle), axis: (x: 0, y: 1, z: 0)) .onTapGesture { + HapticFeedback.shared.mediumImpact() withAnimation(.easeInOut(duration: 0.8)) { rotationAngle += 180 isFlipped.toggle() From 7a164a5568f876171e100b7eb7eadd3df21b1065 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Nov 2025 17:11:31 +0000 Subject: [PATCH 3/3] fix: add Equatable conformance to IdentifiableUUID for onChange Fix build errors where IdentifiableUUID needs to conform to Equatable to work with SwiftUI's onChange(of:) modifier. Changes: - NationalRankingView: Add Equatable conformance to IdentifiableUUID - TeamRankingView: Add Equatable conformance to IdentifiableUUID This fixes the compilation errors: "referencing instance method 'onChange(of:initial:_:)' on 'Optional' requires that 'IdentifiableUUID' conform to 'Equatable'" --- .../escape/Views/Components/Ranking/NationalRankingView.swift | 2 +- .../escape/Views/Components/Ranking/TeamRankingView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/escape/escape/Views/Components/Ranking/NationalRankingView.swift b/ios/escape/escape/Views/Components/Ranking/NationalRankingView.swift index 6ed923a..739c818 100644 --- a/ios/escape/escape/Views/Components/Ranking/NationalRankingView.swift +++ b/ios/escape/escape/Views/Components/Ranking/NationalRankingView.swift @@ -8,7 +8,7 @@ import SwiftUI // Wrapper to make UUID work with .sheet(item:) -private struct IdentifiableUUID: Identifiable { +private struct IdentifiableUUID: Identifiable, Equatable { let id: UUID } diff --git a/ios/escape/escape/Views/Components/Ranking/TeamRankingView.swift b/ios/escape/escape/Views/Components/Ranking/TeamRankingView.swift index 3608448..cf546b4 100644 --- a/ios/escape/escape/Views/Components/Ranking/TeamRankingView.swift +++ b/ios/escape/escape/Views/Components/Ranking/TeamRankingView.swift @@ -8,7 +8,7 @@ import SwiftUI // Wrapper to make UUID work with .sheet(item:) -private struct IdentifiableUUID: Identifiable { +private struct IdentifiableUUID: Identifiable, Equatable { let id: UUID }