Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2025-12-17 14:14:12 -06:00
parent 612b04843b
commit 22884fcba7
2 changed files with 235 additions and 25 deletions

View File

@ -412,6 +412,192 @@ Text(String(localized: "Blackjack!"))
Text(String(localized: "Bust!"))
```
## Responsive Layout (iPhone vs iPad)
### The Problem
On iPad, content that looks great on iPhone will **stretch to fill the screen**, making:
- Betting areas look awkward and disproportionate
- Cards appear too spread out
- Buttons become oversized
- Overlays cover the entire screen unnecessarily
### The Solution: Constrained Width Containers
Use `horizontalSizeClass` to detect iPad and constrain content width:
```swift
struct GameTableView: View {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(\.verticalSizeClass) private var verticalSizeClass
/// Whether we're on iPad (regular horizontal size class)
private var isIPad: Bool {
horizontalSizeClass == .regular
}
/// Maximum content width based on device and orientation
private var maxContentWidth: CGFloat {
if isIPad {
// Landscape on iPad gets more width
return verticalSizeClass == .compact
? CasinoDesign.Size.maxContentWidthLandscape // 800pt
: CasinoDesign.Size.maxContentWidthPortrait // 500pt
}
return .infinity // iPhone uses full width
}
var body: some View {
ZStack {
TableBackgroundView()
VStack {
TopBarView(...)
.frame(maxWidth: maxContentWidth)
// Game table - constrained on iPad
YourTableLayoutView(...)
.frame(maxWidth: maxContentWidth)
// Chip selector - constrained
ChipSelectorView(...)
.frame(maxWidth: maxContentWidth)
// Action buttons - constrained
ActionButtonsView(...)
.frame(maxWidth: maxContentWidth)
}
.frame(maxWidth: .infinity) // Centers constrained content
// Overlays - full screen background, constrained content
if showResultBanner {
ResultBannerView(...)
}
}
}
}
```
### Overlay Pattern (Result Banner, Game Over)
Overlays need special handling: **full-screen dim background** with **constrained content card**:
```swift
struct ResultBannerView: View {
var body: some View {
ZStack {
// 1. Full-screen dark background
Color.black.opacity(0.7)
.ignoresSafeArea()
// 2. Constrained content card
VStack {
// Your content
}
.padding()
.background(RoundedRectangle(cornerRadius: 24).fill(...))
.frame(maxWidth: CasinoDesign.Size.maxModalWidth) // 450pt
// Centered automatically by ZStack
}
}
}
```
### Fixed-Size Containers (Prevent Layout Shifts)
When content changes (cards dealt, buttons appear/disappear), prevent jarring layout shifts:
```swift
// ❌ BAD: Container resizes as cards are added
HStack {
ForEach(cards) { card in
CardView(card: card)
}
}
// ✅ GOOD: Fixed container based on max possible content
ZStack {
// Reserve space for max cards (e.g., 3 cards with overlap)
Color.clear
.frame(width: calculateMaxWidth(), height: cardHeight)
// Actual cards centered within
HStack(spacing: cardSpacing) {
ForEach(cards) { card in
CardView(card: card)
}
}
}
```
Same for buttons:
```swift
// ✅ GOOD: Fixed height container for buttons
ZStack {
Color.clear
.frame(height: 60) // Fixed height
// Buttons animate in/out within fixed space
if showDealButton {
ActionButton("Deal", ...)
.transition(.scale.combined(with: .opacity))
}
}
.animation(.spring(duration: 0.3), value: currentPhase)
```
### Confetti Full-Screen Fix
Confetti must cover the entire screen on iPad:
```swift
struct ConfettiView: View {
var body: some View {
GeometryReader { geometry in
ZStack {
ForEach(0..<50, id: \.self) { _ in
ConfettiPiece(containerSize: geometry.size)
}
}
}
.ignoresSafeArea() // Critical!
.allowsHitTesting(false)
}
}
```
### Design Constants for Responsive Layout
```swift
// In CasinoDesign.swift
enum Size {
// Max widths for iPad constraint
static let maxContentWidthPortrait: CGFloat = 500
static let maxContentWidthLandscape: CGFloat = 800
static let maxModalWidth: CGFloat = 450
}
```
### Common Pitfalls
| Issue | Symptom | Fix |
|-------|---------|-----|
| Stretched table | Betting zones look huge on iPad | Add `.frame(maxWidth: maxContentWidth)` |
| Overlay too wide | Result banner covers entire iPad screen | Use full-screen bg + constrained content card |
| Layout shifts | Cards/buttons cause content to jump | Use fixed-size `ZStack` containers |
| Confetti cut off | Only shows in center portion | Use `GeometryReader` + `.ignoresSafeArea()` |
| Settings rows cramped | Title/subtitle too close to divider | Add `.padding(.vertical, ...)` |
### Testing Checklist
- [ ] iPhone SE (smallest)
- [ ] iPhone Pro Max (largest iPhone)
- [ ] iPad Portrait
- [ ] iPad Landscape
- [ ] iPad Split View
- [ ] Dynamic Type at maximum accessibility size
## Checklist for New Game
### Setup
@ -445,11 +631,19 @@ Text(String(localized: "Bust!"))
- [ ] Add localization for all strings
### Polish
- [ ] Test Dynamic Type scaling
- [ ] Test VoiceOver
- [ ] Test iPad layout
- [ ] Test Dynamic Type scaling (all sizes including accessibility)
- [ ] Test VoiceOver navigation
- [ ] Create app icon using `AppIconView`
### Responsive Layout (iPad)
- [ ] Add `maxContentWidth` constraint to main views
- [ ] Test iPad Portrait - content centered, not stretched
- [ ] Test iPad Landscape - wider constraint works well
- [ ] Verify overlays: full-screen bg, constrained content
- [ ] Fixed-size containers prevent layout shifts
- [ ] Confetti covers full screen
- [ ] Settings rows have proper padding
## What Could Be Added to CasinoKit
The following patterns from Baccarat could be abstracted:

View File

@ -65,6 +65,10 @@
"comment" : "The value of the balance displayed in the top bar.",
"isCommentAutoGenerated" : true
},
"$%lld bet" : {
"comment" : "A value describing the bet amount in the accessibility label. The argument is the bet amount.",
"isCommentAutoGenerated" : true
},
"1. Use Xcode's preview to screenshot these icons" : {
},
@ -124,6 +128,10 @@
}
}
},
"Betting disabled" : {
"comment" : "A hint that appears when a betting zone is disabled.",
"isCommentAutoGenerated" : true
},
"Card face down" : {
"localizations" : {
"en" : {
@ -278,6 +286,10 @@
}
}
},
"Double tap to place bet" : {
"comment" : "A hint text describing the action to take in the betting zone.",
"isCommentAutoGenerated" : true
},
"Eight" : {
"localizations" : {
"en" : {
@ -656,6 +668,10 @@
}
}
},
"No bet" : {
"comment" : "A description of a zone with no active bet.",
"isCommentAutoGenerated" : true
},
"Perfect for learning" : {
"localizations" : {
"en" : {
@ -1078,28 +1094,6 @@
}
}
},
"You've run out of chips!" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "You've run out of chips!"
}
},
"es-MX" : {
"stringUnit" : {
"state" : "translated",
"value" : "¡Te quedaste sin fichas!"
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Vous n'avez plus de jetons!"
}
}
}
},
"WIN" : {
"localizations" : {
"en" : {
@ -1121,6 +1115,28 @@
}
}
}
},
"You've run out of chips!" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "You've run out of chips!"
}
},
"es-MX" : {
"stringUnit" : {
"state" : "translated",
"value" : "¡Te quedaste sin fichas!"
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Vous n'avez plus de jetons!"
}
}
}
}
},
"version" : "1.1"