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

This commit is contained in:
Matt Bruce 2025-12-22 12:20:04 -06:00
parent 8ee7b4c30c
commit 03ee03b876
3 changed files with 76 additions and 13 deletions

View File

@ -49,8 +49,66 @@ You are a **Senior iOS Engineer**, specializing in SwiftUI, SwiftData, and relat
- Do not use `GeometryReader` if a newer alternative would work as well, such as `containerRelativeFrame()` or `visualEffect()`.
- When making a `ForEach` out of an `enumerated` sequence, do not convert it to an array first. So, prefer `ForEach(x.enumerated(), id: \.element.id)` instead of `ForEach(Array(x.enumerated()), id: \.element.id)`.
- When hiding scroll view indicators, use the `.scrollIndicators(.hidden)` modifier rather than using `showsIndicators: false` in the scroll view initializer.
- Place view logic into view models or similar, so it can be tested.
- Avoid `AnyView` unless it is absolutely required.
## View/State separation (MVVM-lite)
**Views should be "dumb" renderers.** All business logic belongs in `GameState` or dedicated view models.
### What belongs in the State/ViewModel:
- **Business logic**: Calculations, validations, game rules
- **Computed properties based on game data**: hints, recommendations, derived values
- **State checks**: `isPlayerTurn`, `canHit`, `isGameOver`, `isBetBelowMinimum`
- **Data transformations**: statistics calculations, filtering, aggregations
### What is acceptable in Views:
- **Pure UI layout logic**: `isIPad`, `maxContentWidth` based on size class
- **Visual styling**: color selection based on state (`valueColor`, `resultColor`)
- **@ViewBuilder sub-views**: breaking up complex layouts
- **Accessibility labels**: combining data into accessible descriptions
### Examples
**❌ BAD - Business logic in view:**
```swift
struct MyView: View {
@Bindable var state: GameState
private var isBetBelowMinimum: Bool {
state.currentBet > 0 && state.currentBet < state.settings.minBet
}
private var currentHint: String? {
guard let hand = state.activeHand else { return nil }
return state.engine.getHint(playerHand: hand, dealerUpCard: upCard)
}
}
```
**✅ GOOD - Logic in GameState, view just reads:**
```swift
// In GameState:
var isBetBelowMinimum: Bool {
currentBet > 0 && currentBet < settings.minBet
}
var currentHint: String? {
guard settings.showHints, isPlayerTurn else { return nil }
guard let hand = activeHand, let upCard = dealerUpCard else { return nil }
return engine.getHint(playerHand: hand, dealerUpCard: upCard)
}
// In View:
if state.isBetBelowMinimum { ... }
if let hint = state.currentHint { HintView(hint: hint) }
```
### Benefits:
- **Testable**: GameState logic can be unit tested without UI
- **Single source of truth**: No duplicated logic across views
- **Cleaner views**: Views focus purely on layout and presentation
- **Easier debugging**: Logic is centralized, not scattered
- **Never use raw numeric literals** for padding, spacing, opacity, font sizes, dimensions, corner radii, shadows, or animation durations—always use Design constants (see "No magic numbers" section).
- **Never use inline `Color(red:green:blue:)` or hex colors**—define all colors in the `Color` extension in `DesignConstants.swift` with semantic names.
- Avoid using UIKit colors in SwiftUI code.

View File

@ -185,6 +185,21 @@ final class GameState {
return engine.getHint(playerHand: hand, dealerUpCard: upCard)
}
/// Whether the current bet is below the minimum required.
var isBetBelowMinimum: Bool {
currentBet > 0 && currentBet < settings.minBet
}
/// Amount needed to reach minimum bet.
var amountNeededForMinimum: Int {
max(0, settings.minBet - currentBet)
}
/// Whether the current bet has reached the maximum.
var isBetAtMaximum: Bool {
currentBet >= settings.maxBet
}
/// Betting recommendation based on the true count.
var bettingHint: String? {
guard settings.showCardCount else { return nil }

View File

@ -43,23 +43,13 @@ struct ActionButtonsView: View {
// MARK: - Betting Phase Buttons
/// Whether the current bet meets the minimum requirement
private var isBetBelowMinimum: Bool {
state.currentBet > 0 && state.currentBet < state.settings.minBet
}
/// Amount needed to reach minimum bet
private var amountNeededForMinimum: Int {
state.settings.minBet - state.currentBet
}
@ViewBuilder
private var bettingButtons: some View {
if state.currentBet > 0 {
VStack(spacing: Design.Spacing.small) {
// Show hint if bet is below minimum
if isBetBelowMinimum {
Text(String(localized: "Add $\(amountNeededForMinimum) more to meet minimum"))
if state.isBetBelowMinimum {
Text(String(localized: "Add $\(state.amountNeededForMinimum) more to meet minimum"))
.font(.system(size: Design.BaseFontSize.small, weight: .medium))
.foregroundStyle(.orange)
.transition(.opacity)