SwiftUI记忆游戏开发:如何实现卡牌交换的位移动画效果
SwiftUI记忆卡牌游戏卡牌交换动画实现问题
问题描述
我正在开发一款记忆类卡牌游戏,游戏流程如下:首个环节中所有卡牌会翻面展示,两张匹配的卡牌会高亮显示,随后所有卡牌翻回背面,随机选中的卡牌会交换位置。目前我可以通过array.swap、array.shuffle函数实现卡牌数值的交换,也可以正常实现卡牌翻转动画,但无法实现卡牌本身交换位置的动画效果。我还有多个配套类代码,无法全部上传,若需要更多细节可告知。

现有代码
struct findTheCardsFourCards: View { @Namespace var cardAnimation: Namespace.ID @State var flipped = false @State var animate3d = false @State var rememberTheCard = false @State var timeRemaining = 15 @State var selectYourCardsText = false @State var shuffleCards = false let showCardsFirst = 15 let startGame = 11 let selectYourCards = 5 let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @EnvironmentObject var game: CardModel var rowCount = 2 var colCount = 2 var body: some View { ZStack{ VStack{ ForEach(0 ..< game.cardInGame.count/rowCount){row in HStack{ ForEach(0 ..< colCount){col in CardView(card: game.cardInGame[row * rowCount + col], cardAnimation: cardAnimation, flipped: flipped, animate3d: animate3d, action: {game.cardInGame.swapAt(2, 3)}) .shadow(color: Color.purple, radius: highlightCards(card: game.cardInGame[row*rowCount+col])) .animation(.linear) .onReceive(timer, perform: { _ in switch timeRemaining{ case 14: flipped.toggle() case 8: game.cardInGame.swapAt(1,2) default: break } /* if timeRemaining > startGame { game.cardInGame[row * rowCount + col].cardSelected = true } else if timeRemaining == startGame - 1{ game.cardInGame[row * rowCount + col].cardSelected = false } if timeRemaining == startGame { shuffleCards = true game.cardInGame.shuffle() }*/ }) } } } } HStack{ Text("REMEMBER THE CARDS") } .opacity(rememberTheCard ? 1.0:0.0) HStack{ Text("SELECT YOUR CARDS") } .opacity(selectYourCardsText ? 1.0 : 0.0) //.layoutPriority(rememberTheCard ? 1.0:0.0) } .onReceive(timer, perform: { _ in switch timeRemaining{ case 13: rememberTheCard = true game.flipGame.toggle() case 10: rememberTheCard = false game.flipGame.toggle() case 8: game.cardInGame.swapAt(1, 3) case 6: selectYourCardsText = true case 3: selectYourCardsText = false default: break } /*if timeRemaining == showCardsFirst { rememberTheCard = true game.flipGame.toggle() } if timeRemaining == startGame { rememberTheCard = false game.flipGame.toggle() } if timeRemaining == startGame - 2 { selectYourCardsText = true } if timeRemaining == selectYourCards{ selectYourCardsText = false }*/ timeRemaining -= 1 }) } func highlightCards(card: CardItem) -> CGFloat { //if game.cardInGame[row*rowCount+col].winningCard == true{ if card.winningCard == true { if game.flipGame == true{ return 10}} if card.cardSelected == true{ return 10} return 0 } } struct findTheCardsFourCards_Previews: PreviewProvider { static var previews: some View { findTheCardsFourCards() .environmentObject(testCardModel) .previewDevice("iPad Pro (9.7-inch)") } } struct ContentView: View { @EnvironmentObject var game: CardModel @State private var timeRemaining = 5 let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() var body: some View { VStack{ findTheCardsFourCards() HStack{ Button(action: {game.flipGame.toggle() } ) { Text("Start Game") } } .padding() .padding() .background(Color.green) .cornerRadius(15) .padding() //.onReceive(timer){ time in //if self.timeRemaining == 0 { // self.timeRemaining -= 1 //} //else{ // game.flipGame.toggle() // } //} } } }
解决方案
问题核心是当前ForEach遍历的是索引而非可识别的卡牌元素,同时没有正确触发matchedGeometryEffect的位置过渡动画,可按以下步骤修改:
- 首先确保你的
CardItem结构体实现Identifiable协议,每个卡牌拥有唯一的id属性 - 将原来按索引遍历的
ForEach替换为按卡牌id遍历,让SwiftUI可以识别单个卡牌的位置变化 - 给每个
CardView添加.matchedGeometryEffect(id: card.id, in: cardAnimation)修饰符,绑定你已经定义的命名空间 - 所有数组交换、打乱操作都要包裹在
withAnimation闭包中,触发系统动画
修改后核心代码段
// 遍历部分替换为 ForEach(game.cardInGame) { card in CardView(card: card, cardAnimation: cardAnimation, flipped: flipped, animate3d: animate3d, action: { withAnimation(.linear(duration: 0.5)) { game.cardInGame.swapAt(2, 3) } }) .matchedGeometryEffect(id: card.id, in: cardAnimation) .shadow(color: Color.purple, radius: highlightCards(card: card)) .onReceive(timer, perform: { _ in switch timeRemaining{ case 14: withAnimation { flipped.toggle() } case 8: withAnimation(.linear(duration: 0.8)) { game.cardInGame.swapAt(1,2) } default: break } }) } // 外部的swap操作也需要包裹withAnimation case 8: withAnimation(.linear(duration: 0.8)) { game.cardInGame.swapAt(1, 3) }
如果需要保留网格布局,遍历之后再按行列分组即可,核心是保持每个卡牌的唯一id和matchedGeometryEffect绑定,所有会改变卡牌顺序的操作都放在withAnimation中触发动画。
内容的提问来源于stack exchange,提问作者Alemastr
相关产品推荐
相关产品推荐

