XCode-Claude-Workflow/SoliCards/Views/Game/SpiderBoardView.swift
idev2025 0f989f5c86 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.

Key features:
- MVVM + Protocol-Oriented Strategy architecture
- DragGesture with coordinate-space hit-testing (long press + drag)
- Smart zoom: cards auto-size to fit screen based on deepest column
- Landscape: 30% bigger cards with scrollable overflow (iOS)
- macOS: 120pt card cap, 92% height buffer for window resizing
- Auto-save, game resume, statistics tracking via SwiftData
- Privacy manifest, app icon, String Catalog, zero dependencies

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

77 lines
2.9 KiB
Swift

import SwiftUI
struct SpiderBoardView: View {
@Bindable var viewModel: GameViewModel
let layout: CardLayout
let theme: GameTheme
let cardFaceStyle: CardFaceStyle
let cardBackDesign: CardBackDesign
var body: some View {
VStack(spacing: layout.horizontalPadding) {
HStack(alignment: .top, spacing: layout.horizontalPadding) {
ForEach(0..<min(10, viewModel.state.tableaus.count), id: \.self) { column in
CardStackView(
cards: viewModel.state.tableaus[column],
location: .tableau(column),
layout: layout,
cardFaceStyle: cardFaceStyle,
cardBackDesign: cardBackDesign,
viewModel: viewModel
)
}
}
.padding(.horizontal, layout.horizontalPadding)
HStack(spacing: layout.horizontalPadding) {
stockPileView
Spacer()
completedFoundations
}
.padding(.horizontal, layout.horizontalPadding)
}
}
@ViewBuilder
private var stockPileView: some View {
if viewModel.state.stock.isEmpty {
CardView(card: nil, cardFaceStyle: cardFaceStyle,
cardBackDesign: cardBackDesign, size: layout.cardSize())
.accessibilityLabel(Text("Stock pile, empty"))
} else {
ZStack {
ForEach(0..<min(stockPileCount, 5), id: \.self) { i in
CardView(card: Card(suit: .spades, rank: .ace),
cardFaceStyle: cardFaceStyle, cardBackDesign: cardBackDesign,
size: layout.cardSize())
.offset(x: CGFloat(i) * 2)
}
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(Text("Stock pile, \(stockPileCount) deals remaining"))
.accessibilityAddTraits(.isButton)
.onTapGesture { viewModel.drawFromStock() }
}
}
@ViewBuilder
private var completedFoundations: some View {
HStack(spacing: 2) {
ForEach(0..<min(8, viewModel.state.foundations.count), id: \.self) { index in
if !viewModel.state.foundations[index].isEmpty {
CardView(card: viewModel.state.foundations[index].last,
cardFaceStyle: cardFaceStyle, cardBackDesign: cardBackDesign,
size: CGSize(width: layout.cardWidth * 0.5,
height: layout.cardHeight * 0.5))
}
}
}
}
private var stockPileCount: Int {
let remaining = viewModel.state.stock.count
guard remaining > 0 else { return 0 }
return (remaining + 9) / 10
}
}