XCode-Claude-Workflow/SoliCards/Extensions/CardLayout.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

80 lines
2.6 KiB
Swift

import SwiftUI
struct CardLayout {
let availableSize: CGSize
let variant: GameVariant
let deepestColumn: (faceDown: Int, faceUp: Int)
var columnCount: Int { variant.tableauCount }
var isLandscape: Bool { availableSize.width > availableSize.height }
/// Card size constrained by the tighter of width or height.
var cardWidth: CGFloat {
let computed = min(widthConstrainedCardWidth, heightConstrainedCardWidth)
#if os(macOS)
// Cap card width on macOS so cards don't get absurdly large on big displays
return min(computed, 120)
#else
return computed
#endif
}
var cardHeight: CGFloat { cardWidth * 1.4 }
/// Padding between cards. In landscape, this expands to fill the available width
/// so cards spread evenly across the screen.
var horizontalPadding: CGFloat {
let basePadding = max(2, availableSize.width * 0.008)
// If height-constrained (landscape), distribute extra width as padding
if heightConstrainedCardWidth < widthConstrainedCardWidth {
let totalCardWidth = CGFloat(columnCount) * cardWidth
let availableForPadding = availableSize.width - totalCardWidth
let gaps = CGFloat(columnCount + 1)
return max(basePadding, availableForPadding / gaps)
}
return basePadding
}
var verticalOverlapFaceDown: CGFloat { cardHeight * 0.15 }
var verticalOverlapFaceUp: CGFloat { cardHeight * 0.25 }
var cornerRadius: CGFloat { cardWidth * 0.08 }
func cardSize() -> CGSize {
CGSize(width: cardWidth, height: cardHeight)
}
var touchTargetPadding: CGFloat {
max(0, (44 - cardWidth) / 2)
}
// MARK: - Private
private var basePadding: CGFloat {
max(2, availableSize.width * 0.008)
}
private var widthConstrainedCardWidth: CGFloat {
let totalPadding = basePadding * CGFloat(columnCount + 1)
return (availableSize.width - totalPadding) / CGFloat(columnCount)
}
private var heightConstrainedCardWidth: CGFloat {
let d = CGFloat(deepestColumn.faceDown)
let u = CGFloat(max(0, deepestColumn.faceUp - 1))
let totalFactor = 1.0 + 0.1 + (d * 0.15 + u * 0.25 + 1.0)
// Use 92% of available height to leave room for VStack spacing,
// bottom padding, and any unmeasured platform chrome
let usableHeight = availableSize.height * 0.92
let base = max(30, usableHeight / (1.4 * totalFactor))
#if os(iOS)
return isLandscape ? base * 1.3 : base
#else
return base
#endif
}
}