最近有个需求,需要使用SwiftUI实现一个卡片滑动效果,左右滑动切换下一张卡片。可以缓慢拖动,拖的时候卡片会跟着手指动
还会像扇子旋转角度。

首先构思一个页面的基本结构

主界面

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Text("单向滑动卡片")
                .font(.title)
                .padding()
            
            CardStackView()
                .frame(height: 500)
        }
    }
}

// 预览代码
#Preview {
    ContentView()
}

创建卡片样式

struct CardView: View {
    let text: String
    let color: Color
    
    var body: some View {
        RoundedRectangle(cornerRadius: 20)
            .fill(color)
            .frame(width: 300, height: 400)
            .overlay(
                Text(text)
                    .font(.largeTitle)
                    .foregroundColor(.white)
            )
            .shadow(radius: 10)
    }
}

实现卡堆的主要逻辑

struct CardStackView: View {
    // 卡片数据
    let cards = [
        (text: "卡片1", color: Color.blue),
        (text: "卡片2", color: Color.green),
        (text: "卡片3", color: Color.orange),
        (text: "卡片4", color: Color.purple),
        (text: "卡片5", color: Color.red)
    ]
    
    // 当前显示的卡片索引
    @State private var currentIndex = 0
    
    // 滑动相关状态
    @State private var offset: CGFloat = 0
    @State private var rotation: Double = 0
    @State private var isSwiping = false
    
    var body: some View {
        ZStack {
            // 只显示当前卡片和下一张卡片
            if currentIndex + 1 < cards.count {
                CardView(text: cards[currentIndex + 1].text, 
                         color: cards[currentIndex + 1].color)
                    .zIndex(1)
            }
            
            if currentIndex < cards.count {
                CardView(text: cards[currentIndex].text, 
                         color: cards[currentIndex].color)
                    .zIndex(2)
                    .offset(x: offset, y: isSwiping ? -abs(offset/10) : 0)
                    .rotationEffect(.degrees(rotation), anchor: .bottom)
                    .animation(.interactiveSpring(), value: offset)
                    .gesture(
                        DragGesture()
                            .onChanged { value in
                                isSwiping = true
                                offset = value.translation.width
                                // 限制旋转角度在-30到30度之间
                                rotation = min(max(Double(value.translation.width / 10), -30), 30)
                            }
                            .onEnded { value in
                                // 滑动距离超过阈值则切换卡片
                                if abs(value.translation.width) > 100 {
                                    withAnimation(.easeOut(duration: 0.3)) {
                                        // 根据滑动方向决定飞出方向
                                        let direction = value.translation.width > 0 ? 1 : -1
                                        offset = direction * UIScreen.main.bounds.width
                                        rotation = direction * 45
                                    }
                                    
                                    // 延迟切换到下一张卡片
                                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                                        goToNextCard()
                                    }
                                } else {
                                    // 复位
                                    withAnimation(.spring()) {
                                        resetCardPosition()
                                    }
                                }
                            }
                    )
            } else {
                // 所有卡片已显示完毕
                Text("没有更多卡片了")
                    .font(.title)
            }
        }
    }
    
    private func goToNextCard() {
        currentIndex += 1
        resetCardPosition()
    }
    
    private func resetCardPosition() {
        offset = 0
        rotation = 0
        isSwiping = false
    }
}

以上就是完整实现的过程和思路

其实效果实现起来也不是很复杂,实现关键主要是以下几点:

旋转锚点设置

.rotationEffect(.degrees(rotation), anchor: .bottom)

将旋转锚点设置在底部,实现更自然的扇形效果

动画效果

  1. 拖动时使用.interactiveSpring()实现跟随手感
  2. 飞出时使用.easeOut(duration: 0.3)实现流畅退出
  3. 复位时使用.spring()实现弹性效果

SwiftiOSSwiftUI

1 条评论

  1. tt
    2025-07-18 21:42
    回复

    厉害哦

添加新评论