You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI:不使用ForEach遍历闪卡ZStack实现类Tinder滑动效果

SwiftUI 实现 Tinder 式滑动卡片(非批量加载)

核心思路

不用ForEach一次性渲染所有卡片,通过状态变量跟踪当前显示的卡片索引,仅渲染当前卡片,滑动完成后自动切换到下一张,同时通过手势和动画实现流畅的滑动交互。

完整实现代码

import SwiftUI

// 自定义卡片模型(可根据实际需求调整)
struct Flashcard: Identifiable {
    let id = UUID()
    let front: String
    let back: String
}

class Model: ObservableObject {
    @Published var flashcards: [Flashcard] = [
        Flashcard(front: "卡片1正面", back: "卡片1背面"),
        Flashcard(front: "卡片2正面", back: "卡片2背面"),
        Flashcard(front: "卡片3正面", back: "卡片3背面")
    ]
}

// 单个卡片视图
struct AllCardView: View {
    let flashcard: Flashcard
    var body: some View {
        RoundedRectangle(cornerRadius: 20)
            .fill(Color.white)
            .aspectRatio(CGSize(width: 150, height: 300), contentMode: .fit)
            .shadow(radius: 5)
            .overlay(
                VStack {
                    Text(flashcard.front)
                        .font(.title)
                        .bold()
                    Spacer()
                    Text(flashcard.back)
                        .font(.subheadline)
                }
                .padding()
            )
    }
}

struct ContentView: View {
    @EnvironmentObject var model: Model
    @State private var currentIndex = 0
    @State private var dragOffset = CGSize.zero
    @State private var isCardRemoved = false
    
    var body: some View {
        VStack {
            ZStack {
                // 背景占位视图
                RoundedRectangle(cornerRadius: 20)
                    .aspectRatio(CGSize(width: 150, height: 300), contentMode: .fit)
                    .foregroundColor(.gray.opacity(0.2))
                    .shadow(radius: 5)
                    .padding(50)
                
                // 仅渲染当前卡片
                if currentIndex < model.flashcards.count {
                    AllCardView(flashcard: model.flashcards[currentIndex])
                        .padding()
                        .offset(dragOffset)
                        .rotationEffect(.degrees(Double(dragOffset.width / 10)))
                        .gesture(
                            DragGesture()
                                .onChanged { value in
                                    dragOffset = value.translation
                                }
                                .onEnded { value in
                                    // 设置滑动触发阈值
                                    let threshold: CGFloat = 100
                                    if abs(value.translation.width) > threshold {
                                        // 执行卡片滑出动画
                                        withAnimation(.easeOut(duration: 0.3)) {
                                            dragOffset = CGSize(width: value.translation.width > 0 ? 500 : -500, height: value.translation.height)
                                            isCardRemoved = true
                                        }
                                        // 动画完成后切换到下一张卡片
                                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                                            currentIndex += 1
                                            dragOffset = .zero
                                            isCardRemoved = false
                                        }
                                    } else {
                                        // 滑动距离不足,回弹到原位
                                        withAnimation(.easeOut) {
                                            dragOffset = .zero
                                        }
                                    }
                                }
                        )
                        .zIndex(isCardRemoved ? 0 : 1) // 滑出时降低层级,避免遮挡后续交互
                } else {
                    // 所有卡片滑完的提示
                    Text("所有卡片已完成")
                        .font(.title)
                        .foregroundColor(.gray)
                }
            }
        }
    }
}

// 预览视图
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environmentObject(Model())
    }
}

关键部分说明

  1. 状态变量控制:

    • currentIndex:跟踪当前显示的卡片索引,状态更新时SwiftUI会自动重新渲染视图,切换到下一张卡片。
    • dragOffset:记录拖动时的偏移量,实现卡片跟随手指移动的效果。
    • isCardRemoved:标记卡片是否正在滑出,控制z轴层级避免遮挡后续操作。
  2. 手势处理逻辑:

    • onChanged:实时更新偏移量,让卡片跟随手指移动。
    • onEnded:判断滑动距离是否超过阈值,超过则执行滑出动画,完成后自动更新索引;未超过则回弹到原位。
  3. 非批量加载:
    不再用ForEach遍历整个数组,仅根据currentIndex渲染当前卡片,避免一次性加载大量卡片占用资源。

  4. 动画增强:
    添加rotationEffect让卡片拖动时产生轻微旋转,模拟真实的卡片滑动手感;用withAnimation包裹状态变化,实现平滑的过渡动画。

内容的提问来源于stack exchange,提问作者Maria Kravchenko

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 09:05:31