BusinessCard/Agents.md

22 KiB

Agent guide for Swift and SwiftUI

This repository contains an Xcode project written with Swift and SwiftUI. Please follow the guidelines below so that the development experience is built on modern, safe API usage.

Additional context files (read first)

  • README.md — product scope, features, and project structure
  • ai_implmentation.md — AI implementation context and architecture notes

Role

You are a Senior iOS Engineer, specializing in SwiftUI, SwiftData, and related frameworks. Your code must always adhere to Apple's Human Interface Guidelines and App Review guidelines.

Core instructions

  • Target iOS 26.0 or later. (Yes, it definitely exists.)
  • Swift 6.2 or later, using modern Swift concurrency.
  • SwiftUI backed up by @Observable classes for shared data.
  • Prioritize Protocol-Oriented Programming (POP) for reusability and testability—see dedicated section below.
  • Do not introduce third-party frameworks without asking first.
  • Avoid UIKit unless requested.

Protocol-Oriented Programming (POP)

Protocol-first architecture is a priority. When designing new features or reviewing existing code, always think about protocols and composition before concrete implementations. This enables code reuse across games, easier testing, and cleaner architecture.

When architecting new code:

  1. Start with the protocol: Before writing a concrete type, ask "What capability am I defining?" and express it as a protocol.
  2. Identify shared behavior: If multiple types will need similar functionality, define a protocol first.
  3. Use protocol extensions for defaults: Provide sensible default implementations to reduce boilerplate.
  4. Prefer composition over inheritance: Combine multiple protocols rather than building deep class hierarchies.

When reviewing existing code for reuse:

  1. Look for duplicated patterns: If you see similar logic in Blackjack and Baccarat, extract a protocol to CasinoKit.
  2. Identify common interfaces: Types that expose similar properties/methods are candidates for protocol unification.
  3. Check before implementing: Before writing new code, search for existing protocols that could be adopted or extended.
  4. Propose refactors proactively: When you spot an opportunity to extract a protocol, mention it.

Protocol design guidelines:

  • Name protocols for capabilities: Use -able, -ing, or -Provider suffixes (e.g., Bettable, CardDealing, StatisticsProvider).
  • Keep protocols focused: Each protocol should represent one capability (Interface Segregation Principle).
  • Use associated types sparingly: Prefer concrete types or generics at the call site when possible.
  • Constrain to AnyObject only when needed: Prefer value semantics unless reference semantics are required.

Examples

BAD - Concrete implementations without protocols:

// Blackjack/GameState.swift
@Observable @MainActor
class BlackjackGameState {
    var balance: Int = 1000
    var currentBet: Int = 0
    func placeBet(_ amount: Int) { ... }
    func resetBet() { ... }
}

// Baccarat/GameState.swift - duplicates the same pattern
@Observable @MainActor
class BaccaratGameState {
    var balance: Int = 1000
    var currentBet: Int = 0
    func placeBet(_ amount: Int) { ... }
    func resetBet() { ... }
}

GOOD - Protocol in CasinoKit, adopted by games:

// CasinoKit/Protocols/Bettable.swift
protocol Bettable: AnyObject {
    var balance: Int { get set }
    var currentBet: Int { get set }
    var minimumBet: Int { get }
    var maximumBet: Int { get }
    
    func placeBet(_ amount: Int)
    func resetBet()
}

extension Bettable {
    func placeBet(_ amount: Int) {
        guard amount <= balance else { return }
        currentBet += amount
        balance -= amount
    }
    
    func resetBet() {
        balance += currentBet
        currentBet = 0
    }
}

// Blackjack/GameState.swift - adopts protocol
@Observable @MainActor
class BlackjackGameState: Bettable {
    var balance: Int = 1000
    var currentBet: Int = 0
    var minimumBet: Int { settings.minBet }
    var maximumBet: Int { settings.maxBet }
    // placeBet and resetBet come from protocol extension
}

BAD - View only works with one concrete type:

struct ChipSelectorView: View {
    @Bindable var state: BlackjackGameState
    // Tightly coupled to Blackjack
}

GOOD - View works with any Bettable type:

struct ChipSelectorView<State: Bettable & Observable>: View {
    @Bindable var state: State
    // Reusable across all games
}

Common protocols to consider extracting:

Capability Protocol Name Shared By
Betting mechanics Bettable All games
Statistics tracking StatisticsProvider All games
Game settings GameConfigurable All games
Card management CardProviding Card games
Round lifecycle RoundManaging All games
Result calculation ResultCalculating All games

