SoliCards-iOS-iPadOS-MacOS/SoliCards/Views/Game/CardView.swift
idev2025 de0da01f25 feat: SoliCards v1.2.0 — native SwiftUI solitaire for iOS, iPadOS, macOS
Complete native rewrite of the web-based SoliCards game as a SwiftUI
multiplatform app targeting iOS 17+, iPadOS 17+, and macOS 14+.

Three solitaire variants (Klondike, Spider, FreeCell) with full game
rules, drag & drop, smart zoom layout, 6 themes, 4 difficulty levels,
SwiftData persistence, VoiceOver accessibility, and 57 unit tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 08:26:14 -04:00

124 lines
3.9 KiB
Swift

import SwiftUI
struct CardView: View {
let card: Card?
let cardFaceStyle: CardFaceStyle
let cardBackDesign: CardBackDesign
let size: CGSize
var isHighlighted: Bool = false
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View {
Group {
if let card {
if card.isFaceUp {
cardFront(card)
} else {
cardBack
}
} else {
emptySlot
}
}
.frame(width: size.width, height: size.height)
.clipShape(RoundedRectangle(cornerRadius: size.width * 0.08))
.shadow(color: .black.opacity(0.15), radius: 2, y: 1)
.overlay {
if isHighlighted {
RoundedRectangle(cornerRadius: size.width * 0.08)
.stroke(Color.yellow, lineWidth: 3)
.animation(reduceMotion ? nil : .easeInOut(duration: 0.8).repeatForever(), value: isHighlighted)
}
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(accessibilityLabel)
.accessibilityHint(accessibilityHint)
.accessibilityAddTraits(card?.isFaceUp == true ? .isButton : [])
}
private var accessibilityLabel: Text {
guard let card else { return Text("Empty card slot") }
if card.isFaceUp {
return Text("\(card.rank.displayName) of \(card.suit.displayName)")
} else {
return Text("Face down card")
}
}
private var accessibilityHint: Text {
guard let card, card.isFaceUp else { return Text("") }
return Text("Double tap to move to best available position")
}
private func cardFront(_ card: Card) -> some View {
let imageName = card.frontImageName(style: cardFaceStyle)
return ZStack {
RoundedRectangle(cornerRadius: size.width * 0.08)
.fill(.white)
if let uiImage = loadImage(named: imageName) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
.padding(1)
.accessibilityHidden(true)
} else {
VStack(spacing: 2) {
Text(card.rank.shortName)
.font(.system(size: size.width * 0.28, weight: .bold))
Text(card.suit.symbol)
.font(.system(size: size.width * 0.22))
}
.foregroundStyle(card.color == .red ? .red : .black)
.accessibilityHidden(true)
}
}
}
private var cardBack: some View {
let imageName = cardBackDesign.imageName
return ZStack {
RoundedRectangle(cornerRadius: size.width * 0.08)
.fill(Color.blue)
if let uiImage = loadImage(named: imageName) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
.padding(2)
.accessibilityHidden(true)
} else {
RoundedRectangle(cornerRadius: size.width * 0.08)
.strokeBorder(Color.white.opacity(0.3), lineWidth: 2)
.padding(3)
}
}
}
private var emptySlot: some View {
RoundedRectangle(cornerRadius: size.width * 0.08)
.strokeBorder(Color.white.opacity(0.3), lineWidth: 2)
}
#if os(macOS)
private func loadImage(named name: String) -> NSImage? {
NSImage(named: name)
}
private func Image(uiImage: NSImage) -> SwiftUI.Image {
SwiftUI.Image(nsImage: uiImage)
}
#else
private func loadImage(named name: String) -> UIImage? {
UIImage(named: name)
}
private func Image(uiImage: UIImage) -> SwiftUI.Image {
SwiftUI.Image(uiImage: uiImage)
}
#endif
}