如何用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 } } } } }
关键逻辑说明
状态跟踪:
currentIndex:记录当前高亮的卡片索引isRolling:标记是否处于滚动状态,用于控制按钮可用性和动画类型timer:定时切换卡片索引,实现循环滚动效果scale:控制停止后卡片的放大比例
滚动与停止逻辑:
- 点击按钮后启动定时器,每隔0.1秒切换到下一个卡片索引,通过取模运算实现循环遍历
- 随机生成滚动次数,保证每次滚动的圈数不同,停止位置更随机
- 停止滚动后触发弹簧动画,将当前卡片放大到1.1倍,增强突出效果
视图动画:
- 滚动时使用
linear线性动画,保证边框切换流畅无卡顿 - 停止时使用
spring弹簧动画,让缩放效果更自然有弹性 - 通过
border修饰符给当前索引的卡片添加蓝色边框
- 滚动时使用
内容的提问来源于stack exchange,提问作者AdamLeet
相关产品推荐
相关产品推荐