Refactoring checklist:

When you encounter code that could benefit from POP:

  • Is this logic duplicated across multiple games?
  • Could this type conform to an existing protocol in CasinoKit?
  • Would extracting a protocol make this code testable in isolation?
  • Can views be made generic over a protocol instead of a concrete type?
  • Would a protocol extension reduce boilerplate across conforming types?

Benefits:

  • Reusability: Shared protocols in CasinoKit work across all games
  • Testability: Mock types can conform to protocols for unit testing
  • Flexibility: New games can adopt existing protocols immediately
  • Maintainability: Fix a bug in a protocol extension, fix it everywhere
  • Discoverability: Protocols document the expected interface clearly

Swift instructions

  • Always mark @Observable classes with @MainActor.
  • Assume strict Swift concurrency rules are being applied.
  • Prefer Swift-native alternatives to Foundation methods where they exist, such as using replacing("hello", with: "world") with strings rather than replacingOccurrences(of: "hello", with: "world").
  • Prefer modern Foundation API, for example URL.documentsDirectory to find the app's documents directory, and appending(path:) to append strings to a URL.
  • Never use C-style number formatting such as Text(String(format: "%.2f", abs(myNumber))); always use Text(abs(change), format: .number.precision(.fractionLength(2))) instead.
  • Prefer static member lookup to struct instances where possible, such as .circle rather than Circle(), and .borderedProminent rather than BorderedProminentButtonStyle().
  • Never use old-style Grand Central Dispatch concurrency such as DispatchQueue.main.async(). If behavior like this is needed, always use modern Swift concurrency.
  • Filtering text based on user-input must be done using localizedStandardContains() as opposed to contains().
  • Avoid force unwraps and force try unless it is unrecoverable.

SwiftUI instructions

  • Always use foregroundStyle() instead of foregroundColor().
  • Always use clipShape(.rect(cornerRadius:)) instead of cornerRadius().
  • Always use the Tab API instead of tabItem().
  • Never use ObservableObject; always prefer @Observable classes instead.
  • Never use the onChange() modifier in its 1-parameter variant; either use the variant that accepts two parameters or accepts none.
  • Never use onTapGesture() unless you specifically need to know a tap's location or the number of taps. All other usages should use Button.
  • Never use Task.sleep(nanoseconds:); always use Task.sleep(for:) instead.
  • Never use UIScreen.main.bounds to read the size of the available space.
  • Do not break views up using computed properties; place them into new View structs instead.
  • Do not force specific font sizes; prefer using Dynamic Type instead.
  • Use the navigationDestination(for:) modifier to specify navigation, and always use NavigationStack instead of the old NavigationView.
  • If using an image for a button label, always specify text alongside like this: Button("Tap me", systemImage: "plus", action: myButtonAction).
  • When rendering SwiftUI views, always prefer using ImageRenderer to UIGraphicsImageRenderer.
  • Don't apply the fontWeight() modifier unless there is good reason. If you want to make some text bold, always use bold() instead of fontWeight(.bold).
  • 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.
  • Avoid AnyView unless it is absolutely required.
  • 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/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:

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:

// 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

SwiftData instructions

If SwiftData is configured to use CloudKit:

  • Never use @Attribute(.unique).
  • Model properties must always either have default values or be marked as optional.
  • All relationships must be marked optional.

Localization instructions

  • Use String Catalogs (.xcstrings files) for localization—this is Apple's modern approach for iOS 17+.
  • SwiftUI Text("literal") views automatically look up strings in the String Catalog; no additional code is needed for static strings.
  • For strings outside of Text views or with dynamic content, use String(localized:) or create a helper extension:
    extension String {
        static func localized(_ key: String) -> String {
            String(localized: String.LocalizationValue(key))
        }
        static func localized(_ key: String, _ arguments: CVarArg...) -> String {
            let format = String(localized: String.LocalizationValue(key))
            return String(format: format, arguments: arguments)
        }
    }
    
  • For format strings with interpolation (e.g., "Balance: $%@"), define a key in the String Catalog and use String.localized("key", value).
  • Store all user-facing strings in the String Catalog; avoid hardcoding strings directly in views.
  • Support at minimum: English (en), Spanish-Mexico (es-MX), and French-Canada (fr-CA).
  • Never use NSLocalizedString; prefer the modern String(localized:) API.

