skip to Main Content

I want to build a View with ZStack of cards which get info from an array. Cards have to swipe to the left and to the right by .gesture modifier, without any other buttons. What I want is when the current card is displayed, the next card isn’t seen. And when the current card goes away, the next card automatically is shows up.

Long story short, I want to make swipe cards UI like in Tinder or Quizlet, without using Foreach and laying out all the cards at once (there may be a lot of them)

I tried to loop through ForEach(array.indices) without any modifiers, and all cards displayed simultaneously in a stack, but I want them to update as user swipes the current card. I don’t know of other ways to write code for updating views (beginner here).

struct ContentView: View {
@EnvironmentObject var model: Model
var body: some View {
    
    VStack {
        ZStack {
            
            RoundedRectangle(cornerRadius: 20)
                .aspectRatio(CGSize(width: 150, height: 300), contentMode: .fit)
                .foregroundColor(.white)
                .shadow(radius: 5)
                .padding(50)
            
            ForEach(model.flashcard.reversed()) { card in
                AllCardView(flashcard: Flashcard(front: card.front, back: card.back))
            }
            .padding()
        }
    }
}

}

2

Answers


  1. Approach:

    • Use ZStack to stack the cards
    • Add DragGesture on the card
    • Use the .onChanged and .onEnded on the gesture to move the card
    • Rotate the card
    • Add offset to the card
    • Use withAnimation { } to animate the value changes

    Given below is a crude implementation, you could use it as a guidance and refine it:

    Card:

    import Foundation
    import SwiftUI
    
    struct Card: Identifiable {
        let id: Int
        let title: String
        let color: Color
    }
    

    Model:

    class Model: ObservableObject {
        @Published var cards = [
            Card(id: 1, title: "1", color: .red),
            Card(id: 2, title: "2", color: .green),
            Card(id: 3, title: "3", color: .blue),
            Card(id: 4, title: "4", color: .yellow),
            Card(id: 5, title: "5", color: .purple),
            Card(id: 6, title: "6", color: .orange),
        ]
        
        func removeFirstCard() {
            guard !cards.isEmpty else {
                return
            }
            cards.remove(at: 0)
        }
    }
    

    CardView:

    struct CardView: View {
        let title: String
        let color: Color
        
        @EnvironmentObject private var model: Model
        
        private let xthreshold = CGFloat(150)
        private let yMovement = CGFloat(10)
        
        @State private var xOffset = CGFloat(0)
        @State private var yOffset = CGFloat(0)
        
        var body: some View {
            ZStack {
                RoundedRectangle(cornerRadius: 18)
                    .foregroundColor(color)
                Text(title)
                    .foregroundColor(.white)
            }
            .frame(width: 300, height: 400)
            .gesture(dragGesture)
            .offset(x: xOffset, y: yOffset)
            .rotationEffect(rotationAngle, anchor: rotationAnchor)
        }
        
        var rotationAngle: Angle {
            let degrees: Double
            switch xOffset {
            case 0:
                degrees = 0
            case ...0:
                degrees = -5
            case 0...:
                degrees = 5
            default:
                degrees = 0
            }
            return Angle(degrees: degrees)
        }
        
        var rotationAnchor: UnitPoint {
            xOffset >= 0 ? .topLeading : .topTrailing
        }
        
        var dragGesture: some Gesture {
            DragGesture(minimumDistance: 20)
                .onEnded { value in
                    let xOffset: CGFloat
                    let yOffset: CGFloat
                    switch self.xOffset {
                    case ...(-xthreshold):
                        print("negative")
                        xOffset = -200
                        yOffset = yMovement
                    case -xthreshold...0:
                        print("negative - not enough")
                        xOffset = 0
                        yOffset = 0
                    case xthreshold...:
                        print("positive")
                        xOffset = 200
                        yOffset = yMovement
                    case 0...:
                        print("positive - not enough")
                        xOffset = 0
                        yOffset = 0
                    default:
                        print("default")
                        xOffset = 0
                        yOffset = 0
                    }
                    
                    withAnimation {
                        self.xOffset = xOffset
                        self.yOffset = yOffset
                        if xOffset != 0 {
                            model.removeFirstCard()
                        }
                    }
                }
                .onChanged { value in
                    switch value.translation.width {
                    case ...0:
                        print("negative")
                        withAnimation {
                            yOffset = yMovement
                        }
                    case 0...:
                        print("positive")
                        withAnimation {
                            yOffset = yMovement
                        }
                    default:
                        print("default")
                        withAnimation {
                            yOffset = 0
                        }
                    }
    
                    xOffset += value.translation.width
                }
        }
    }
    

    ContentView:

    struct ContentView: View {
        @StateObject var model = Model()
        var body: some View {
            ZStack {
                ForEach(model.cards.reversed()) { card in
                    CardView(title: card.title, color: card.color)
                        .environmentObject(model)
                }
            }
        }
    }
    
    Login or Signup to reply.
  2. You need some more Views to transform the data you want to display, e.g.

    struct CardView: View {
        let cards: [Card]
    
        var cardsReversed: [Card] {
            cards.reversed()
        }
    
        var body: some View {
            CardView2(cardsReversed: cardsReversed)
        }
    }
    
    struct CardView2: View {
        let cardsReversed: [Card]
    
        var body: some View {
           ...
        }
    }
    

    Name the Views however you like.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search