From 8ea1f5a586e6a76cd0445b260f226ae3368ace83 Mon Sep 17 00:00:00 2001 From: Eugene Kugut Date: Thu, 16 Jan 2025 21:36:03 +0500 Subject: [PATCH 1/4] Fix: Proper zIndex management for card dismissal animation Ensured the card being swiped is always above the stack during dismissal animation by managing zIndex. --- Sources/CardStack/CardStack.swift | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/Sources/CardStack/CardStack.swift b/Sources/CardStack/CardStack.swift index e458776..ff95ae4 100644 --- a/Sources/CardStack/CardStack.swift +++ b/Sources/CardStack/CardStack.swift @@ -28,19 +28,25 @@ where Data.Index: Hashable { self._currentIndex = State(initialValue: data.startIndex) } + @ViewBuilder private func cardViewOrEmpty(index: Data.Index) -> some View { + let relativeIndex = self.data.distance(from: self.currentIndex, to: index) + if relativeIndex >= 0 && relativeIndex < self.configuration.maxVisibleCards { + self.card(index: index, relativeIndex: relativeIndex) + } else { + EmptyView() + } + } + public var body: some View { ZStack { - ForEach(data.indices.reversed(), id: \.self) { index -> AnyView in - let relativeIndex = self.data.distance(from: self.currentIndex, to: index) - if relativeIndex >= 0 && relativeIndex < self.configuration.maxVisibleCards { - return AnyView(self.card(index: index, relativeIndex: relativeIndex)) - } else { - return AnyView(EmptyView()) - } + ForEach(data.indices.reversed(), id: \.self) { index in + cardViewOrEmpty(index: index) + .zIndex(Double(self.data.distance(from: index, to: self.data.startIndex))) } } } + private func card(index: Data.Index, relativeIndex: Int) -> some View { CardView( direction: direction, From db0cfb92b2326c09e0708d08e65b2567fd7a639d Mon Sep 17 00:00:00 2001 From: Eugene Kugut Date: Thu, 16 Jan 2025 21:46:51 +0500 Subject: [PATCH 2/4] Fix: Prevent false taps on interactive elements during card swipe Disabled interactions with clickable elements (e.g., buttons) on cards while swiping to prevent unintended actions caused by simultaneous gestures. --- Sources/CardStack/CardView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/CardStack/CardView.swift b/Sources/CardStack/CardView.swift index 21f0e86..9e75710 100644 --- a/Sources/CardStack/CardView.swift +++ b/Sources/CardStack/CardView.swift @@ -24,6 +24,7 @@ struct CardView: View { var body: some View { GeometryReader { geometry in self.content(self.swipeDirection(geometry)) + .disabled(self.translation != .zero) .offset(self.translation) .rotationEffect(self.rotation(geometry)) .simultaneousGesture(self.isOnTop ? self.dragGesture(geometry) : nil) From f8be724901dd6a5aad85a2138d1b268cf8f8f3a4 Mon Sep 17 00:00:00 2001 From: Eugene Kugut Date: Thu, 16 Jan 2025 22:00:21 +0500 Subject: [PATCH 3/4] Fix: Resolved animation synchronization error for translation Fixed an issue causing debug errors during rapid swipes due to improper synchronization of translation animation states: Invalid sample AnimatablePair, AnimatablePair>(first: SwiftUI.AnimatablePair(first: -2.0, second: 0.0), second: SwiftUI.AnimatablePair(first: 0.0, second: 0.0)) with time Time(seconds: 0.0) > last time Time(seconds: 0.016671041666995734) --- Sources/CardStack/CardView.swift | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Sources/CardStack/CardView.swift b/Sources/CardStack/CardView.swift index 9e75710..9150599 100644 --- a/Sources/CardStack/CardView.swift +++ b/Sources/CardStack/CardView.swift @@ -3,12 +3,19 @@ import SwiftUI struct CardView: View { @Environment(\.cardStackConfiguration) private var configuration: CardStackConfiguration @State private var translation: CGSize = .zero + @State private var draggingState: CardDraggingState = .idle private let direction: (Double) -> Direction? private let isOnTop: Bool private let onSwipe: (Direction) -> Void private let content: (Direction?) -> Content + private enum CardDraggingState { + case dragging + case ended + case idle + } + init( direction: @escaping (Double) -> Direction?, isOnTop: Bool, @@ -28,6 +35,7 @@ struct CardView: View { .offset(self.translation) .rotationEffect(self.rotation(geometry)) .simultaneousGesture(self.isOnTop ? self.dragGesture(geometry) : nil) + .animation(draggingState == .dragging ? .easeInOut(duration: 0.05) : self.configuration.animation, value: translation) } .transition(transition) } @@ -35,14 +43,19 @@ struct CardView: View { private func dragGesture(_ geometry: GeometryProxy) -> some Gesture { DragGesture() .onChanged { value in + self.draggingState = .dragging self.translation = value.translation } .onEnded { value in - self.translation = value.translation + draggingState = .ended if let direction = self.swipeDirection(geometry) { - withAnimation(self.configuration.animation) { self.onSwipe(direction) } + self.translation = value.translation + withAnimation(self.configuration.animation) { + self.onSwipe(direction) + } } else { - withAnimation { self.translation = .zero } + draggingState = .idle + self.translation = .zero } } } From 7aa2e0284251a25ed921a0540053b65e3ad83e2c Mon Sep 17 00:00:00 2001 From: Eugene Kugut Date: Thu, 16 Jan 2025 22:18:55 +0500 Subject: [PATCH 4/4] Fix: Resolve stuck card state when DragGesture is interrupted by another gesture Ensured proper gesture state reset to avoid cards being stuck in an intermediate position when DragGesture is interrupted by another gesture. For example, if a second finger taps the card during a swipe, the onEnded callback now triggers correctly. --- Sources/CardStack/CardView.swift | 33 +++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/Sources/CardStack/CardView.swift b/Sources/CardStack/CardView.swift index 9150599..465853f 100644 --- a/Sources/CardStack/CardView.swift +++ b/Sources/CardStack/CardView.swift @@ -1,9 +1,11 @@ import SwiftUI +import Combine struct CardView: View { @Environment(\.cardStackConfiguration) private var configuration: CardStackConfiguration @State private var translation: CGSize = .zero @State private var draggingState: CardDraggingState = .idle + @GestureState private var isDragging: Bool = false private let direction: (Double) -> Direction? private let isOnTop: Bool @@ -28,7 +30,7 @@ struct CardView: View { self.content = content } - var body: some View { + @ViewBuilder var cardView: some View { GeometryReader { geometry in self.content(self.swipeDirection(geometry)) .disabled(self.translation != .zero) @@ -40,8 +42,34 @@ struct CardView: View { .transition(transition) } + private func cancelDragging() { + draggingState = .idle + translation = .zero + } + + var body: some View { + if #available(iOS 14.0, *) { + cardView + .onChange(of: isDragging) { newValue in + if !newValue && draggingState == .dragging { + cancelDragging() + } + } + } else { // iOS 13.0, * + cardView + .onReceive(Just(isDragging)) { newValue in + if !newValue && draggingState == .dragging { + cancelDragging() + } + } + } + } + private func dragGesture(_ geometry: GeometryProxy) -> some Gesture { DragGesture() + .updating($isDragging) { value, state, transaction in + state = true + } .onChanged { value in self.draggingState = .dragging self.translation = value.translation @@ -54,8 +82,7 @@ struct CardView: View { self.onSwipe(direction) } } else { - draggingState = .idle - self.translation = .zero + cancelDragging() } } }