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>
77 lines
2.9 KiB
Swift
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
|
|
}
|
|
}
|