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()`.
|
- 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 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.
|
- 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.
|
- 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 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.
|
- **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.
|
- Avoid using UIKit colors in SwiftUI code.
|
||||||
|
|||||||
@ -185,6 +185,21 @@ final class GameState {
|
|||||||
return engine.getHint(playerHand: hand, dealerUpCard: upCard)
|
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.
|
/// Betting recommendation based on the true count.
|
||||||
var bettingHint: String? {
|
var bettingHint: String? {
|
||||||
guard settings.showCardCount else { return nil }
|
guard settings.showCardCount else { return nil }
|
||||||
|
|||||||
@ -43,23 +43,13 @@ struct ActionButtonsView: View {
|
|||||||
|
|
||||||
// MARK: - Betting Phase Buttons
|
// 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
|
@ViewBuilder
|
||||||
private var bettingButtons: some View {
|
private var bettingButtons: some View {
|
||||||
if state.currentBet > 0 {
|
if state.currentBet > 0 {
|
||||||
VStack(spacing: Design.Spacing.small) {
|
VStack(spacing: Design.Spacing.small) {
|
||||||
// Show hint if bet is below minimum
|
// Show hint if bet is below minimum
|
||||||
if isBetBelowMinimum {
|
if state.isBetBelowMinimum {
|
||||||
Text(String(localized: "Add $\(amountNeededForMinimum) more to meet minimum"))
|
Text(String(localized: "Add $\(state.amountNeededForMinimum) more to meet minimum"))
|
||||||
.font(.system(size: Design.BaseFontSize.small, weight: .medium))
|
.font(.system(size: Design.BaseFontSize.small, weight: .medium))
|
||||||
.foregroundStyle(.orange)
|
.foregroundStyle(.orange)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user