SoliCards-iOS-iPadOS-MacOS/SoliCards/Views/Game/FreeCellBoardView.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

84 lines
3.4 KiB
Swift

import SwiftUI
struct FreeCellBoardView: 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(spacing: layout.horizontalPadding) {
ForEach(0..<min(4, viewModel.state.freeCells.count), id: \.self) { index in
freeCellView(index: index)
}
Spacer()
ForEach(0..<min(4, viewModel.state.foundations.count), id: \.self) { index in
foundationView(index: index)
}
}
.padding(.horizontal, layout.horizontalPadding)
HStack(alignment: .top, spacing: layout.horizontalPadding) {
ForEach(0..<min(8, 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)
}
}
private func freeCellView(index: Int) -> some View {
let card = viewModel.state.freeCells[index]
return CardView(card: card,
cardFaceStyle: cardFaceStyle, cardBackDesign: cardBackDesign,
size: layout.cardSize())
.dropTarget(.freeCell(index))
.accessibilityLabel(card != nil
? Text("Free cell \(index + 1), \(card!.rank.displayName) of \(card!.suit.displayName)")
: Text("Free cell \(index + 1), empty"))
.onTapGesture { viewModel.tapCard(at: .freeCell(index), cardIndex: 0) }
.simultaneousGesture(freeCellDragGesture(index: index))
}
private func foundationView(index: Int) -> some View {
let topCard = viewModel.state.foundations[index].last
return CardView(card: topCard,
cardFaceStyle: cardFaceStyle, cardBackDesign: cardBackDesign,
size: layout.cardSize())
.dropTarget(.foundation(index))
.accessibilityLabel(topCard != nil
? Text("Foundation \(index + 1), \(topCard!.rank.displayName) of \(topCard!.suit.displayName)")
: Text("Foundation \(index + 1), empty"))
}
private func freeCellDragGesture(index: Int) -> some Gesture {
LongPressGesture(minimumDuration: 0.15)
.sequenced(before: DragGesture(coordinateSpace: .named("board")))
.onChanged { value in
if case .second(true, let drag?) = value {
if viewModel.draggedCards.isEmpty, let card = viewModel.state.freeCells[index] {
viewModel.beginDrag(cards: [card], from: .freeCell(index))
}
viewModel.dragPosition = drag.location
}
}
.onEnded { value in
if case .second(true, let drag?) = value {
viewModel.endDrag(at: drag.location)
} else {
viewModel.cancelDrag()
}
}
}
}