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, diff --git a/Sources/CardStack/CardView.swift b/Sources/CardStack/CardView.swift index 21f0e86..465853f 100644 --- a/Sources/CardStack/CardView.swift +++ b/Sources/CardStack/CardView.swift @@ -1,14 +1,23 @@ 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 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, @@ -21,27 +30,59 @@ 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) .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) } + 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 } .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 } + cancelDragging() } } }