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

如何用SwiftUI实现带卡片的转轮式选中视图

实现卡片滚动高亮并随机停止的SwiftUI动画

针对你的需求,我们可以通过添加状态跟踪、定时器逻辑和视图动画来实现蓝色边框循环移动+随机停止+放大突出的效果,以下是修改后的完整代码及关键说明:

修改后的完整代码

struct Card: Identifiable, Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(name)
    }
    
    let id: String = UUID().uuidString
    var cardBackGroundColor: Color
    var image: Image
    var name: String
}

struct PriceRollView: View {
    @State var cards = [
        Card(cardBackGroundColor: ColorConstants.turquoise, image: Image("vacation"), name: "Hawaii vacation"),
        Card(cardBackGroundColor: ColorConstants.lightPink, image: Image("suit"), name: "Tailored suit"),
        Card(cardBackGroundColor: ColorConstants.lightYellow, image: Image("shoes"), name: "Sneaker"),
        Card(cardBackGroundColor: ColorConstants.darkPink, image: Image("watch"), name: "Classic Chronometer"),
        Card(cardBackGroundColor: ColorConstants.purple, image: Image("car"), name: "Sports car"),
        Card(cardBackGroundColor: ColorConstants.turquoise, image: Image("airplane"), name: "Private jet flight"),
        Card(cardBackGroundColor: ColorConstants.darkPink, image: Image("smartWatch"), name: "Smart watch"),
        Card(cardBackGroundColor: ColorConstants.lightYellow, image: Image("coliseum"), name: "Italy vacation")
    ]
    let columns = [
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible())
    ]
    
    // 添加状态变量
    @State private var currentIndex = 0
    @State private var isRolling = false
    @State private var timer: Timer?
    @State private var scale: CGFloat = 1.0
    
    var body: some View {
        VStack {
            Text("Rolling for your price...").foregroundColor(ColorConstants.darkBlue)
            
            LazyVGrid(columns: columns, spacing: 12) {
                // 改用enumerated遍历获取索引
                ForEach(Array(cards.enumerated()), id: \.element.id) { index, card in
                    VStack(alignment: .center) {
                        card.image
                            .resizable()
                            .scaledToFit()
                        Text(card.name)
                            .foregroundColor(ColorConstants.darkBlue)
                            .lineLimit(1)
                            .minimumScaleFactor(0.5)
                    }
                    .padding(12)
                    .frame(width: (ScreenConstants.width - 48) / 3, height: (ScreenConstants.width - 48) / 3)
                    .background(card.cardBackGroundColor)
                    .cornerRadius(4)
                    // 蓝色边框:当前索引的卡片显示边框
                    .border(index == currentIndex ? Color.blue : .clear, width: 3)
                    // 缩放效果:停止滚动后放大当前卡片
                    .scaleEffect(index == currentIndex && !isRolling ? scale : 1.0)
                    // 动画:滚动时用线性动画保证流畅切换,停止时用弹簧动画增强质感
                    .animation(isRolling ? .linear(duration: 0.1) : .spring(response: 0.5, dampingFraction: 0.7), value: currentIndex)
                }
            }
            .padding(.leading, 12)
            .padding(.trailing, 12)
            .padding(.top, ScreenConstants.hasNotch ? 12 : 20)
            
            // 添加开始滚动按钮
            Button(action: startRolling) {
                Text("开始抽奖")
                    .foregroundColor(.white)
                    .padding(.horizontal, 32)
                    .padding(.vertical, 12)
                    .background(ColorConstants.darkBlue)
                    .cornerRadius(8)
            }
            .disabled(isRolling) // 滚动时禁用按钮
            .padding(.top, 24)
            
            Spacer()
        }
        .statusBar(hidden: true)
    }
    
    // 滚动逻辑实现
    private func startRolling() {
        isRolling = true
        scale = 1.0 // 重置缩放比例
        currentIndex = 0 // 可选:每次从第一个开始滚动
        
        // 随机生成滚动次数(20-30次,保证滚动足够多圈后停止)
        let maxRolls = Int.random(in: 20...30)
        var rollCount = 0
        
        // 定时器每隔0.1秒切换下一个卡片
        timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] timer in
            guard let self = self else { return }
            
            self.currentIndex = (self.currentIndex + 1) % self.cards.count
            rollCount += 1
            
            // 达到滚动次数后停止
            if rollCount >= maxRolls {
                timer.invalidate()
                self.isRolling = false
                // 停止时触发弹簧动画放大卡片
                withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) {
                    self.scale = 1.1
                }
            }
        }
    }
}

关键逻辑说明

  1. 状态跟踪:

    • currentIndex:记录当前高亮的卡片索引
    • isRolling:标记是否处于滚动状态,用于控制按钮可用性和动画类型
    • timer:定时切换卡片索引,实现循环滚动效果
    • scale:控制停止后卡片的放大比例
  2. 滚动与停止逻辑:

    • 点击按钮后启动定时器,每隔0.1秒切换到下一个卡片索引,通过取模运算实现循环遍历
    • 随机生成滚动次数,保证每次滚动的圈数不同,停止位置更随机
    • 停止滚动后触发弹簧动画,将当前卡片放大到1.1倍,增强突出效果
  3. 视图动画:

    • 滚动时使用linear线性动画,保证边框切换流畅无卡顿
    • 停止时使用spring弹簧动画,让缩放效果更自然有弹性
    • 通过border修饰符给当前索引的卡片添加蓝色边框

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 04:45:32