No magic numbers or hardcoded values

Never use raw numeric literals or hardcoded colors directly in views. All values must be extracted to named constants, enums, or variables. This applies to:

Values that MUST be constants:

  • Spacing & Padding: .padding(Design.Spacing.medium) not .padding(12)
  • Corner Radii: Design.CornerRadius.large not cornerRadius: 16
  • Font Sizes: Design.BaseFontSize.body not size: 14
  • Opacity Values: Design.Opacity.strong not .opacity(0.7)
  • Colors: Color.Primary.accent not Color(red: 0.8, green: 0.6, blue: 0.2)
  • Line Widths: Design.LineWidth.medium not lineWidth: 2
  • Shadow Values: Design.Shadow.radiusLarge not radius: 10
  • Animation Durations: Design.Animation.quick not duration: 0.3
  • Component Sizes: Design.Size.chipBadge not frame(width: 32)

What to do when you see a magic number:

  1. Check if an appropriate constant already exists in DesignConstants.swift
  2. If not, add a new constant with a semantic name
  3. Use the constant in place of the raw value
  4. If it's truly view-specific and used only once, extract to a private let at the top of the view struct

Examples of violations:

// ❌ BAD - Magic numbers everywhere
.padding(16)
.opacity(0.6)
.frame(width: 80, height: 52)
.shadow(radius: 10, y: 5)
Color(red: 0.25, green: 0.3, blue: 0.45)

// ✅ GOOD - Named constants
.padding(Design.Spacing.large)
.opacity(Design.Opacity.accent)
.frame(width: Design.Size.bonusZoneWidth, height: Design.Size.topBetRowHeight)
.shadow(radius: Design.Shadow.radiusLarge, y: Design.Shadow.offsetLarge)
Color.BettingZone.dragonBonusLight

Design constants instructions

  • Create a centralized design constants file (e.g., DesignConstants.swift) using enums for namespacing:
    enum Design {
        enum Spacing {
            static let xxSmall: CGFloat = 2
            static let xSmall: CGFloat = 4
            static let small: CGFloat = 8
            static let medium: CGFloat = 12
            static let large: CGFloat = 16
            static let xLarge: CGFloat = 20
        }
        enum CornerRadius {
            static let small: CGFloat = 8
            static let medium: CGFloat = 12
            static let large: CGFloat = 16
        }
        enum BaseFontSize {
            static let small: CGFloat = 10
            static let body: CGFloat = 14
            static let large: CGFloat = 18
            static let title: CGFloat = 24
        }
        enum Opacity {
            static let subtle: Double = 0.1
            static let hint: Double = 0.2
            static let light: Double = 0.3
            static let medium: Double = 0.5
            static let accent: Double = 0.6
            static let strong: Double = 0.7
            static let heavy: Double = 0.8
            static let almostFull: Double = 0.9
        }
        enum LineWidth {
            static let thin: CGFloat = 1
            static let medium: CGFloat = 2
            static let thick: CGFloat = 3
        }
        enum Shadow {
            static let radiusSmall: CGFloat = 2
            static let radiusMedium: CGFloat = 6
            static let radiusLarge: CGFloat = 10
            static let offsetSmall: CGFloat = 1
            static let offsetMedium: CGFloat = 3
        }
        enum Animation {
            static let quick: Double = 0.3
            static let springDuration: Double = 0.4
            static let staggerDelay1: Double = 0.1
            static let staggerDelay2: Double = 0.25
        }
    }
    
  • For colors used across the app, extend Color with semantic color definitions:
    extension Color {
        enum Primary {
            static let background = Color(red: 0.1, green: 0.2, blue: 0.3)
            static let accent = Color(red: 0.8, green: 0.6, blue: 0.2)
        }
        enum Button {
            static let goldLight = Color(red: 1.0, green: 0.85, blue: 0.3)
            static let goldDark = Color(red: 0.9, green: 0.7, blue: 0.2)
        }
    }
    
  • Within each view, extract view-specific magic numbers to private constants at the top of the struct with a comment explaining why they're local:
    struct MyView: View {
        // Layout: fixed card dimensions for consistent appearance
        private let cardWidth: CGFloat = 45
        // Typography: constrained space requires fixed size
        private let headerFontSize: CGFloat = 18
        // ...
    }
    
  • Reference design constants in views: Design.Spacing.medium, Design.CornerRadius.large, Color.Primary.accent.
  • Keep design constants organized by category: Spacing, CornerRadius, BaseFontSize, IconSize, Size, Animation, Opacity, LineWidth, Shadow.
  • When adding new features, check existing constants first before creating new ones.
  • Name constants semantically (what they represent) not literally (their value): accent not pointSix, large not sixteen.

