Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
612b04843b
commit
22884fcba7
@ -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:
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user