165 lines
5.3 KiB
Swift
165 lines
5.3 KiB
Swift
//
|
|
// 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<Int>()
|
|
|
|
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)
|
|
}
|
|
}
|