fix: swipe vs tap — use highPriorityGesture + hidden NavigationLink
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s

- DragGesture with minimumDistance:15 as highPriorityGesture
- Tap only navigates when not swiping
- Tapping while swiped closes the delete button
- Hidden NavigationLink for programmatic navigation
- Reverted FitnessTabView back to page-style TabView

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-03 11:25:55 -05:00
parent c448b945d5
commit 2c5311264b
2 changed files with 50 additions and 26 deletions

View File

@@ -31,16 +31,18 @@ struct FitnessTabView: View {
.padding(.horizontal) .padding(.horizontal)
.padding(.top, 8) .padding(.top, 8)
// Content no page swipe (conflicts with swipe-to-delete) // Content
Group { TabView(selection: $selectedSubTab) {
switch selectedSubTab { TodayView()
case 0: TodayView() .tag(0)
case 1: TemplatesView() TemplatesView()
case 2: GoalsView() .tag(1)
case 3: FoodLibraryView() GoalsView()
default: TodayView() .tag(2)
} FoodLibraryView()
.tag(3)
} }
.tabViewStyle(.page(indexDisplayMode: .never))
} }
.background(Color.canvas) .background(Color.canvas)
.navigationBarHidden(true) .navigationBarHidden(true)

View File

@@ -68,16 +68,16 @@ struct MealSectionView: View {
if isExpanded { if isExpanded {
ForEach(entries) { entry in ForEach(entries) { entry in
SwipeToDeleteRow(onDelete: { onDelete(entry) }) { SwipeToDeleteRow(
NavigationLink(destination: EntryDetailView(entry: entry, onDelete: { onDelete: { onDelete(entry) },
onDelete(entry) onTap: { /* handled by NavigationLink */ },
})) { destination: { EntryDetailView(entry: entry, onDelete: { onDelete(entry) }) }
) {
entryRow(entry) entryRow(entry)
} }
} }
} }
} }
}
.background(Color.surfaceCard) .background(Color.surfaceCard)
.clipShape(RoundedRectangle(cornerRadius: 14)) .clipShape(RoundedRectangle(cornerRadius: 14))
.shadow(color: .black.opacity(0.04), radius: 6, y: 2) .shadow(color: .black.opacity(0.04), radius: 6, y: 2)
@@ -118,40 +118,62 @@ struct MealSectionView: View {
// MARK: - Swipe to Delete Row // MARK: - Swipe to Delete Row
struct SwipeToDeleteRow<Content: View>: View { struct SwipeToDeleteRow<Content: View, Destination: View>: View {
let onDelete: () -> Void let onDelete: () -> Void
let onTap: () -> Void
let destination: () -> Destination
@ViewBuilder let content: () -> Content @ViewBuilder let content: () -> Content
@State private var offset: CGFloat = 0 @State private var offset: CGFloat = 0
@State private var showDelete = false @State private var isSwiping = false
@State private var showDetail = false
var body: some View { var body: some View {
ZStack(alignment: .trailing) { ZStack(alignment: .trailing) {
// Delete background // Delete button background
HStack { HStack {
Spacer() Spacer()
Button { Button {
withAnimation(.easeInOut(duration: 0.2)) { withAnimation(.easeInOut(duration: 0.2)) {
onDelete() onDelete()
offset = 0 offset = 0
isSwiping = false
} }
} label: { } label: {
Image(systemName: "trash.fill") Image(systemName: "trash.fill")
.foregroundStyle(.white) .foregroundStyle(.white)
.frame(width: 70, height: .infinity) .frame(maxHeight: .infinity)
}
.frame(width: 70) .frame(width: 70)
}
.background(Color.red) .background(Color.red)
} }
// Content // Content row
content() content()
.offset(x: offset) .offset(x: offset)
.gesture( .background(
DragGesture() NavigationLink(destination: destination(), isActive: $showDetail) {
EmptyView()
}
.opacity(0)
)
.onTapGesture {
if !isSwiping {
showDetail = true
} else {
// Close swipe
withAnimation(.easeOut(duration: 0.2)) {
offset = 0
isSwiping = false
}
}
}
.highPriorityGesture(
DragGesture(minimumDistance: 15)
.onChanged { value in .onChanged { value in
if value.translation.width < 0 { if value.translation.width < 0 {
offset = max(value.translation.width, -80) offset = max(value.translation.width, -80)
} else if showDelete { } else if isSwiping {
offset = min(value.translation.width - 70, 0) offset = min(value.translation.width - 70, 0)
} }
} }
@@ -159,10 +181,10 @@ struct SwipeToDeleteRow<Content: View>: View {
withAnimation(.easeOut(duration: 0.2)) { withAnimation(.easeOut(duration: 0.2)) {
if value.translation.width < -40 { if value.translation.width < -40 {
offset = -70 offset = -70
showDelete = true isSwiping = true
} else { } else {
offset = 0 offset = 0
showDelete = false isSwiping = false
} }
} }
} }