Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
8ee7b4c30c
commit
03ee03b876
@ -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.
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user