Dynamic Type instructions

  • Always support Dynamic Type for accessibility; never use fixed font sizes without scaling.
  • Use @ScaledMetric to scale custom font sizes and dimensions based on user accessibility settings:
    struct MyView: View {
        @ScaledMetric(relativeTo: .body) private var bodyFontSize: CGFloat = 14
        @ScaledMetric(relativeTo: .title) private var titleFontSize: CGFloat = 24
        @ScaledMetric(relativeTo: .caption) private var chipTextSize: CGFloat = 11
    
        var body: some View {
            Text("Hello")
                .font(.system(size: bodyFontSize, weight: .medium))
        }
    }
    
  • Choose the appropriate relativeTo text style based on the semantic purpose:
    • .largeTitle, .title, .title2, .title3 for headings
    • .headline, .subheadline for emphasized content
    • .body for main content
    • .callout, .footnote, .caption, .caption2 for smaller text
  • For constrained UI elements (chips, cards, badges) where overflow would break the design, you may use fixed sizes but document the reason:
    // Fixed size: chip face has strict space constraints
    private let chipValueFontSize: CGFloat = 11
    
  • Prefer system text styles when possible: .font(.body), .font(.title), .font(.caption).
  • Test with accessibility settings: Settings > Accessibility > Display & Text Size > Larger Text.

VoiceOver accessibility instructions

  • All interactive elements (buttons, betting zones, selectable items) must have meaningful .accessibilityLabel().
  • Use .accessibilityValue() to communicate dynamic state (e.g., current bet amount, selection state, hand value).
  • Use .accessibilityHint() to describe what will happen when interacting with an element:
    Button("Deal", action: deal)
        .accessibilityHint("Deals cards and starts the round")
    
  • Use .accessibilityAddTraits() to communicate element type:
    • .isButton for tappable elements that aren't SwiftUI Buttons
    • .isHeader for section headers
    • .isModal for modal overlays
    • .updatesFrequently for live-updating content
  • Hide purely decorative elements from VoiceOver:
    TableBackgroundView()
        .accessibilityHidden(true) // Decorative element
    
  • Group related elements to reduce VoiceOver navigation complexity:
    VStack {
        handLabel
        cardStack
        valueDisplay
    }
    .accessibilityElement(children: .ignore)
    .accessibilityLabel("Player hand")
    .accessibilityValue("Ace of Hearts, King of Spades. Value: 1")
    
  • For complex elements, use .accessibilityElement(children: .contain) to allow navigation to children while adding context.
  • Post accessibility announcements for important events:
    Task { @MainActor in
        try? await Task.sleep(for: .milliseconds(500))
        UIAccessibility.post(notification: .announcement, argument: "Player wins!")
    }
    
  • Provide accessibility names for model types that appear in UI:
    enum Suit {
        var accessibilityName: String {
            switch self {
            case .hearts: return String(localized: "Hearts")
            // ...
            }
        }
    }
    
  • Test with VoiceOver enabled: Settings > Accessibility > VoiceOver.

Project structure

  • Use a consistent project structure, with folder layout determined by app features.
  • Follow strict naming conventions for types, properties, methods, and SwiftData models.
  • Break different types up into different Swift files rather than placing multiple structs, classes, or enums into a single file.
  • Write unit tests for core application logic.
  • Only write UI tests if unit tests are not possible.
  • Add code comments and documentation comments as needed.
  • If the project requires secrets such as API keys, never include them in the repository.

Documentation instructions

  • Always keep each game's README.md file up to date when adding new functionality or making changes that users or developers need to know about.
  • Document new features, settings, or gameplay mechanics in the appropriate game's README.
  • Update the README when modifying existing behavior that affects how the game works.
  • Include any configuration options, keyboard shortcuts, or special interactions.
  • If adding a new game to the workspace, create a comprehensive README following the existing games' format.
  • README updates should be part of the same commit as the feature/change they document.

PR instructions

  • If installed, make sure SwiftLint returns no warnings or errors before committing.
  • Verify that the game's README.md reflects any new functionality or behavioral changes.