From 045a705d83e165d2662a5f4a09b88875afa1c948 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Sat, 10 Jan 2026 15:27:49 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- Agents.md | 1034 ++++++++++++++++++------------ BRANDING_IMPLEMENTATION_GUIDE.md | 694 ++++++++++++++++++++ SETTINGS_STYLING_GUIDE.md | 957 +++++++++++++++++++++++++++ WORKSPACE.md | 34 + 4 files changed, 2322 insertions(+), 397 deletions(-) create mode 100644 BRANDING_IMPLEMENTATION_GUIDE.md create mode 100644 SETTINGS_STYLING_GUIDE.md create mode 100644 WORKSPACE.md diff --git a/Agents.md b/Agents.md index 3c76fef..d21d466 100644 --- a/Agents.md +++ b/Agents.md @@ -1,508 +1,748 @@ -# Agent guide for Swift and SwiftUI +# 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 + +Before starting work, read project documentation: + +- `WORKSPACE.md` — (if present) Multi-project workspace overview and project relationships +- `README.md` — Project scope, features, and architecture +- In multi-project workspaces, each project folder has its own `README.md` + +When making architectural changes, keep documentation files in sync with code changes. + ## 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 +## 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. +- **Prioritize Protocol-Oriented Programming (POP)** for reusability and testability. +- **Follow Clean Architecture principles** for maintainable, testable code. - Do not introduce third-party frameworks without asking first. - Avoid UIKit unless requested. +## Clean Architecture + +**Separation of concerns is mandatory.** Code should be organized into distinct layers with clear responsibilities and dependencies flowing inward. + +### File Organization Principles + +1. **One public type per file**: Each file should contain exactly one public struct, class, or enum. Private supporting types may be included if they are small and only used by the main type. + +2. **Keep files lean**: Aim for files under 300 lines. If a file exceeds this: + - Extract reusable sub-views into separate files in a `Components/` folder + - Extract sheets/modals into a `Sheets/` folder + - Move complex logic into dedicated types + +3. **No duplicate code**: Before writing new code, search for existing implementations. Extract common patterns into reusable components. + +4. **Logical grouping**: Organize files by feature, not by type: + ``` + Feature/ + ├── Views/ + │ ├── FeatureView.swift + │ ├── Components/ + │ │ ├── FeatureRowView.swift + │ │ └── FeatureHeaderView.swift + │ └── Sheets/ + │ └── FeatureEditSheet.swift + ├── Models/ + │ └── FeatureModel.swift + └── State/ + └── FeatureStore.swift + ``` + +### Layer Responsibilities + +| Layer | Contains | Depends On | +|-------|----------|------------| +| **Views** | SwiftUI views, UI components | State, Models | +| **State** | `@Observable` stores, view models | Models, Services | +| **Services** | Business logic, networking, persistence | Models | +| **Models** | Data types, entities, DTOs | Nothing | +| **Protocols** | Interfaces for services and stores | Models | + +### Architecture Rules + +1. **Views are dumb renderers**: No business logic in views. Views read state and call methods. +2. **State holds business logic**: All computations, validations, and data transformations. +3. **Services are stateless**: Pure functions where possible. Injected via protocols. +4. **Models are simple**: Plain data types. No dependencies on UI or services. + +### Example Structure + +``` +App/ +├── Design/ # Design constants, colors, typography +├── Localization/ # String helpers +├── Models/ # Data models (SwiftData, plain structs) +├── Protocols/ # Protocol definitions for DI +├── Services/ # Business logic, API clients, persistence +├── State/ # Observable stores, app state +└── Views/ + ├── Components/ # Reusable UI components + ├── Sheets/ # Modal presentations + └── [Feature]/ # Feature-specific views +``` + + ## 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. +**Protocol-first architecture is a priority.** When designing new features, always think about protocols and composition before concrete implementations. -### When architecting new code: +### 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: +### When Reviewing Existing Code -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. +1. **Look for duplicated patterns**: Similar logic across files is a candidate for protocol extraction. +2. **Identify common interfaces**: Types that expose similar properties/methods should conform to a shared protocol. +3. **Check before implementing**: 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: +### Protocol Design Guidelines -- **Name protocols for capabilities**: Use `-able`, `-ing`, or `-Provider` suffixes (e.g., `Bettable`, `CardDealing`, `StatisticsProvider`). +- **Name protocols for capabilities**: Use `-able`, `-ing`, or `-Provider` suffixes (e.g., `Shareable`, `DataProviding`, `Persistable`). - **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 +### Benefits -**❌ BAD - Concrete implementations without protocols:** -```swift -// 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:** -```swift -// 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:** -```swift -struct ChipSelectorView: View { - @Bindable var state: BlackjackGameState - // Tightly coupled to Blackjack -} -``` - -**✅ GOOD - View works with any Bettable type:** -```swift -struct ChipSelectorView: 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 +- **Reusability**: Shared protocols work across features - **Testability**: Mock types can conform to protocols for unit testing -- **Flexibility**: New games can adopt existing protocols immediately +- **Flexibility**: New features 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 +## View/State Separation (MVVM-lite) + +**Views should be "dumb" renderers.** All business logic belongs in stores or dedicated view models. + +### What Belongs in State/Store + +- **Business logic**: Calculations, validations, rules +- **Computed properties based on data**: Hints, recommendations, derived values +- **State checks**: `canSubmit`, `isLoading`, `hasError` +- **Data transformations**: Filtering, sorting, aggregations + +### What is Acceptable in Views + +- **Pure UI layout logic**: Adaptive layouts based on size class +- **Visual styling**: Color selection based on state +- **@ViewBuilder sub-views**: Breaking up complex layouts (keep in same file if small) +- **Accessibility labels**: Combining data into accessible descriptions + +### Example + +```swift +// ❌ BAD - Business logic in view +struct MyView: View { + @Bindable var state: FeatureState + + private var isValid: Bool { + !state.name.isEmpty && state.email.contains("@") + } +} + +// ✅ GOOD - Logic in State, view just reads +// In FeatureState: +var isValid: Bool { + !name.isEmpty && email.contains("@") +} + +// In View: +Button("Save") { state.save() } + .disabled(!state.isValid) +``` + + +## 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. +- Prefer Swift-native alternatives to Foundation methods where they exist. +- Prefer modern Foundation API (e.g., `URL.documentsDirectory`, `appending(path:)`). +- Never use C-style number formatting; use `format:` modifiers instead. +- Prefer static member lookup to struct instances (`.circle` not `Circle()`). +- Never use old-style GCD; use modern Swift concurrency. +- Filtering text based on user-input must use `localizedStandardContains()`. +- Avoid force unwraps and force `try` unless unrecoverable. -## SwiftUI instructions +## 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. +- Never use `ObservableObject`; always prefer `@Observable` classes. +- Never use `onChange()` in its 1-parameter variant. +- Never use `onTapGesture()` unless you need tap location/count; use `Button`. +- Never use `Task.sleep(nanoseconds:)`; use `Task.sleep(for:)`. +- Never use `UIScreen.main.bounds` to read available space. +- Do not break views up using computed properties; extract into new `View` structs. +- Do not force specific font sizes; prefer Dynamic Type. +- Use `NavigationStack` with `navigationDestination(for:)`. +- If using an image for a button label, always specify text alongside. +- Prefer `ImageRenderer` to `UIGraphicsImageRenderer`. +- Use `bold()` instead of `fontWeight(.bold)`. +- Avoid `GeometryReader` if newer alternatives work (e.g., `containerRelativeFrame()`). +- When enumerating in `ForEach`, don't convert to Array first. +- Hide scroll indicators with `.scrollIndicators(.hidden)`. +- Avoid `AnyView` unless absolutely required. +- **Never use raw numeric literals** for padding, spacing, opacity, etc.—use Design constants. +- **Never use inline colors**—define all colors with semantic names. +- Avoid UIKit colors in SwiftUI code. -## View/State separation (MVVM-lite) +## watchOS Development (CRITICAL) -**Views should be "dumb" renderers.** All business logic belongs in `GameState` or dedicated view models. +**Read this entire section before implementing any watch functionality.** -### 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 +### Creating a Watch Target -### 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 +When adding a watchOS target to an existing iOS app: -### Examples +1. **File → New → Target → "Watch App for watchOS"** +2. Choose **"Watch App for Existing iOS App"** (NOT standalone) +3. Name it appropriately (e.g., `AppNameWatch`) +4. Xcode creates a folder like `AppNameWatch Watch App/` + +### CRITICAL: Embedding the Watch App + +⚠️ **THIS IS THE #1 CAUSE OF "WATCH APP NOT INSTALLED" ERRORS** ⚠️ + +The watch app MUST be embedded in the iOS app for deployment to real devices: + +1. Select the **iOS target** in Xcode +2. Go to **Build Phases** tab +3. Verify there's an **"Embed Watch Content"** phase +4. **CRITICAL**: Ensure **"Code Sign On Copy"** is CHECKED ✓ + +If "Embed Watch Content" doesn't exist: +1. Click **"+"** → **"New Copy Files Phase"** +2. Rename to **"Embed Watch Content"** +3. Set **Destination** to **"Products Directory"** +4. Set **Subpath** to `$(CONTENTS_FOLDER_PATH)/Watch` +5. Add the watch app (e.g., `AppNameWatch Watch App.app`) +6. **CHECK "Code Sign On Copy"** ← This is critical! + +Without proper embedding, the iOS app installs but the watch app does NOT install on the paired Apple Watch. + +### Bundle Identifiers + +Watch app bundle IDs MUST be prefixed with the iOS app's bundle ID: + +``` +iOS app: com.company.AppName +Watch app: com.company.AppName.watchkitapp ← MUST start with iOS bundle ID +``` + +Also verify `WKCompanionAppBundleIdentifier` in the watch target's build settings matches the iOS app's bundle ID exactly. + +### Data Sync: WatchConnectivity (NOT App Groups) + +**DO NOT use App Groups for iPhone ↔ Watch data sharing.** + +App Groups: +- ❌ Do NOT work between iPhone and Apple Watch +- ❌ Different container paths on each device +- ❌ Will waste hours debugging why data isn't syncing +- ✅ Only work between an app and its extensions on the SAME device + +**Use WatchConnectivity framework instead:** -**❌ BAD - Business logic in view:** ```swift -struct MyView: View { - @Bindable var state: GameState +// iOS side - WatchConnectivityService.swift +import WatchConnectivity + +@MainActor +final class WatchConnectivityService: NSObject, WCSessionDelegate { + static let shared = WatchConnectivityService() - private var isBetBelowMinimum: Bool { - state.currentBet > 0 && state.currentBet < state.settings.minBet + private override init() { + super.init() + if WCSession.isSupported() { + WCSession.default.delegate = self + WCSession.default.activate() + } } - private var currentHint: String? { - guard let hand = state.activeHand else { return nil } - return state.engine.getHint(playerHand: hand, dealerUpCard: upCard) + func syncData(_ data: [String: Any]) { + guard WCSession.default.activationState == .activated, + WCSession.default.isPaired, + WCSession.default.isWatchAppInstalled else { return } + + try? WCSession.default.updateApplicationContext(data) } } ``` -**✅ GOOD - Logic in GameState, view just reads:** +### WatchConnectivity Methods + +| Method | Use Case | +|--------|----------| +| `updateApplicationContext` | Latest state that persists (use this for most syncs) | +| `sendMessage` | Immediate delivery when counterpart is reachable | +| `transferUserInfo` | Queued delivery, guaranteed but not immediate | + +### watchOS Framework Limitations + +These iOS frameworks are NOT available on watchOS: + +- ❌ `CoreImage` - Generate QR codes on iOS, send image data to watch +- ❌ `UIKit` (mostly) - Use SwiftUI +- ❌ `AVFoundation` (limited) + +### Simulator Limitations + +WatchConnectivity on simulators is **unreliable**: + +- `isWatchAppInstalled` often returns `false` even when running +- `isReachable` may be `false` even with both apps running +- `updateApplicationContext` may fail with "counterpart not installed" + +**Workarounds for simulator testing:** +1. Add `#if targetEnvironment(simulator)` blocks with sample data +2. Test real sync functionality on physical devices only + +### Debugging Watch Sync Issues + +If `isWatchAppInstalled` returns `false`: + +1. ✅ Check "Embed Watch Content" build phase exists +2. ✅ Check "Code Sign On Copy" is enabled +3. ✅ Verify bundle ID is prefixed correctly +4. ✅ Clean build folder (⇧⌘K) and rebuild +5. ✅ On iPhone, open Watch app → verify app appears under "Installed" + +### NSObject Requirement + +`WCSessionDelegate` is an Objective-C protocol, so conforming classes must inherit from `NSObject`: + ```swift -// In GameState: -var isBetBelowMinimum: Bool { - currentBet > 0 && currentBet < settings.minBet +final class WatchConnectivityService: NSObject, WCSessionDelegate { + // NSObject is required for WCSessionDelegate conformance } - -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 +## 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. +- Model properties must have default values or be optional. - All relationships must be marked optional. -## Localization instructions +## Model Design: Single Source of Truth -- 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: - ```swift - 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. +**Computed properties should be the single source of truth for derived data.** + +### Name Fields Pattern + +When a model has multiple name components (prefix, firstName, middleName, lastName, suffix, etc.), use a computed property for the display name: + +```swift +// ✅ GOOD - Computed from individual fields +var fullName: String { + var parts: [String] = [] + if !prefix.isEmpty { parts.append(prefix) } + if !firstName.isEmpty { parts.append(firstName) } + if !lastName.isEmpty { parts.append(lastName) } + // ... etc + return parts.joined(separator: " ") +} + +// ❌ BAD - Stored displayName that can get out of sync +var displayName: String // Never add this +``` + +### Benefits + +- **Always up to date**: Changes to individual fields are immediately reflected +- **No sync bugs**: No risk of stored value diverging from component fields +- **Simpler code**: No need to update displayName when editing name fields + +### Related Properties + +If you need different formats for different purposes: + +- `fullName` — For display (may include formatting like quotes, parentheses) +- `vCardName` — For export (plain format, no special formatting) -## No magic numbers or hardcoded values +## Localization Instructions -**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: +- Use **String Catalogs** (`.xcstrings` files) for localization. +- SwiftUI `Text("literal")` views automatically look up strings in the catalog. +- For strings outside of `Text` views, use `String(localized:)` or a helper extension. +- Store all user-facing strings in the String Catalog. +- Support at minimum: English (en), Spanish-Mexico (es-MX), French-Canada (fr-CA). +- Never use `NSLocalizedString`; prefer `String(localized:)`. -### Values that MUST be constants: -- **Spacing & Padding**: `.padding(Design.Spacing.medium)` not `.padding(12)` + +## Design Constants + +**Never use raw numeric literals or hardcoded colors directly in views.** + +### Values That MUST Be Constants + +- **Spacing & 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)` +- **Colors**: `Color.Primary.accent` not `Color(red:green:blue:)` - **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)` +- **Component Sizes**: `Design.Size.avatar` not `frame(width: 56)` -### 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 +### Organization -### Examples of violations: -```swift -// ❌ 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) +- Create a `DesignConstants.swift` file using enums for namespacing. +- Extend `Color` with semantic color definitions. +- View-specific constants go at the top of the view struct with a comment. +- Name constants semantically: `accent` not `pointSix`, `large` not `sixteen`. -// ✅ 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 + +## App Identifiers (xcconfig) + +**Centralize all company-specific identifiers** using xcconfig files for true single-source configuration. This enables one-line migration between developer accounts. + +### Why xcconfig? + +- **Single source of truth**: Change one file, everything updates +- **Build-time resolution**: Bundle IDs, entitlements, and Swift code all derive from same source +- **No manual updates**: Entitlements use variable substitution +- **Environment support**: Easy Debug/Release/Staging configurations + +### Setup Instructions + +#### Step 1: Create xcconfig Files + +Create `Configuration/Base.xcconfig`: + +``` +// Base.xcconfig - Source of truth for all identifiers +// MIGRATION: Update COMPANY_IDENTIFIER and DEVELOPMENT_TEAM below + +// ============================================================================= +// COMPANY IDENTIFIER - CHANGE THIS FOR MIGRATION +// ============================================================================= + +COMPANY_IDENTIFIER = com.yourcompany +APP_NAME = YourAppName +DEVELOPMENT_TEAM = YOUR_TEAM_ID + +// ============================================================================= +// DERIVED IDENTIFIERS - DO NOT EDIT +// ============================================================================= + +APP_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME) +WATCH_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).watchkitapp +APPCLIP_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).Clip +TESTS_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME)Tests +UITESTS_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME)UITests + +APP_GROUP_IDENTIFIER = group.$(COMPANY_IDENTIFIER).$(APP_NAME) +CLOUDKIT_CONTAINER_IDENTIFIER = iCloud.$(COMPANY_IDENTIFIER).$(APP_NAME) + +APPCLIP_DOMAIN = yourapp.example.com ``` +Create `Configuration/Debug.xcconfig`: -## Design constants instructions +``` +// Debug.xcconfig +#include "Base.xcconfig" +// Add debug-specific settings here +``` -- Create a centralized design constants file (e.g., `DesignConstants.swift`) using enums for namespacing: - ```swift - 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: - ```swift - 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: - ```swift - 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`. +Create `Configuration/Release.xcconfig`: + +``` +// Release.xcconfig +#include "Base.xcconfig" +// Add release-specific settings here +``` + +#### Step 2: Configure Xcode Project + +In `project.pbxproj`, add file references and set `baseConfigurationReference` for each build configuration: + +**1. Add xcconfig file references to PBXFileReference section:** + +``` +/* Use SOURCE_ROOT and full path from project root */ +EACONFIG001 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppName/Configuration/Base.xcconfig; sourceTree = SOURCE_ROOT; }; +EACONFIG002 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppName/Configuration/Debug.xcconfig; sourceTree = SOURCE_ROOT; }; +EACONFIG003 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppName/Configuration/Release.xcconfig; sourceTree = SOURCE_ROOT; }; +``` + +**IMPORTANT**: Use `sourceTree = SOURCE_ROOT` (not `""`) and include the full path from project root (e.g., `AppName/Configuration/Base.xcconfig`). + +**2. Set `baseConfigurationReference` on project-level Debug/Release configurations:** + +``` +EA123456 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EACONFIG002 /* Debug.xcconfig */; + buildSettings = { ... }; +}; +``` + +**3. Replace hardcoded values with variables:** + +``` +PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)"; +DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; +INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "$(APP_BUNDLE_IDENTIFIER)"; +``` + +#### Step 3: Update Entitlements + +Use variable substitution in `.entitlements` files: + +```xml +com.apple.developer.icloud-container-identifiers + + $(CLOUDKIT_CONTAINER_IDENTIFIER) + +com.apple.security.application-groups + + $(APP_GROUP_IDENTIFIER) + +``` + +#### Step 4: Bridge to Swift via Info.plist + +Add keys to `Info.plist` that bridge xcconfig values to Swift: + +```xml +AppGroupIdentifier +$(APP_GROUP_IDENTIFIER) +CloudKitContainerIdentifier +$(CLOUDKIT_CONTAINER_IDENTIFIER) +AppClipDomain +$(APPCLIP_DOMAIN) +``` + +#### Step 5: Create Swift Interface + +Create `Configuration/AppIdentifiers.swift`: + +```swift +import Foundation + +enum AppIdentifiers { + // Read from Info.plist (values come from xcconfig) + static let appGroupIdentifier: String = { + Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as? String + ?? "group.com.yourcompany.AppName" + }() + + static let cloudKitContainerIdentifier: String = { + Bundle.main.object(forInfoDictionaryKey: "CloudKitContainerIdentifier") as? String + ?? "iCloud.com.yourcompany.AppName" + }() + + static let appClipDomain: String = { + Bundle.main.object(forInfoDictionaryKey: "AppClipDomain") as? String + ?? "yourapp.example.com" + }() + + // Derived from bundle identifier + static var bundleIdentifier: String { + Bundle.main.bundleIdentifier ?? "com.yourcompany.AppName" + } + + static var watchBundleIdentifier: String { "\(bundleIdentifier).watchkitapp" } + static var appClipBundleIdentifier: String { "\(bundleIdentifier).Clip" } + + static func appClipURL(recordName: String) -> URL? { + URL(string: "https://\(appClipDomain)/appclip?id=\(recordName)") + } +} +``` + +### Data Flow + +``` +Base.xcconfig (source of truth) + ↓ +project.pbxproj (baseConfigurationReference) + ↓ +Build Settings → Bundle IDs, Team ID, etc. + ↓ +Info.plist (bridges values via $(VARIABLE)) + ↓ +AppIdentifiers.swift (Swift reads from Bundle.main) +``` + +### Usage in Code + +```swift +// Always use AppIdentifiers instead of hardcoding +FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: AppIdentifiers.appGroupIdentifier +) + +CKContainer(identifier: AppIdentifiers.cloudKitContainerIdentifier) +``` + +### Adding New Targets + +When adding new targets (Widgets, Intents, App Clips, etc.), follow this pattern: + +#### 1. Add Bundle ID Variable to Base.xcconfig + +``` +// In Base.xcconfig, add new derived identifier +WIDGET_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).Widget +INTENT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).Intent +``` + +#### 2. Set Target to Use xcconfig + +For the new target's Debug/Release configurations in `project.pbxproj`: + +``` +EA_NEW_TARGET_DEBUG /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EACONFIG002 /* Debug.xcconfig */; + buildSettings = { + PRODUCT_BUNDLE_IDENTIFIER = "$(WIDGET_BUNDLE_IDENTIFIER)"; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; + // ... other settings + }; +}; +``` + +#### 3. Configure Entitlements (if needed) + +If the target needs App Groups or CloudKit access, create an entitlements file using variables: + +```xml + +com.apple.security.application-groups + + $(APP_GROUP_IDENTIFIER) + +``` + +#### 4. Share Code via App Groups + +Extensions must use App Groups to share data with the main app: + +```swift +// In extension code +let sharedDefaults = UserDefaults(suiteName: AppIdentifiers.appGroupIdentifier) +let containerURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: AppIdentifiers.appGroupIdentifier +) +``` + +#### 5. Update AppIdentifiers.swift (if needed) + +Add new computed properties for target-specific identifiers: + +```swift +static var widgetBundleIdentifier: String { "\(bundleIdentifier).Widget" } +static var intentBundleIdentifier: String { "\(bundleIdentifier).Intent" } +``` + +#### Common Target Types and Bundle ID Patterns + +| Target Type | Bundle ID Variable | Example Value | +|-------------|-------------------|---------------| +| Widget Extension | `WIDGET_BUNDLE_IDENTIFIER` | `$(APP_BUNDLE_IDENTIFIER).Widget` | +| Intent Extension | `INTENT_BUNDLE_IDENTIFIER` | `$(APP_BUNDLE_IDENTIFIER).Intent` | +| App Clip | `APPCLIP_BUNDLE_IDENTIFIER` | `$(APP_BUNDLE_IDENTIFIER).Clip` | +| Watch App | `WATCH_BUNDLE_IDENTIFIER` | `$(APP_BUNDLE_IDENTIFIER).watchkitapp` | +| Notification Extension | `NOTIFICATION_BUNDLE_IDENTIFIER` | `$(APP_BUNDLE_IDENTIFIER).NotificationExtension` | +| Share Extension | `SHARE_BUNDLE_IDENTIFIER` | `$(APP_BUNDLE_IDENTIFIER).ShareExtension` | + +#### Checklist for New Targets + +- [ ] Add bundle ID variable to `Base.xcconfig` +- [ ] Set `baseConfigurationReference` to Debug/Release xcconfig +- [ ] Use `$(VARIABLE)` for `PRODUCT_BUNDLE_IDENTIFIER` +- [ ] Use `$(DEVELOPMENT_TEAM)` for team +- [ ] Create entitlements with `$(APP_GROUP_IDENTIFIER)` if sharing data +- [ ] Add to `AppIdentifiers.swift` if Swift code needs the identifier +- [ ] Register App ID in Apple Developer Portal (uses same App Group) + +### Migration + +To migrate to a new developer account, edit **one file** (`Base.xcconfig`): + +``` +COMPANY_IDENTIFIER = com.newcompany +DEVELOPMENT_TEAM = NEW_TEAM_ID +``` + +Then clean build (⇧⌘K) and rebuild. Everything updates automatically—including all extension targets. -## Dynamic Type instructions +## 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: - ```swift - 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: - ```swift - // 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. +- Always support Dynamic Type for accessibility. +- Use `@ScaledMetric` to scale custom dimensions. +- Choose appropriate `relativeTo` text styles based on semantic purpose. +- For constrained UI elements, you may use fixed sizes but document the reason. +- Prefer system text styles: `.font(.body)`, `.font(.title)`, `.font(.caption)`. -## VoiceOver accessibility instructions +## 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: - ```swift - 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: - ```swift - TableBackgroundView() - .accessibilityHidden(true) // Decorative element - ``` -- Group related elements to reduce VoiceOver navigation complexity: - ```swift - 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: - ```swift - 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: - ```swift - enum Suit { - var accessibilityName: String { - switch self { - case .hearts: return String(localized: "Hearts") - // ... - } - } - } - ``` -- Test with VoiceOver enabled: Settings > Accessibility > VoiceOver. +- All interactive elements must have meaningful `.accessibilityLabel()`. +- Use `.accessibilityValue()` for dynamic state. +- Use `.accessibilityHint()` to describe what happens on interaction. +- Use `.accessibilityAddTraits()` for element type. +- Hide decorative elements with `.accessibilityHidden(true)`. +- Group related elements to reduce navigation complexity. +- Post accessibility announcements for important events. -## Project structure +## 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. +- Use a consistent project structure organized by feature. +- Follow strict naming conventions for types, properties, and methods. +- **One public type per file**—break types into separate files. - 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. +- Add code comments and documentation as needed. +- Never include secrets or API keys in the repository. -## Documentation instructions +## 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. +- **Keep `README.md` files up to date** when adding new functionality. +- In multi-project workspaces, update the relevant project's `README.md`. +- Document new features, settings, or mechanics in the appropriate README. +- Update documentation when modifying existing behavior. +- Include configuration options and special interactions. +- README updates should be part of the same commit as the feature. -## PR instructions +## 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. +- If installed, ensure SwiftLint returns no warnings or errors. +- Verify that documentation reflects any new functionality. +- Check for duplicate code before submitting. +- Ensure all new files follow the one-type-per-file rule. diff --git a/BRANDING_IMPLEMENTATION_GUIDE.md b/BRANDING_IMPLEMENTATION_GUIDE.md new file mode 100644 index 0000000..daf64f9 --- /dev/null +++ b/BRANDING_IMPLEMENTATION_GUIDE.md @@ -0,0 +1,694 @@ +# Branding Implementation Guide + +A step-by-step guide to implementing the CasinoKit branding system (app icon and launch screen) in your casino game app. + +## Table of Contents + +1. [Overview](#overview) +2. [Prerequisites](#prerequisites) +3. [Step 1: Copy Branding Files](#step-1-copy-branding-files) +4. [Step 2: Create BrandingConfig.swift](#step-2-create-brandingconfigswift) +5. [Step 3: Add Launch Screen to App Entry Point](#step-3-add-launch-screen-to-app-entry-point) +6. [Step 4: Add Branding Tools to Settings (Optional)](#step-4-add-branding-tools-to-settings-optional) +7. [Step 5: Generate Your App Icon](#step-5-generate-your-app-icon) +8. [Step 6: Add Icon to Xcode Assets](#step-6-add-icon-to-xcode-assets) +9. [Complete Example](#complete-example) +10. [Troubleshooting](#troubleshooting) + +--- + +## Overview + +The CasinoKit branding system provides: + +- **AppIconView**: A customizable icon design with gradient backgrounds, SF Symbols, and text +- **LaunchScreenView**: An animated launch screen that matches your icon +- **AppLaunchView**: A wrapper that seamlessly transitions from launch to app +- **IconGeneratorView**: A development tool to generate and export icon images +- **BrandingPreviewView**: A preview tool to see icons and launch screens side-by-side + +**Key Files in CasinoKit:** +- `AppIconView.swift` - Icon design view +- `LaunchScreenView.swift` - Launch screen design view +- `AppLaunchView.swift` - Launch wrapper with animation +- `IconGeneratorView.swift` - Icon export tool +- `IconRenderer.swift` - Rendering utilities +- `BrandingPreviewView.swift` - Preview tool + +--- + +## Prerequisites + +### If Using CasinoKit Package + +Your app must have `CasinoKit` as a dependency. No additional files needed—everything is available through `import CasinoKit`. + +### If You Copied the Branding Folder + +If you manually copied the branding views from CasinoKit into your app, ensure you have: + +1. All 6 files from `CasinoKit/Sources/CasinoKit/Views/Branding/`: + - `AppIconView.swift` + - `LaunchScreenView.swift` + - `AppLaunchView.swift` + - `IconGeneratorView.swift` + - `IconRenderer.swift` + - `BrandingPreviewView.swift` + +2. Dependencies these files require: + - `DiamondPatternView.swift` (used by `AppIconView`) + - `DebugSection.swift` (if using debug tools) + +--- + +## Step 1: Copy Branding Files + +**Skip this step if using CasinoKit as a package dependency.** + +If copying manually, create a `Theme/` folder in your app target and copy all branding files: + +``` +YourApp/ + YourApp/ + Theme/ + AppIconView.swift + LaunchScreenView.swift + AppLaunchView.swift + IconGeneratorView.swift + IconRenderer.swift + BrandingPreviewView.swift + BrandingConfig.swift ← You'll create this in Step 2 +``` + +**Important:** Ensure you also have `DiamondPatternView.swift` if `AppIconView` references it, or copy that pattern code inline. + +--- + +## Step 2: Create BrandingConfig.swift + +Create a new Swift file in your app's `Theme/` folder called `BrandingConfig.swift`. This file defines your game's branding. + +### Template + +```swift +// +// BrandingConfig.swift +// YourGame +// +// App-specific branding configurations for icons and launch screens. +// + +import SwiftUI +import CasinoKit // Remove this line if not using CasinoKit package + +// MARK: - App Icon Configuration + +extension AppIconConfig { + /// YourGame app icon configuration. + static let yourGame = AppIconConfig( + title: "YOUR GAME", // App name (uppercase looks best) + subtitle: "21", // Optional: number or short text below icon + iconSymbol: "suit.club.fill", // SF Symbol for the main icon + primaryColor: Color(red: 0.1, green: 0.2, blue: 0.35), // Top gradient color + secondaryColor: Color(red: 0.05, green: 0.12, blue: 0.25), // Bottom gradient color + accentColor: .yellow // Color for icon and text + ) +} + +// MARK: - Launch Screen Configuration + +extension LaunchScreenConfig { + /// YourGame launch screen configuration. + static let yourGame = LaunchScreenConfig( + title: "YOUR GAME", // App name (uppercase looks best) + subtitle: "21", // Optional: appears below icon symbols + tagline: "Beat the Dealer", // Optional: tagline at bottom + iconSymbols: [ // 1-3 SF Symbols for the launch logo + "suit.club.fill", + "suit.diamond.fill" + ], + primaryColor: Color(red: 0.1, green: 0.2, blue: 0.35), // Top gradient color + secondaryColor: Color(red: 0.05, green: 0.12, blue: 0.25), // Bottom gradient color + accentColor: .yellow // Color for text and accents + ) +} +``` + +### Customization Tips + +**Title:** +- Use uppercase for a casino aesthetic +- Keep it concise (6-9 characters max) +- The view automatically scales longer titles + +**Subtitle:** +- Optional, best for short numbers or text ("21", "DELUXE", etc.) +- Appears larger and more prominent than the title + +**Icon Symbol:** +- Use SF Symbols names (e.g., `"suit.spade.fill"`, `"diamond.fill"`) +- Browse available symbols in Xcode: Editor → Insert SF Symbol +- Card suits work great: `suit.spade.fill`, `suit.heart.fill`, `suit.diamond.fill`, `suit.club.fill` + +**Colors:** +- Use `Color(red:green:blue:)` with values 0.0-1.0 +- Primary color = top of gradient +- Secondary color = bottom of gradient (usually darker) +- Accent color = icon symbol and text highlights + +**Launch Screen Icon Symbols:** +- Use 1-3 symbols for visual variety +- Hearts and diamonds automatically render red +- Spades and clubs render white + +### Real Examples + +**Blackjack:** +```swift +extension AppIconConfig { + static let blackjack = AppIconConfig( + title: "BLACKJACK", + subtitle: "21", + iconSymbol: "suit.club.fill", + primaryColor: Color(red: 0.05, green: 0.35, blue: 0.15), // Green felt + secondaryColor: Color(red: 0.03, green: 0.2, blue: 0.1), + accentColor: .yellow + ) +} + +extension LaunchScreenConfig { + static let blackjack = LaunchScreenConfig( + title: "BLACKJACK", + subtitle: "21", + tagline: "Beat the Dealer", + iconSymbols: ["suit.club.fill", "suit.diamond.fill"], + primaryColor: Color(red: 0.05, green: 0.35, blue: 0.15), + secondaryColor: Color(red: 0.03, green: 0.2, blue: 0.1), + accentColor: .yellow + ) +} +``` + +**Baccarat:** +```swift +extension AppIconConfig { + static let baccarat = AppIconConfig( + title: "BACCARAT", + iconSymbol: "suit.spade.fill", + primaryColor: Color(red: 0.1, green: 0.2, blue: 0.35), // Blue elegant + secondaryColor: Color(red: 0.05, green: 0.12, blue: 0.25) + ) +} + +extension LaunchScreenConfig { + static let baccarat = LaunchScreenConfig( + title: "BACCARAT", + tagline: "The Classic Casino Card Game", + iconSymbols: ["suit.spade.fill", "suit.heart.fill"] + ) +} +``` + +--- + +## Step 3: Add Launch Screen to App Entry Point + +Update your `@main` App struct to wrap your content with `AppLaunchView`. + +### Before + +```swift +@main +struct YourGameApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} +``` + +### After + +```swift +import SwiftUI +import CasinoKit // If using the package + +@main +struct YourGameApp: App { + var body: some Scene { + WindowGroup { + AppLaunchView(config: .yourGame) { + ContentView() + } + } + } +} +``` + +**What this does:** +- Shows an animated launch screen for ~2 seconds +- Fades smoothly into your main content +- Creates a polished, professional app opening experience + +**Note:** Replace `.yourGame` with the static property name you defined in `BrandingConfig.swift` (e.g., `.blackjack`, `.poker`, `.roulette`). + +--- + +## Step 4: Add Branding Tools to Settings (Optional) + +Add debug tools to your settings view so you can easily generate and preview icons during development. + +### If Using CasinoKit Package + +Add this to your settings view inside a `#if DEBUG` block: + +```swift +#if DEBUG +SheetSection(title: "DEBUG", icon: "ant.fill") { + BrandingDebugRows( + iconConfig: .yourGame, + launchConfig: .yourGame, + appName: "YourGame" + ) +} +#endif +``` + +### Full Example in Settings + +```swift +import SwiftUI +import CasinoKit + +struct SettingsView: View { + @Environment(\.dismiss) private var dismiss + + var body: some View { + SheetContainerView( + title: "Settings", + content: { + // ... your normal settings sections ... + + // DEBUG section at the bottom + #if DEBUG + SheetSection(title: "DEBUG", icon: "ant.fill") { + BrandingDebugRows( + iconConfig: .yourGame, + launchConfig: .yourGame, + appName: "YourGame" + ) + } + #endif + }, + onCancel: nil, + onDone: { + dismiss() + } + ) + } +} +``` + +**What this adds:** +- **Icon Generator** button: Generates and saves a 1024px PNG to the Files app +- **Branding Preview** button: Shows a live preview of your icon and launch screen + +**Important:** This only appears in DEBUG builds and will automatically be excluded from App Store releases. + +--- + +## Step 5: Generate Your App Icon + +Now it's time to create your actual app icon image. + +### Method 1: Using IconGeneratorView (Recommended) + +1. **Build and run your app** (simulator or device) +2. **Open Settings** in your app +3. **Scroll to the DEBUG section** (only visible in DEBUG builds) +4. **Tap "Icon Generator"** +5. **Tap "Generate & Save Icon"** +6. **Wait for confirmation**: "✅ Icon saved to Documents folder!" + +The icon is now saved as `AppIcon.png` (1024×1024) in your app's Documents folder. + +### Method 2: Using Xcode Previews + +1. Open `BrandingConfig.swift` in Xcode +2. Add a preview at the bottom: + +```swift +#Preview("App Icon") { + AppIconView(config: .yourGame, size: 512) + .clipShape(.rect(cornerRadius: 512 * 0.22)) + .padding() + .background(Color.gray) +} +``` + +3. Open the Canvas (Editor → Canvas) +4. Take a screenshot of the preview +5. Crop and scale to 1024×1024 in an image editor + +### Method 3: Programmatic Export + +Create a temporary view or function: + +```swift +@MainActor +func exportIcon() { + let config = AppIconConfig.yourGame + let view = AppIconView(config: config, size: 1024) + let renderer = ImageRenderer(content: view) + renderer.scale = 1.0 + + if let image = renderer.uiImage, + let data = image.pngData() { + let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + .appending(path: "AppIcon.png") + try? data.write(to: url) + print("Icon saved to: \(url.path)") + } +} +``` + +--- + +## Step 6: Add Icon to Xcode Assets + +### Retrieve the Icon from Device/Simulator + +**On Simulator:** +1. Open **Finder** +2. Go to: `~/Library/Developer/CoreSimulator/Devices/` +3. Find your simulator device folder +4. Navigate to: `data/Containers/Data/Application/[YourApp]/Documents/` +5. Copy `AppIcon.png` to your Mac desktop + +**On Physical Device:** +1. Open the **Files** app on your device +2. Navigate to: **On My iPhone** → **YourGame** +3. Find `AppIcon.png` +4. **AirDrop** or **share** it to your Mac + +**Alternative (Xcode):** +1. In Xcode, go to **Window** → **Devices and Simulators** +2. Select your device/simulator +3. Find your app in the Installed Apps list +4. Click the **⚙️ gear** icon → **Download Container** +5. Right-click the downloaded `.xcappdata` file → **Show Package Contents** +6. Navigate to `AppData/Documents/` and copy `AppIcon.png` + +### Add to Xcode Assets + +1. **Open your Xcode project** +2. **Navigate to** `Assets.xcassets` in the Project Navigator +3. **Click on AppIcon** (the app icon asset) +4. **Drag `AppIcon.png`** from Finder into the **1024×1024** slot (labeled "iOS App Store") +5. **Xcode automatically generates** all required sizes from this single image + +**Important:** Modern iOS projects only need the 1024×1024 image. Xcode generates all other sizes automatically. + +### Verify the Icon + +1. **Build and run** your app +2. **Press the Home button** (or swipe up) +3. Check that your new icon appears on the home screen +4. If it doesn't update immediately, **delete the app** and reinstall + +--- + +## Complete Example + +Here's a full example for a Poker game: + +### File: `Poker/Theme/BrandingConfig.swift` + +```swift +// +// BrandingConfig.swift +// Poker +// + +import SwiftUI +import CasinoKit + +extension AppIconConfig { + static let poker = AppIconConfig( + title: "POKER", + iconSymbol: "suit.diamond.fill", + primaryColor: Color(red: 0.2, green: 0.05, blue: 0.1), + secondaryColor: Color(red: 0.1, green: 0.02, blue: 0.05), + accentColor: Color(red: 1.0, green: 0.85, blue: 0.3) // Gold + ) +} + +extension LaunchScreenConfig { + static let poker = LaunchScreenConfig( + title: "POKER", + tagline: "All In", + iconSymbols: ["suit.diamond.fill", "suit.heart.fill", "suit.club.fill"], + primaryColor: Color(red: 0.2, green: 0.05, blue: 0.1), + secondaryColor: Color(red: 0.1, green: 0.02, blue: 0.05), + accentColor: Color(red: 1.0, green: 0.85, blue: 0.3) + ) +} +``` + +### File: `Poker/PokerApp.swift` + +```swift +import SwiftUI +import CasinoKit + +@main +struct PokerApp: App { + var body: some Scene { + WindowGroup { + AppLaunchView(config: .poker) { + ContentView() + } + } + } +} +``` + +### File: `Poker/Views/SettingsView.swift` + +```swift +import SwiftUI +import CasinoKit + +struct SettingsView: View { + @Environment(\.dismiss) private var dismiss + + var body: some View { + SheetContainerView( + title: "Settings", + content: { + // Game settings sections... + + SheetSection(title: "SOUND", icon: "speaker.wave.2.fill") { + // Sound settings... + } + + #if DEBUG + SheetSection(title: "DEBUG", icon: "ant.fill") { + BrandingDebugRows( + iconConfig: .poker, + launchConfig: .poker, + appName: "Poker" + ) + } + #endif + }, + onDone: { + dismiss() + } + ) + } +} +``` + +--- + +## Troubleshooting + +### Issue: Can't find `AppIconView` or other branding types + +**Solution:** +- If using CasinoKit package: Ensure `import CasinoKit` is at the top of your file +- If copying manually: Ensure all 6 branding files are in your app target +- Check that files are added to your target's "Compile Sources" in Build Phases + +### Issue: Launch screen doesn't appear + +**Solution:** +- Verify `AppLaunchView` wraps your content in the App struct +- Check that you're using the correct config name (e.g., `.poker` not `.example`) +- Ensure the config extension is defined in `BrandingConfig.swift` + +### Issue: Icon looks cut off at the edges + +**Explanation:** iOS applies a superellipse mask to all app icons. The branding system accounts for this, but if you see clipping: + +**Solution:** +- Don't add rounded corners yourself—iOS does this automatically +- Ensure decorative borders are inset from edges +- The system is designed to work with iOS's mask; trust the 22% corner radius preview + +### Issue: "Icon saved" message appears but can't find the file + +**Solution:** +- Open the **Files** app on your device/simulator +- Navigate to: **Browse** → **On My iPhone/iPad** → **[Your App Name]** +- If folder doesn't exist, try granting Files access in Settings + +**Alternative:** Use Xcode's Devices and Simulators window to download the app container (see Step 6). + +### Issue: Icon doesn't update after adding to Assets + +**Solution:** +1. **Clean build folder**: Product → Clean Build Folder (Cmd+Shift+K) +2. **Delete app** from device/simulator +3. **Rebuild and reinstall** +4. If still not working, check that the 1024×1024 slot is filled in Assets.xcassets + +### Issue: `BrandingDebugRows` not showing in settings + +**Solution:** +- Ensure you're running a DEBUG build (not Release) +- Wrap the section in `#if DEBUG ... #endif` +- Verify `import CasinoKit` if using the package +- Check that your settings view is inside a `NavigationStack` (required for navigation rows) + +### Issue: Colors don't match between icon and launch screen + +**Solution:** +- Ensure both configs use identical color values +- Copy-paste color definitions to avoid typos +- Consider extracting colors to a shared constant: + +```swift +extension Color { + static let pokerPrimary = Color(red: 0.2, green: 0.05, blue: 0.1) + static let pokerSecondary = Color(red: 0.1, green: 0.02, blue: 0.05) +} + +extension AppIconConfig { + static let poker = AppIconConfig( + title: "POKER", + iconSymbol: "suit.diamond.fill", + primaryColor: .pokerPrimary, + secondaryColor: .pokerSecondary + ) +} +``` + +### Issue: Title text is too small or too large + +**Solution:** The system automatically scales titles based on length: +- **6 characters or less**: Full size (100%) +- **7 characters**: 95% scale +- **8 characters**: 85% scale +- **9 characters**: 75% scale +- **10+ characters**: 65% scale + +If your title is very long, consider: +- Using an abbreviation in the icon +- Using subtitle for additional text +- Manually adjusting the `titleSize` calculation in `AppIconView.swift` + +--- + +## Additional Resources + +### Color Palette Ideas + +**Classic Casino:** +```swift +primaryColor: Color(red: 0.1, green: 0.2, blue: 0.35) // Deep blue +secondaryColor: Color(red: 0.05, green: 0.12, blue: 0.25) +accentColor: .yellow +``` + +**Luxury Gold:** +```swift +primaryColor: Color(red: 0.2, green: 0.15, blue: 0.05) // Deep gold +secondaryColor: Color(red: 0.1, green: 0.08, blue: 0.02) +accentColor: Color(red: 1.0, green: 0.85, blue: 0.3) // Bright gold +``` + +**Green Felt:** +```swift +primaryColor: Color(red: 0.05, green: 0.35, blue: 0.15) // Casino green +secondaryColor: Color(red: 0.03, green: 0.2, blue: 0.1) +accentColor: .yellow +``` + +**Elegant Red:** +```swift +primaryColor: Color(red: 0.3, green: 0.05, blue: 0.1) // Deep red +secondaryColor: Color(red: 0.15, green: 0.02, blue: 0.05) +accentColor: Color(red: 1.0, green: 0.85, blue: 0.3) +``` + +### SF Symbol Recommendations + +**Card Suits:** +- `suit.spade.fill` - Classic, elegant +- `suit.heart.fill` - Warm, friendly +- `suit.diamond.fill` - Luxurious +- `suit.club.fill` - Traditional + +**Other Casino Icons:** +- `diamond.fill` - Luxury, wealth +- `star.fill` - Premium, winning +- `crown.fill` - VIP, royalty +- `dollarsign.circle.fill` - Money, stakes + +### Launch Screen Animation Timing + +The default timing is: +- **Logo fade-in**: 0.6 seconds +- **Tagline fade-in**: 0.6 seconds (delayed by 0.3s) +- **Total display**: 2.0 seconds +- **Fade-out**: 0.5 seconds + +To customize, modify `AppLaunchView.swift`: + +```swift +.task { + try? await Task.sleep(for: .seconds(3.0)) // Change display duration + withAnimation(.easeOut(duration: 1.0)) { // Change fade-out duration + showLaunchScreen = false + } +} +``` + +--- + +## Summary Checklist + +- [ ] Copy branding files from CasinoKit or ensure package dependency is set up +- [ ] Create `BrandingConfig.swift` with your game's configurations +- [ ] Add `AppLaunchView` wrapper to your App entry point +- [ ] (Optional) Add `BrandingDebugRows` to your settings view +- [ ] Build and run app in DEBUG mode +- [ ] Generate icon using Icon Generator tool +- [ ] Retrieve icon PNG from device/simulator +- [ ] Add 1024×1024 PNG to Assets.xcassets AppIcon slot +- [ ] Clean build and reinstall to verify icon appears +- [ ] Test launch screen animation on device + +--- + +**Need Help?** + +- Check the `BrandingPreviewView` to see your icon and launch screen side-by-side +- Use Xcode previews to iterate on colors and layout quickly +- Test on multiple device sizes to ensure text scales properly +- Remember: DEBUG tools are automatically excluded from Release builds + +**Happy Branding! 🎰✨** diff --git a/SETTINGS_STYLING_GUIDE.md b/SETTINGS_STYLING_GUIDE.md new file mode 100644 index 0000000..2d13398 --- /dev/null +++ b/SETTINGS_STYLING_GUIDE.md @@ -0,0 +1,957 @@ +# Settings View Styling Guide + +A comprehensive guide to achieving the polished casino-style settings interface with golden accents, proper section separation, and consistent styling. + +## Table of Contents + +1. [Overview](#overview) +2. [The Complete Stack](#the-complete-stack) +3. [Step 1: Set Up Design Constants](#step-1-set-up-design-constants) +4. [Step 2: Use SheetContainerView](#step-2-use-sheetcontainerview) +5. [Step 3: Structure with SheetSection](#step-3-structure-with-sheetsection) +6. [Step 4: Use Proper Settings Components](#step-4-use-proper-settings-components) +7. [Step 5: Apply Consistent Colors](#step-5-apply-consistent-colors) +8. [Complete Example](#complete-example) +9. [Color Reference](#color-reference) +10. [Troubleshooting](#troubleshooting) + +--- + +## Overview + +The polished casino settings interface you see in Blackjack and Baccarat is achieved through a combination of: + +1. **SheetContainerView** - Dark background, proper navigation bar styling +2. **SheetSection** - Icon + title headers with card-like content containers +3. **CasinoKit components** - Pre-styled toggles, pickers, and selectable rows +4. **Color.Sheet constants** - Consistent golden accent and background colors +5. **Design constants** - Standardized spacing, corner radius, and opacity values + +### Key Styling Elements + +- **Golden accent color**: `Color.Sheet.accent = Color(red: 0.9, green: 0.75, blue: 0.3)` +- **Dark blue background**: `Color.Sheet.background = Color(red: 0.08, green: 0.12, blue: 0.18)` +- **Section cards**: White opacity fill with rounded corners +- **Radio buttons**: Golden checkmark circles for selected items +- **Proper spacing**: xxLarge (24pt) between sections + +--- + +## The Complete Stack + +### Required Components from CasinoKit + +```swift +import CasinoKit +``` + +**Views:** +- `SheetContainerView` - Outer container with navigation and dark background +- `SheetSection` - Section container with icon/title header and card +- `SelectableRow` - Radio button rows for pickers +- `SelectionIndicator` - Golden checkmark circles +- `SettingsToggle` - Toggle switches with titles and subtitles +- `SpeedPicker`, `VolumePicker`, `BalancePicker` - Specialized pickers +- `BadgePill` - Badge for displaying values like "$10 - $1,000" + +**Constants:** +- `CasinoDesign` - Spacing, corner radius, opacity, font sizes, etc. +- `Color.Sheet` - Sheet-specific colors (background, accent, etc.) + +### Local Design Constants Setup + +Create a `DesignConstants.swift` file in your app that imports and typealias CasinoKit constants: + +```swift +import SwiftUI +import CasinoKit + +enum Design { + // Import shared constants via typealias + typealias Spacing = CasinoDesign.Spacing + typealias CornerRadius = CasinoDesign.CornerRadius + typealias LineWidth = CasinoDesign.LineWidth + typealias Shadow = CasinoDesign.Shadow + typealias Opacity = CasinoDesign.Opacity + typealias Animation = CasinoDesign.Animation + typealias BaseFontSize = CasinoDesign.BaseFontSize + typealias IconSize = CasinoDesign.IconSize + + // Your game-specific constants + enum Size { + static let cardWidth: CGFloat = 90 + // ... other game-specific sizes + } +} +``` + +This allows you to write `Design.Spacing.large` instead of `CasinoDesign.Spacing.large` throughout your app. + +--- + +## Step 1: Set Up Design Constants + +### 1.1 Create DesignConstants.swift + +Create a file called `DesignConstants.swift` in your app's `Theme/` folder: + +```swift +// +// DesignConstants.swift +// YourGame +// + +import SwiftUI +import CasinoKit + +enum Design { + // MARK: - Shared Constants (from CasinoKit) + + typealias Spacing = CasinoDesign.Spacing + typealias CornerRadius = CasinoDesign.CornerRadius + typealias LineWidth = CasinoDesign.LineWidth + typealias Shadow = CasinoDesign.Shadow + typealias Opacity = CasinoDesign.Opacity + typealias Animation = CasinoDesign.Animation + typealias Scale = CasinoDesign.Scale + typealias MinScaleFactor = CasinoDesign.MinScaleFactor + typealias BaseFontSize = CasinoDesign.BaseFontSize + typealias IconSize = CasinoDesign.IconSize + + // MARK: - Game-Specific Sizes + + enum Size { + // Add your game-specific sizes here + static let cardWidth: CGFloat = 90 + } +} + +// MARK: - App Colors + +extension Color { + // Import CasinoKit table colors + typealias Table = CasinoTable + + // Add game-specific colors here +} +``` + +### 1.2 Why This Matters + +By typealiasing `CasinoDesign`, you get: +- Consistent spacing throughout your app +- Proper opacity values for layering +- Standardized corner radii +- Matching font sizes + +**Don't hardcode values!** Use constants instead: + +❌ **Bad:** +```swift +.padding(24) +.opacity(0.1) +.cornerRadius(12) +``` + +✅ **Good:** +```swift +.padding(Design.Spacing.xxLarge) +.opacity(Design.Opacity.subtle) +.clipShape(.rect(cornerRadius: Design.CornerRadius.large)) +``` + +--- + +## Step 2: Use SheetContainerView + +### 2.1 Basic Structure + +`SheetContainerView` provides the foundation: dark background, navigation bar, and done button. + +```swift +import SwiftUI +import CasinoKit + +struct SettingsView: View { + @Environment(\.dismiss) private var dismiss + + var body: some View { + SheetContainerView( + title: "Settings", + content: { + // Your sections go here + }, + onCancel: nil, // Optional cancel button + onDone: { + dismiss() + }, + doneButtonText: "Done" + ) + } +} +``` + +### 2.2 What SheetContainerView Provides + +**Automatic Styling:** +- Dark blue background: `Color.Sheet.background` +- NavigationStack with inline title +- Golden "Done" button: `Color.Sheet.accent` +- Optional "Cancel" button +- Toolbar with proper dark scheme +- ScrollView with proper vertical spacing + +**Key Parameters:** +- `title` - The navigation bar title +- `content` - A `@ViewBuilder` closure for your sections +- `onCancel` - Optional cancel action (if nil, no cancel button) +- `onDone` - Done button action +- `doneButtonText` - Text for done button (default: "Done") +- `cancelButtonText` - Text for cancel button (default: "Cancel") + +### 2.3 Section Spacing + +SheetContainerView automatically adds `xxLarge` (24pt) spacing between sections: + +```swift +VStack(spacing: CasinoDesign.Spacing.xxLarge) { + content // Your sections +} +``` + +This creates the clean separation you see between sections. + +--- + +## Step 3: Structure with SheetSection + +### 3.1 Basic SheetSection + +Each logical group of settings should be in a `SheetSection`: + +```swift +SheetSection(title: "DISPLAY", icon: "eye") { + // Settings content here +} +``` + +### 3.2 What SheetSection Provides + +**Header:** +- Icon in golden color with opacity +- Title in uppercase, bold, rounded font +- Subtle spacing and padding + +**Content Card:** +- White opacity background (`Color.Sheet.sectionFill`) +- Rounded corners (`CornerRadius.large`) +- Proper padding +- Horizontal margins + +**Visual Structure:** +``` +┌─────────────────────────────────────┐ +│ 👁 DISPLAY │ ← Header (icon + title) +│ ┌─────────────────────────────────┐ │ +│ │ │ │ ← Content card +│ │ [Settings content here] │ │ (white opacity fill) +│ │ │ │ +│ └─────────────────────────────────┘ │ +└─────────────────────────────────────┘ +``` + +### 3.3 Section Best Practices + +**Use semantic grouping:** +- GAME STYLE - Game variants and modes +- RULES - Rule customization +- TABLE LIMITS - Betting limits +- DECK SETTINGS - Shoe configuration +- STARTING BALANCE - Balance picker +- DISPLAY - Visual settings +- SOUND - Audio settings +- DATA - Reset and export + +**Choose appropriate icons:** +```swift +SheetSection(title: "GAME STYLE", icon: "suit.club.fill") +SheetSection(title: "RULES", icon: "list.bullet.clipboard") +SheetSection(title: "TABLE LIMITS", icon: "banknote") +SheetSection(title: "DECK SETTINGS", icon: "rectangle.portrait.on.rectangle.portrait") +SheetSection(title: "STARTING BALANCE", icon: "dollarsign.circle") +SheetSection(title: "DISPLAY", icon: "eye") +SheetSection(title: "SOUND", icon: "speaker.wave.2.fill") +SheetSection(title: "DATA", icon: "externaldrive") +``` + +--- + +## Step 4: Use Proper Settings Components + +### 4.1 SelectableRow (Radio Button Rows) + +Use `SelectableRow` for picker options with radio buttons: + +```swift +SheetSection(title: "DECK SETTINGS", icon: "rectangle.portrait.on.rectangle.portrait") { + VStack(spacing: Design.Spacing.small) { + SelectableRow( + title: "1 Deck", + subtitle: "Single deck, higher variance", + isSelected: deckCount == 1, + accentColor: Color.Sheet.accent, + action: { deckCount = 1 } + ) + + SelectableRow( + title: "6 Decks", + subtitle: "Standard casino shoe", + isSelected: deckCount == 6, + accentColor: Color.Sheet.accent, + action: { deckCount = 6 } + ) + } +} +``` + +**What SelectableRow Provides:** +- Title in white, large font +- Subtitle in white with medium opacity +- Optional badge (e.g., "$10 - $1,000") +- Golden checkmark circle when selected +- Outlined circle when not selected +- Golden border when selected +- Subtle golden background fill when selected +- Proper padding and rounded corners + +**With Badge:** +```swift +SelectableRow( + title: "Low Stakes", + subtitle: "Standard mini table", + isSelected: true, + accentColor: Color.Sheet.accent, + badge: { + BadgePill(text: "$10 - $1,000", isSelected: true) + }, + action: { } +) +``` + +### 4.2 SettingsToggle (Toggle Switches) + +Use `SettingsToggle` for on/off options: + +```swift +SheetSection(title: "DISPLAY", icon: "eye") { + VStack(spacing: Design.Spacing.small) { + SettingsToggle( + title: "Show Animations", + subtitle: "Card dealing animations", + isOn: $showAnimations, + accentColor: Color.Sheet.accent + ) + + Divider().background(Color.white.opacity(Design.Opacity.hint)) + + SettingsToggle( + title: "Show Hints", + subtitle: "Basic strategy suggestions", + isOn: $showHints, + accentColor: Color.Sheet.accent + ) + } +} +``` + +**What SettingsToggle Provides:** +- Title in large font, white +- Subtitle in body font, white with medium opacity +- Toggle switch with golden accent color +- Proper vertical alignment +- Minimum touch target height (44pt) + +### 4.3 Specialized Pickers + +**SpeedPicker:** +```swift +SpeedPicker(speed: $dealingSpeed, accentColor: Color.Sheet.accent) +``` + +**VolumePicker:** +```swift +VolumePicker(volume: $soundVolume, accentColor: Color.Sheet.accent) +``` + +**BalancePicker:** +```swift +BalancePicker(balance: $startingBalance, accentColor: Color.Sheet.accent) +``` + +### 4.4 Dividers Between Items + +Use dividers to separate items within a section: + +```swift +VStack(spacing: Design.Spacing.small) { + SettingsToggle(title: "Option 1", isOn: $option1, accentColor: accent) + + Divider() + .background(Color.white.opacity(Design.Opacity.hint)) + + SettingsToggle(title: "Option 2", isOn: $option2, accentColor: accent) +} +``` + +**Divider opacity levels:** +- `Design.Opacity.hint` (0.2) - Very subtle, recommended +- `Design.Opacity.subtle` (0.1) - Almost invisible +- `Design.Opacity.light` (0.3) - More prominent + +--- + +## Step 5: Apply Consistent Colors + +### 5.1 Use Color.Sheet Constants + +**Always use `Color.Sheet.accent` for highlights:** + +```swift +private let accent = Color.Sheet.accent + +// Then use it everywhere: +SelectableRow(..., accentColor: accent) +SettingsToggle(..., accentColor: accent) +SpeedPicker(..., accentColor: accent) +``` + +### 5.2 Color Constants Available + +**From CasinoKit:** +```swift +Color.Sheet.background // Dark blue: Color(red: 0.08, green: 0.12, blue: 0.18) +Color.Sheet.accent // Gold: Color(red: 0.9, green: 0.75, blue: 0.3) +Color.Sheet.sectionFill // White.opacity(0.1) +Color.Sheet.cardBackground // White.opacity(0.05) +Color.Sheet.secondaryText // White.opacity(0.6) +Color.Sheet.cancelText // White.opacity(0.7) +``` + +### 5.3 Text Colors + +**Primary text (titles):** +```swift +.foregroundStyle(.white) +``` + +**Secondary text (subtitles, descriptions):** +```swift +.foregroundStyle(.white.opacity(Design.Opacity.medium)) +// or +.foregroundStyle(Color.Sheet.secondaryText) +``` + +**Accent highlights:** +```swift +.foregroundStyle(Color.Sheet.accent) +``` + +### 5.4 Background Colors + +**Section card fill:** +```swift +.background( + RoundedRectangle(cornerRadius: Design.CornerRadius.large) + .fill(Color.Sheet.sectionFill) +) +``` + +**Selection highlight:** +```swift +.background( + RoundedRectangle(cornerRadius: Design.CornerRadius.medium) + .fill(isSelected ? accent.opacity(Design.Opacity.subtle) : .clear) +) +``` + +--- + +## Complete Example + +Here's a complete settings view demonstrating all the concepts: + +```swift +// +// SettingsView.swift +// YourGame +// + +import SwiftUI +import CasinoKit + +struct SettingsView: View { + @Bindable var settings: GameSettings + @Environment(\.dismiss) private var dismiss + + @State private var showClearDataAlert = false + + /// Accent color for settings components + private let accent = Color.Sheet.accent + + var body: some View { + SheetContainerView( + title: "Settings", + content: { + // 1. Table Limits + SheetSection(title: "TABLE LIMITS", icon: "banknote") { + TableLimitsPicker(selection: $settings.tableLimits) + } + + // 2. Deck Settings + SheetSection(title: "DECK SETTINGS", icon: "rectangle.portrait.on.rectangle.portrait") { + DeckCountPicker(selection: $settings.deckCount) + } + + // 3. Starting Balance + SheetSection(title: "STARTING BALANCE", icon: "dollarsign.circle") { + BalancePicker(balance: $settings.startingBalance, accentColor: accent) + } + + // 4. Display + SheetSection(title: "DISPLAY", icon: "eye") { + VStack(spacing: Design.Spacing.small) { + SettingsToggle( + title: "Show Animations", + subtitle: "Card dealing animations", + isOn: $settings.showAnimations, + accentColor: accent + ) + + if settings.showAnimations { + Divider() + .background(Color.white.opacity(Design.Opacity.hint)) + + SpeedPicker(speed: $settings.dealingSpeed, accentColor: accent) + } + + Divider() + .background(Color.white.opacity(Design.Opacity.hint)) + + SettingsToggle( + title: "Show Hints", + subtitle: "Strategy suggestions", + isOn: $settings.showHints, + accentColor: accent + ) + } + } + + // 5. Sound + SheetSection(title: "SOUND", icon: "speaker.wave.2.fill") { + VStack(spacing: Design.Spacing.small) { + SettingsToggle( + title: "Sound Effects", + subtitle: "Play game sounds", + isOn: $settings.soundEnabled, + accentColor: accent + ) + + if settings.soundEnabled { + Divider() + .background(Color.white.opacity(Design.Opacity.hint)) + + VolumePicker(volume: $settings.soundVolume, accentColor: accent) + } + + Divider() + .background(Color.white.opacity(Design.Opacity.hint)) + + SettingsToggle( + title: "Haptics", + subtitle: "Vibration feedback", + isOn: $settings.hapticsEnabled, + accentColor: accent + ) + } + } + + // 6. Data + SheetSection(title: "DATA", icon: "externaldrive") { + Button { + showClearDataAlert = true + } label: { + HStack { + VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { + Text("Clear All Data") + .font(.system(size: Design.BaseFontSize.large, weight: .semibold)) + .foregroundStyle(.red) + + Text("Reset progress and statistics") + .font(.system(size: Design.BaseFontSize.body)) + .foregroundStyle(.white.opacity(Design.Opacity.medium)) + } + + Spacer() + + Image(systemName: "trash") + .foregroundStyle(.red) + } + .padding() + .background( + RoundedRectangle(cornerRadius: Design.CornerRadius.medium) + .fill(.clear) + ) + .overlay( + RoundedRectangle(cornerRadius: Design.CornerRadius.medium) + .strokeBorder(Color.white.opacity(Design.Opacity.subtle), lineWidth: Design.LineWidth.thin) + ) + } + .buttonStyle(.plain) + } + + // Version info + Text("YourGame v1.0 (1)") + .font(.system(size: Design.BaseFontSize.body)) + .foregroundStyle(.white.opacity(Design.Opacity.light)) + .frame(maxWidth: .infinity) + .padding(.top, Design.Spacing.large) + .padding(.bottom, Design.Spacing.medium) + }, + onCancel: nil, + onDone: { + settings.save() + dismiss() + } + ) + .alert("Clear All Data?", isPresented: $showClearDataAlert) { + Button("Cancel", role: .cancel) { } + Button("Clear", role: .destructive) { + // Clear data logic + } + } message: { + Text("This will delete all saved progress and statistics. This cannot be undone.") + } + } +} + +// MARK: - Custom Pickers + +struct TableLimitsPicker: View { + @Binding var selection: TableLimits + + var body: some View { + VStack(spacing: Design.Spacing.small) { + ForEach(TableLimits.allCases) { limit in + SelectableRow( + title: limit.displayName, + subtitle: limit.description, + isSelected: selection == limit, + accentColor: Color.Sheet.accent, + badge: { + BadgePill( + text: "$\(limit.minBet) - $\(limit.maxBet)", + isSelected: selection == limit + ) + }, + action: { selection = limit } + ) + } + } + } +} + +struct DeckCountPicker: View { + @Binding var selection: Int + + var body: some View { + VStack(spacing: Design.Spacing.small) { + ForEach([1, 2, 4, 6, 8], id: \.self) { count in + SelectableRow( + title: "\(count) Deck\(count == 1 ? "" : "s")", + subtitle: subtitleFor(count: count), + isSelected: selection == count, + accentColor: Color.Sheet.accent, + action: { selection = count } + ) + } + } + } + + private func subtitleFor(count: Int) -> String { + switch count { + case 1: return "Single deck, higher variance" + case 2: return "Lower house edge" + case 4: return "Common shoe game" + case 6: return "Standard casino" + case 8: return "Maximum penetration" + default: return "" + } + } +} +``` + +--- + +## Color Reference + +### Complete Color.Sheet Definitions + +From `CasinoKit/Sources/CasinoKit/Theme/CasinoDesign.swift`: + +```swift +public extension Color { + enum Sheet { + /// Dark background for sheets and popups. + public static let background = Color(red: 0.08, green: 0.12, blue: 0.18) + + /// Subtle fill for section cards. + public static let sectionFill = Color.white.opacity(CasinoDesign.Opacity.subtle) + + /// Card background in settings/sheets. + public static let cardBackground = Color.white.opacity(CasinoDesign.Opacity.verySubtle) + + /// Accent color for buttons and highlights (gold). + public static let accent = Color(red: 0.9, green: 0.75, blue: 0.3) + + /// Secondary text color. + public static let secondaryText = Color.white.opacity(CasinoDesign.Opacity.accent) + + /// Cancel button color. + public static let cancelText = Color.white.opacity(CasinoDesign.Opacity.strong) + } +} +``` + +### Opacity Reference + +```swift +public enum Opacity { + public static let verySubtle: Double = 0.05 // Barely visible backgrounds + public static let subtle: Double = 0.1 // Section fills + public static let selection: Double = 0.15 // Selection highlights + public static let hint: Double = 0.2 // Dividers, borders + public static let quarter: Double = 0.25 // Quarter opacity + public static let light: Double = 0.3 // Light text, borders + public static let overlay: Double = 0.4 // Modal overlays + public static let medium: Double = 0.5 // Subtitles, secondary info + public static let secondary: Double = 0.5 // Same as medium + public static let disabled: Double = 0.5 // Disabled controls + public static let accent: Double = 0.6 // Accent text + public static let strong: Double = 0.7 // Cancel buttons + public static let heavy: Double = 0.8 // Icons, strong text + public static let nearOpaque: Double = 0.85 // Almost solid + public static let almostFull: Double = 0.9 // Very solid +} +``` + +### Common Color Patterns + +**Selected row:** +```swift +.background( + RoundedRectangle(cornerRadius: Design.CornerRadius.medium) + .fill(accent.opacity(Design.Opacity.subtle)) // 0.1 +) +.overlay( + RoundedRectangle(cornerRadius: Design.CornerRadius.medium) + .strokeBorder(accent.opacity(Design.Opacity.medium), lineWidth: Design.LineWidth.thin) // 0.5 +) +``` + +**Unselected row:** +```swift +.background( + RoundedRectangle(cornerRadius: Design.CornerRadius.medium) + .fill(.clear) +) +.overlay( + RoundedRectangle(cornerRadius: Design.CornerRadius.medium) + .strokeBorder(Color.white.opacity(Design.Opacity.subtle), lineWidth: Design.LineWidth.thin) // 0.1 +) +``` + +--- + +## Troubleshooting + +### Issue: Colors don't look golden + +**Problem:** Accent colors appear white or wrong color. + +**Solution:** Ensure you're passing `Color.Sheet.accent` to the `accentColor` parameter: + +```swift +// ❌ Wrong +SelectableRow(..., accentColor: .yellow) + +// ✅ Correct +private let accent = Color.Sheet.accent +SelectableRow(..., accentColor: accent) +``` + +### Issue: Sections not separated properly + +**Problem:** Sections appear cramped together. + +**Solution:** Ensure you're using `SheetContainerView` which automatically adds `xxLarge` (24pt) spacing: + +```swift +SheetContainerView(title: "Settings", content: { + SheetSection(title: "SECTION 1", icon: "icon1") { ... } + SheetSection(title: "SECTION 2", icon: "icon2") { ... } + // Auto-spaced by xxLarge (24pt) +}) +``` + +### Issue: Section headers not styled properly + +**Problem:** Section headers missing icon, wrong font, or poor spacing. + +**Solution:** Use `SheetSection`, not custom VStack: + +```swift +// ❌ Wrong +VStack { + Text("DISPLAY") + // content +} + +// ✅ Correct +SheetSection(title: "DISPLAY", icon: "eye") { + // content +} +``` + +### Issue: Radio buttons not golden + +**Problem:** Selection indicators are white or blue circles. + +**Solution:** Pass `accentColor: Color.Sheet.accent` to `SelectableRow`: + +```swift +SelectableRow( + title: "Option", + subtitle: "Description", + isSelected: true, + accentColor: Color.Sheet.accent, // ← Must include this + action: { } +) +``` + +### Issue: Background is wrong color + +**Problem:** Background is white, gray, or wrong shade of blue. + +**Solution:** Use `SheetContainerView` which sets `Color.Sheet.background` automatically: + +```swift +// ❌ Wrong +var body: some View { + NavigationStack { + ScrollView { + // sections + } + .background(Color.gray) // Wrong! + } +} + +// ✅ Correct +var body: some View { + SheetContainerView(title: "Settings", content: { + // sections - background automatically applied + }) +} +``` + +### Issue: Dividers are too prominent or invisible + +**Problem:** Dividers within sections are too thick or can't be seen. + +**Solution:** Use white with `Design.Opacity.hint` (0.2): + +```swift +Divider() + .background(Color.white.opacity(Design.Opacity.hint)) +``` + +### Issue: Text not readable + +**Problem:** Text appears dark gray or hard to read. + +**Solution:** Always use `.foregroundStyle(.white)` for primary text: + +```swift +Text("Title") + .foregroundStyle(.white) + +Text("Subtitle") + .foregroundStyle(.white.opacity(Design.Opacity.medium)) +``` + +### Issue: Spacing inconsistent + +**Problem:** Some spacing is 10pt, some 12pt, some 15pt. + +**Solution:** Always use `Design.Spacing` constants: + +```swift +VStack(spacing: Design.Spacing.small) { // 8pt + // items +} + +.padding(Design.Spacing.medium) // 12pt +``` + +### Issue: Missing import + +**Problem:** `SheetContainerView`, `SelectableRow`, or other components not found. + +**Solution:** Add `import CasinoKit` to the top of your file: + +```swift +import SwiftUI +import CasinoKit // ← Required for CasinoKit components +``` + +### Issue: Design constants not available + +**Problem:** `Design.Spacing`, `Design.Opacity`, etc. not found. + +**Solution:** Create `DesignConstants.swift` with typealias to CasinoKit constants (see Step 1). + +--- + +## Quick Checklist + +Use this checklist to ensure your settings view has the proper styling: + +- [ ] Import CasinoKit at top of file +- [ ] Create local `DesignConstants.swift` with typealiases +- [ ] Wrap entire settings view in `SheetContainerView` +- [ ] Each logical group in a `SheetSection` with icon and title +- [ ] Use `SelectableRow` for radio button options +- [ ] Use `SettingsToggle` for on/off switches +- [ ] Pass `accentColor: Color.Sheet.accent` to all components +- [ ] Use `Design.Spacing.small` between items in a section +- [ ] Use `Divider().background(Color.white.opacity(Design.Opacity.hint))` between items +- [ ] Use `.foregroundStyle(.white)` for primary text +- [ ] Use `.foregroundStyle(.white.opacity(Design.Opacity.medium))` for secondary text +- [ ] No hardcoded spacing, opacity, or corner radius values +- [ ] Version info at bottom with subtle opacity + +--- + +## Summary + +The polished casino settings interface is achieved through: + +1. **SheetContainerView** for the dark background and navigation +2. **SheetSection** for icon headers and card-like content containers +3. **SelectableRow** for radio button pickers with golden checkmarks +4. **SettingsToggle** for toggle switches +5. **Color.Sheet.accent** for consistent golden highlights +6. **Design constants** for spacing, opacity, and font sizes +7. **Proper text colors** (white for primary, white with opacity for secondary) +8. **Subtle dividers** with 0.2 opacity between items + +Follow this guide and your settings will match the professional casino aesthetic! 🎰✨ diff --git a/WORKSPACE.md b/WORKSPACE.md new file mode 100644 index 0000000..01f6c4f --- /dev/null +++ b/WORKSPACE.md @@ -0,0 +1,34 @@ +# CasinoGames Workspace + +This is a multi-project workspace for casino card games. + + +## Projects + +| Project | Description | +|---------|-------------| +| `Blackjack/` | Blackjack card game app | +| `Baccarat/` | Baccarat card game app | +| `CasinoKit/` | Shared framework (protocols, views, audio, utilities) | + + +## Shared Code + +`CasinoKit` contains reusable components adopted by all games: + +- Card models and rendering +- Chip and betting UI components +- Audio playback service +- Design constants patterns +- Shared view components (settings, statistics, walkthroughs) + +When adding functionality that could be shared across games, consider adding it to `CasinoKit` first. + + +## Per-Project Documentation + +Each project has its own `README.md` with game-specific details: + +- `Blackjack/README.md` — Blackjack rules, settings, and architecture +- `Baccarat/README.md` — Baccarat rules, settings, and architecture +- `CasinoKit/README.md` — Shared framework API and components