Search code examples
swiftswiftuirotationeffectcard

How to make card sets spread?


I want to spread the card set around, but I can't make it into that shape even if I try it. I need help.

GPT says to use sin and cos with it, but I'm not familiar with sin and cos and SwiftUI.

        var body: some View {
        HStack(spacing: 10) {
            Spacer()
            ForEach(0..<10) { num in
                VStack {
                    GeometryReader { geo in
                        ZStack {
                            CardFront(width: width, height: height, num: num, degree: $frontDegrees[num])
                            CardBack(width: width, height: height, degree: $backDegrees[num])
                        }.onTapGesture {
                            flipCard(num)
                        }

                        // spread the cards based on the center
                        .offset(x: ((Double(num) + 0.6) * 90), y: 600).rotationEffect(.degrees(Double(num) * 6))
                    }
                }
                Spacer()
            }
            .navigationBarBackButtonHidden(true)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        CardFlipView()

        CardFlipView()
            .previewInterfaceOrientation(.landscapeLeft)
    }
}

card set preview

How it should look:

how should be


Solution

  • I had a go at getting an example to work. Tap the cards to toggle the fanned state:

    import SwiftUI
    
    /// Encapsulates the layout logic for the cards
    class Layout {
    
        /// The width available for showing the fanned cards
        let viewWidth: CGFloat
    
        /// The number of cards in the pack
        static let nCardsInPack = 10
    
        /// The width of a single card
        static let cardWidth = CGFloat(50)
    
        /// The height of a single card
        static let cardHeight = CGFloat(cardWidth * 1.4)
    
        /// The number of degrees for the arc of the spread.
        private static let arcDegrees = CGFloat(40)
    
        /// The number of radians for the arc of the spread.
        /// NB: 180 degrees = Pi radians
        private static let arcRadians = (arcDegrees * CGFloat.pi) / 180
    
        /// Creates the layout for showing the fanned cards, dependent on the supplied view width
        init(viewWidth: CGFloat) {
            self.viewWidth = viewWidth
        }
    
        /// - Returns the angle for the specified card when the pack is shown fanned
        func angleForCard(n: Int) -> CGFloat {
            let nGaps = max(CGFloat(Layout.nCardsInPack) - 1, 1)
            let fraction = (CGFloat(n) - (nGaps / 2)) / nGaps
            return fraction * Layout.arcRadians
        }
    
        /// - Returns the radius of the circle on which the fanned cards are
        /// spread out. Computed from the view width and arc for the spread
        private lazy var fanRadius: CGFloat = {
            let sinAngle = sin(Layout.arcRadians / 2.0)
            let availableWidth = (viewWidth - Layout.cardWidth) / 2.0
            return sinAngle == 0 ? availableWidth : availableWidth / sinAngle
        }()
    
        /// - Returns the x offset for card n, which may be negative
        func xOffsetForCard(n: Int) -> CGFloat {
            return sin(angleForCard(n: n)) * fanRadius
        }
    
        /// - Returns the y offset for card n
        func yOffsetForCard(n: Int) -> CGFloat {
            return fanRadius - (cos(angleForCard(n: n)) * fanRadius)
        }
    }
    
    /// View of an individual card
    struct CardView: View {
    
        /// The index of this card in the pack, 0-based
        private let n: Int
    
        private let layout: Layout
    
        @Binding private var showSpreadOut: Bool
    
        init(n: Int, layout: Layout, showSpreadOut: Binding<Bool>) {
            self.n = n
            self.layout = layout
            self._showSpreadOut = showSpreadOut
        }
    
        private var angle: CGFloat {
            showSpreadOut ? layout.angleForCard(n: n) : 0
        }
    
        private var xOffset: CGFloat {
            showSpreadOut ? layout.xOffsetForCard(n: n) : 0
        }
    
        private var yOffset: CGFloat {
            showSpreadOut ? layout.yOffsetForCard(n: n) : 0
        }
    
        var body: some View {
            Text("\(n)")
                .font(.largeTitle)
                .foregroundColor(.white)
                .frame(width: Layout.cardWidth, height: Layout.cardHeight)
                .background(
                    Color.blue
                        .opacity(0.2)
                        .cornerRadius(Layout.cardWidth / 10)
                )
                // Important: rotation must come before offsets!
                .rotationEffect(Angle(radians: angle))
                .offset(x: xOffset, y: yOffset)
                .animation(.easeInOut, value: showSpreadOut)
        }
    }
    
    /// View of a collection of cards
    struct PackView: View {
    
        private let layout: Layout
    
        @Binding private var showSpreadOut: Bool
    
        init(layout: Layout, showSpreadOut: Binding<Bool>) {
            self.layout = layout
            self._showSpreadOut = showSpreadOut
        }
    
        var body: some View {
            ZStack() {
                ForEach(0..<Layout.nCardsInPack, id: \.self) { n in
                    CardView(n: n, layout: layout, showSpreadOut: $showSpreadOut)
                }
            }
        }
    }
    
    struct ContentView: View {
    
        @State private var showSpreadOut = false
    
        var body: some View {
            GeometryReader { proxy in
                PackView(
                    layout: Layout(viewWidth: proxy.size.width),
                    showSpreadOut: $showSpreadOut
                )
                .onTapGesture { showSpreadOut.toggle() }
                .frame(width: proxy.size.width, height: proxy.size.height)
            }
        }
    }
    

    FannedCards