import SwiftUI
struct CardView: View {
enum SwipeDirection { case left, right, down, none }
struct Model: Identifiable, Equatable {
let id = UUID()
let text: String
var swipeDirection: SwipeDirection = .none
}
var model: Model
var size: CGSize
var dragOffset: CGSize
var isTopCard: Bool
var isSecondCard: Bool
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 15, style: .continuous)
.fill(Color(cgColor: UIColor.secondarySystemBackground.cgColor))
Text(model.text)
.font(.largeTitle).bold()
.foregroundColor(.black)
if isTopCard {
let status = getStatusText()
ZStack {
LinearGradient(
gradient: Gradient(stops: [
.init(color: getShadowColor().opacity(0.6), location: 0),
.init(color: .clear, location: 1)
]),
startPoint: .top,
endPoint: .bottom
)
Text(status)
.font(.title)
.fontDesign(.monospaced).bold()
.foregroundColor(.black)
.background(Color.orange) // show container
}
.compositingGroup()
.frame(width: size.width * 0.8, height: size.height * 0.6)
.offset(y: -size.height * 0.2)
.opacity(getStatusOpacity())
}
}
.frame(width: size.width * 0.8, height: size.height * 0.6)
.cornerRadius(15)
.padding()
}
private func getShadowColor() -> Color {
if dragOffset.height > abs(dragOffset.width) {
return Color.blue.opacity(0.8)
} else if abs(dragOffset.width) > abs(dragOffset.height) {
return dragOffset.width > 0 ? Color.green.opacity(0.8) : Color.red.opacity(0.8)
}
return Color.clear
}
private func getStatusText() -> String {
let h = dragOffset.width, v = dragOffset.height
let absX = abs(h), absY = abs(v)
let threshold: CGFloat = 50
let buffer: CGFloat = 10
if absX > absY {
if absX > threshold + buffer {
return h > 0 ? "Right" : "Left"
}
} else if absY > absX {
if absY > threshold + buffer {
return "Down"
}
}
return ""
}
private func getStatusOpacity() -> Double {
let h = dragOffset.width, v = dragOffset.height
let absX = abs(h), absY = abs(v)
let threshold: CGFloat = 50
let maxDistance: CGFloat = 100
let distance = max(absX, absY)
if distance > threshold {
return Double(min((distance - threshold) / (maxDistance - threshold), 1))
}
return 0
}
}
< /code>
struct SwipeableCardsView: View {
class Model: ObservableObject {
private var originalCards: [CardView.Model]
@Published var unswipedCards: [CardView.Model]
@Published var swipedCards: [CardView.Model]
init(cards: [CardView.Model]) {
self.originalCards = cards
self.unswipedCards = cards
self.swipedCards = []
}
func removeTopCard() {
if !unswipedCards.isEmpty {
guard let card = unswipedCards.first else { return }
unswipedCards.removeFirst()
swipedCards.append(card)
print(card.swipeDirection)
}
}
func updateTopCardSwipeDirection(_ direction: CardView.SwipeDirection) {
if !unswipedCards.isEmpty {
unswipedCards[0].swipeDirection = direction
}
}
func reset() {
unswipedCards = originalCards
swipedCards = []
}
}
@ObservedObject var model: Model
@State private var dragState = CGSize.zero
@State private var cardRotation: Double = 0
@GestureState private var isDragging: Bool = false
@State private var hasTriggeredHapticForThisDrag = false
private let swipeThreshold: CGFloat = 100.0
private let rotationFactor: Double = 35.0
var action: (Model) -> Void
var body: some View {
GeometryReader { geometry in
if model.unswipedCards.isEmpty && model.swipedCards.isEmpty {
emptyCardsView
.frame(width: geometry.size.width, height: geometry.size.height)
} else if model.unswipedCards.isEmpty {
swipingCompletionView
.frame(width: geometry.size.width, height: geometry.size.height)
} else {
ZStack {
Color.white.ignoresSafeArea()
ForEach(model.unswipedCards.reversed()) { card in
let isTop = card == model.unswipedCards.first
let isSecond = card == model.unswipedCards.dropFirst().first
let generator = UIImpactFeedbackGenerator(style: .medium)
CardView(
model: card,
size: geometry.size,
dragOffset: dragState,
isTopCard: isTop,
isSecondCard: isSecond
)
.offset(x: isTop ? dragState.width : 0,
y: isTop ? dragState.height : 0)
.rotationEffect(.degrees(isTop ? Double(dragState.width) / rotationFactor : 0))
.onChange(of: isDragging) { _, dragging in
generator.prepare()
if dragging {
generator.impactOccurred()
}
}
.gesture(
DragGesture()
.updating($isDragging) { _, state, _ in
state = true
}
.onChanged { gesture in
dragState = gesture.translation
cardRotation = Double(gesture.translation.width) / rotationFactor
}
.onEnded { _ in
let absX = abs(dragState.width)
let absY = abs(dragState.height)
let horizontalThreshold: CGFloat = swipeThreshold
let verticalThreshold: CGFloat = swipeThreshold
if absX > absY && absX > horizontalThreshold {
// Horizontal swipe
let dir: CardView.SwipeDirection = dragState.width > 0 ? .right : .left
model.updateTopCardSwipeDirection(dir)
withAnimation(.easeOut(duration: 0.5)) {
self.dragState.width = dragState.width > 0 ? 1000 : -1000
self.dragState.height = 0
}
generator.prepare()
generator.impactOccurred()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.model.removeTopCard()
self.dragState = .zero
self.cardRotation = 0
}
} else if absY > absX && dragState.height > verticalThreshold {
// Vertical swipe
model.updateTopCardSwipeDirection(.down)
withAnimation(.easeOut(duration: 0.5)) {
self.dragState.height = 1000
self.dragState.width = 0
}
generator.prepare()
generator.impactOccurred()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.model.removeTopCard()
self.dragState = .zero
self.cardRotation = 0
}
} else {
// Not enough or no clear dominant direction
withAnimation(.spring()) {
self.dragState = .zero
self.cardRotation = 0
}
}
}
)
.animation(.easeInOut, value: dragState)
}
}
.padding()
}
}
}
var emptyCardsView: some View {
VStack {
Text("No Cards")
.font(.title)
.padding(.bottom, 20)
.foregroundStyle(.gray)
}
}
var swipingCompletionView: some View {
VStack {
Text("Finished Swiping")
.font(.title)
.padding(.bottom, 20)
Button(action: {
action(model)
}) {
Text("Reset")
.font(.headline)
.frame(width: 200, height: 50)
.background(Color.accentColor)
.foregroundColor(.white)
.cornerRadius(10)
}
}
}
}
struct ContentView: View {
var body: some View {
VStack {
let cards = [
CardView.Model(text: "Card 1"),
CardView.Model(text: "Card 2"),
CardView.Model(text: "Card 3"),
CardView.Model(text: "Card 4")
]
let model = SwipeableCardsView.Model(cards: cards)
SwipeableCardsView(model: model) { model in
print(model.swipedCards)
model.reset()
}
}
.padding()
}
}
< /code>


Подробнее здесь: https://stackoverflow.com/questions/796 ... in-swiftui
Мобильная версия