// // EmojiMemoryGameView.swift // Memorize // // Created by ching on 2023/2/12. // import SwiftUI struct EmojiMemoryGameView: View { @ObservedObject var game: EmojiMemoryGame @Namespace private var dealingNamespace var body: some View { ZStack(alignment: .bottom) { VStack { gameBody HStack { restart Spacer() shuffle } .padding(.horizontal) } .padding() deckBody } } @State private var dealt = Set() private func deal(_ card: EmojiMemoryGame.Card) { dealt.insert(card.id) } private func isUndealt(_ card: EmojiMemoryGame.Card) -> Bool { !dealt.contains(card.id) } private func dealAnimation(for card: EmojiMemoryGame.Card) -> Animation { var delay = 0.0 if let index = game.cards.firstIndex(where: { $0.id == card.id }) { delay = Double(index) * (const.DrawingConstants.totalDealDuration / Double(game.cards.count)) } return Animation.easeInOut(duration: const.DrawingConstants.dealDuration).delay(delay) } private func zIndex(of card: EmojiMemoryGame.Card) -> Double { -Double(game.cards.firstIndex(where: { $0.id == card.id }) ?? 0) } var gameBody: some View { AspectVGrid(items: game.cards, aspectRatio: const.DrawingConstants.gridAspectRatio) { card in if isUndealt(card) || (card.isMatched && !card.isFaceUp) { Color.clear } else { CardView(card: card) .matchedGeometryEffect(id: card.id, in: dealingNamespace) .padding(const.DrawingConstants.gridPadding) .zIndex(zIndex(of: card)) .transition(AnyTransition.asymmetric(insertion: .identity, removal: .scale)) .onTapGesture { withAnimation { game.choose(card) } } } } .foregroundColor(const.DrawingConstants.cardColor) } var deckBody: some View { ZStack { ForEach(game.cards.filter(isUndealt)) { card in CardView(card: card) .matchedGeometryEffect(id: card.id, in: dealingNamespace) .transition(AnyTransition.asymmetric(insertion: .opacity, removal: .identity)) .zIndex(zIndex(of: card)) } } .frame(width: const.DrawingConstants.undealtWidth, height: const.DrawingConstants.undealtHeight) .foregroundColor(const.DrawingConstants.cardColor) .onTapGesture { for card in game.cards { withAnimation(dealAnimation(for: card)) { deal(card) } } } } var shuffle: some View { Button("Shuffle") { withAnimation { game.shuffle() } } } var restart: some View { Button("Restart") { withAnimation { dealt = [] game.restart() } } } } struct CardView: View { let card: EmojiMemoryGame.Card @State private var animatedBonusRemaining: Double = 0 var body: some View { GeometryReader { geometry in ZStack { Group { if card.isConsumingBonusTime { Pie(startAngle: Angle(degrees: 270), endAngle: Angle(degrees: (1 - animatedBonusRemaining) * 360 - 90)) .onAppear { animatedBonusRemaining = card.bonusRemaining withAnimation(.linear(duration: card.bonusTimeRemaining)) { animatedBonusRemaining = 0 } } } else { Pie(startAngle: Angle(degrees: 0 - 90), endAngle: Angle(degrees: (1 - card.bonusRemaining) * 360 - 90)) } } .padding(const.DrawingConstants.piePadding) .opacity(const.DrawingConstants.pieOpacity) Text(card.content) .rotationEffect(Angle.degrees(card.isMatched ? 360 : 0)) .animation(Animation.easeInOut) .font(Font.system(size: const.DrawingConstants.fontSize)) .scaleEffect(scale(thatFits: geometry.size)) }.cadify(isFaceUp: card.isFaceUp) } } private func scale(thatFits size: CGSize) -> CGFloat { min(size.width, size.height) / const.DrawingConstants.fontSize * const.DrawingConstants.fontScale } private func font(in size: CGSize) -> Font { Font.system(size: min(size.width, size.height) * const.DrawingConstants.fontScale) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { let game = EmojiMemoryGame() game.choose(game.cards.first!) return EmojiMemoryGameView(game: game) // .preferredColorScheme(.dark) // EmojiMemoryGameView(game: game) .preferredColorScheme(.light) } }