True Count
Getting Started

Technical Overview

iOS frameworks, SpriteKit integration, GameplayKit state machine, and concurrency model for True Count.

True Count is a native iOS app built entirely with Apple frameworks. This page documents the technology stack, architecture decisions, and platform requirements for the development team.

At a Glance

SpecificationValue
PlatformiOS only (iPhone)
Minimum iOS VersioniOS 18
LanguageSwift 6.2
ArchitectureMVVM with @Observable
UI FrameworkSwiftUI + SpriteKit (hybrid)
Game CenterOptional for core gameplay, required for leaderboard features (GameKit)
PersistenceSwiftData + CloudKit
In-App PurchasesStoreKit 2

Platform and iOS Version

True Count targets iOS 18 as the minimum deployment target. iOS 18 combined with iOS 26 covers approximately 95% of active iPhones (per TelemetryDeck, January 2026), which is more than sufficient for a premium app.

Apple renamed iOS at WWDC 2025, jumping from iOS 18 directly to iOS 26 to align version numbers across all Apple platforms. There is no iOS 19 through iOS 25.

Starting April 28, 2026, all new App Store submissions must be built with the iOS 26 SDK (Xcode 26). True Count builds with the latest SDK and deploys down to iOS 18. No iPad or macOS targets are planned for the initial release.

Liquid Glass

iOS 26 introduces a new design language called Liquid Glass. Standard SwiftUI components receive Liquid Glass styling automatically when built with Xcode 26. For iOS 18 users, standard material design renders instead. Use if #available(iOS 26, *) only for Liquid Glass-specific customizations.

App Architecture

Design Principles

For the design principles behind these decisions (MVVM rationale, SOLID, dependency injection, code reuse), see Architecture Principles.

True Count uses MVVM with @Observable as its governing architecture pattern. The @Observable macro (iOS 17+) provides property-level tracking, meaning SwiftUI only re-renders views that read the specific property that changed. Combined with @MainActor isolation (automatic in Swift 6.2 app targets), ViewModels are thread-safe by default and testable in isolation.

Architecture Layers

truecount (App Target)
  SwiftUI Views ── menus, settings, stats, store, HUD overlays
  SpriteKit Scene ── card/chip animations, particle effects, table felt
  ViewModels ── GameViewModel, StoreVM, StatsVM, SettingsVM
  Service Layer ── GameCenterService, StoreKitService, HapticsService, AudioService
  Persistence ── SwiftData models with CloudKit sync

TrueCountEngine (Local Swift Package)
  Game Engine ── BlackjackRules, Deck/Hand/Card models, state machine
LayerTargetResponsibility
SwiftUI ViewsAppLayout, display, user interaction, accessibility
SpriteKit SceneAppCard and chip animations, particle effects, table visuals
ViewModelsAppOrchestrate game flow, bridge SwiftUI and SpriteKit
Game EngineTrueCountEnginePure blackjack rules with no UI dependencies (compiler-enforced)
Service LayerAppWrap Apple frameworks behind protocols for testability
PersistenceAppSwiftData models with CloudKit for cross-device sync

The Game Engine lives in a local Swift Package called TrueCountEngine. The package cannot import UIKit, SwiftUI, or any app-level framework. The app target imports TrueCountEngine as a dependency. See Architecture Principles for the design rationale.

Services are injected using SwiftUI's .environment() for @Observable classes that many views share (SoundManager, HapticEngine). Services requiring mock implementations for testing (GameCenterService, StoreKitService) use @Entry custom EnvironmentKeys with protocol abstractions.

Hybrid Rendering: SwiftUI + SpriteKit

True Count uses a hybrid rendering approach. SpriteKit handles the game table. SwiftUI handles everything around it.

SpriteKit handles the game table scene: card dealing, flipping, and sliding animations via SKAction sequences. Chip stacking with positional animations. Particle effects via SKEmitterNode for win celebrations and card sparkle. Table felt background with dynamic elements. SpriteKit's scene graph provides z-ordering and hit testing purpose-built for layered sprite management.

SwiftUI handles the app shell: main menu, settings, statistics (SwiftUI Charts), Game Center dashboard, store, and HUD overlays. SwiftUI provides native accessibility, Dynamic Type, Liquid Glass on iOS 26, and declarative layout for all non-game screens.

SpriteView bridges them. SpriteView (available since iOS 14) is Apple's official integration point for embedding an SKScene in a SwiftUI view hierarchy.

App Shell (SwiftUI)
  ├── Main Menu
  ├── Settings
  ├── Statistics (SwiftUI Charts)
  ├── Game Center Dashboard (GameKit)
  ├── Store (StoreKit 2)
  └── Game Table Screen
       └── SpriteView(scene: BlackjackTableScene)
            ├── Table felt background
            ├── Card nodes (deal/flip/slide SKActions)
            ├── Chip stack nodes
            ├── Particle emitters (sparkle, celebration)
            └── HUD overlay (SwiftUI via ZStack)

SwiftUI and SpriteKit Communication

The GameViewModel is a shared @Observable class referenced by both SwiftUI views and the SpriteKit scene. SwiftUI reads it through automatic @Observable tracking. The SpriteKit scene reads pending actions in its update(_:) loop. Both run on the main thread.

DirectionMechanism
SwiftUI to SpriteKitSet pendingAction on the ViewModel, then unpause the scene
SpriteKit to SwiftUIScene writes state updates (hand result, bankroll) to the ViewModel
Shared stateBoth read the same ViewModel properties (balance, hand cards, game phase)

Game action buttons (Hit, Stand, Double, Split) live in SwiftUI, overlaid on the SpriteKit scene via a ZStack. This provides automatic VoiceOver support, Dynamic Type, and Liquid Glass styling that SpriteKit nodes would not have.

Pause and unpause lifecycle: SpriteKit is paused during idle periods for battery savings (see Performance Notes). When the user taps an action button, the SwiftUI handler sets pendingAction on the ViewModel and unpauses the scene. The scene reads and clears the pending action on the next frame, executes the animation, and re-pauses when the animation completes. Action buttons are disabled during animations (bound to viewModel.isAnimating) to prevent conflicting inputs.

The BlackjackTableScene instance is retained in a @State property to prevent SwiftUI from recreating the scene during view redraws.

Apple Frameworks

Rendering and Animation

FrameworkPurpose
SpriteKitGame table rendering, card and chip animations, particle effects
SwiftUIApp shell, menus, settings, HUD overlays, accessibility

Game Logic

FrameworkPurpose
GameplayKitCard shuffling via GKMersenneTwisterRandomSource, game state machine via GKStateMachine

Audio and Haptics

FrameworkPurpose
AVFoundationSound effects (card snap, chip clink, win jingle). Audio session set to .ambient to mix with the player's music.
CoreHapticsCustom haptic patterns for gameplay events. Engine kept alive with isAutoShutdownEnabled = true to avoid per-event creation latency.

Social and Monetization

FrameworkPurpose
GameKitGame Center for leaderboards, achievements, and player identity. No multiplayer in the initial release.
StoreKit 2In-app purchases for credit packs. Async/await API with Product.products(for:) and Transaction.updates.

Data and Sync

FrameworkPurpose
SwiftDataLocal persistence for game history, statistics, and settings
CloudKit (via SwiftData)Cross-device sync. Automatic when iCloud capability is configured.

SwiftData with CloudKit

When CloudKit sync is enabled, all model relationships must be optional, @Attribute(.unique) constraints are not allowed, all properties need default values, and schemas are additive-only after production deployment. Plan the data model carefully before the first release.

Card Shuffling

True Count uses GameplayKit's GKMersenneTwisterRandomSource with arrayByShufflingObjects(in:) to perform a Fisher-Yates shuffle. This source supports deterministic seeding, which enables replay, debugging, and fairness auditing of specific shuffles. Deterministic seeding is the primary reason to use GameplayKit over Swift's built-in shuffle(), which cannot be seeded.

The app defines a Shoe struct that tracks remaining cards, supports configurable deck counts (2, 4, 6, and 8 as defined in Dealer Tables), and stores the random source so seeded replays produce identical shuffles. See Shuffling and Deck Penetration for the gameplay rules governing reshuffles and penetration depths.

Game State Machine

GKStateMachine manages the round flow. Each round progresses through a defined set of states with enforced transition rules.

StateDescriptionValid Next States
BettingPlayer places a betDealing
DealingCards dealt to player and dealerInsurance, PlayerTurn, Evaluating
InsuranceOffered when the dealer shows an AcePlayerTurn, Evaluating
PlayerTurnHit, Stand, Double Down, Split for the active handPlayerTurn (next split hand), DealerTurn, Evaluating
DealerTurnDealer plays according to table rulesEvaluating
EvaluatingCompare hands and determine winnersPayout
PayoutAnimate chips, update balance and statisticsRoundComplete
RoundCompleteReset the table for the next roundBetting

isValidNextState enforces legal transitions so the game cannot skip from Betting to Payout. didEnter(from:) triggers the corresponding SpriteKit animation (dealing cards, flipping the dealer hole card, sweeping chips).

Natural blackjack: Naturals are resolved from the opening deal flow. The dealer peeks for blackjack when showing an Ace or 10-value upcard. If insurance is applicable (Ace upcard with insurance enabled), the Insurance branch runs first. If a natural outcome then resolves the hand, the flow skips PlayerTurn and DealerTurn and goes directly to Evaluating.

Split hands: When a player splits, PlayerTurn tracks which hand is active via an index. Each split hand plays in sequence before transitioning to DealerTurn. Split aces receive exactly one card each and auto-stand. This is handled as logic within the PlayerTurn state, not as a separate state.

A pure BlackjackRules struct provides static functions for hand value calculation, dealer hit decisions, and available action determination. This layer contains no UI or framework dependencies and is the primary unit test target.

Haptic Design

EventHaptic Feel
Card dealLight tap
Card flipCrisp snap
Chip placementSoft thud
Blackjack or winCelebratory ascending pattern
BustHeavy single thud

True Count offers a haptic feedback toggle: On or Off. See Haptic Feedback for the player-facing settings.

Concurrency

Swift 6.2 (shipping with Xcode 26) defaults app targets to @MainActor isolation. This is ideal for a game where most state mutations happen in response to user interaction on the main thread.

IsolationComponents
@MainActor (default)SwiftUI Views, ViewModels, SpriteKit Scene, HapticsService, AudioService
ModelActorSwiftData persistence (writes on a dedicated context)

SpriteKit types (SKScene, SKNode) and GameplayKit types (GKStateMachine, GKRandomSource) are not Sendable and must remain on @MainActor. SwiftData model objects (PersistentModel) are also not Sendable. When passing data between @MainActor ViewModels and a ModelActor, pass PersistentIdentifier values and re-fetch on the receiving actor's context.

Error Handling

SubsystemFailure ModeHandling
StoreKit 2Purchase failure, network error, cancellationTransactions are journaled locally and resolve when connectivity returns. UI communicates purchase status.
GameKitAuthentication failure, network errorThe app is fully playable without Game Center. Leaderboard submissions queue and retry on reconnection.
CloudKitSync failure, quota limit, schema mismatchSwiftData works offline by default (local SQLite). Sync resumes automatically on reconnection.
SwiftDataDisk full, migration failureLightweight migration only (additive changes). Errors surfaced via a centralized error banner.

The app must be fully playable offline. All network-dependent features (leaderboard submission, CloudKit sync, StoreKit verification) queue automatically and resolve when connectivity returns.

CloudKit uses last-writer-wins for conflict resolution. The Game Center page documents the "one device at a time" constraint. The app detects concurrent sessions and warns the player to avoid sync conflicts.

Performance Notes

Battery optimization is the highest-impact concern. SpriteKit renders at 60fps continuously, even when the card table is static. Players spend approximately 80% of game time thinking with no animations playing. Pause the SpriteKit scene during idle periods (scene.isPaused and skView.isPaused both set to true). Resume when animations start. This saves 60 to 70% of GPU power draw during a session.

Launch strategy: Defer GameKit authentication and StoreKit product fetching until the player first accesses those features. Preload sound effects and start the CoreHaptics engine after the first frame appears. Target under 400ms to the first meaningful frame and under 1 second to a fully interactive game table.

Texture management: Pack all 52 card faces and card backs into a single 2048x2048 texture atlas. This keeps draw calls to 2 to 4 per frame and texture memory predictable.

Data at scale: Index date fields in SwiftData models used in predicates. Use FetchDescriptor with fetchLimit for pagination. Precompute aggregate statistics in a summary model rather than recalculating from raw hand records on every view appearance.

Accessibility

SpriteKit has no built-in accessibility support. True Count bridges this gap with a layered approach.

Game action controls (Hit, Stand, Double Down, Split) live in SwiftUI overlays with automatic VoiceOver support, Dynamic Type, and standard accessibility traits. Custom UIAccessibilityElement objects are created for card positions and chip stacks on the SpriteKit table, with accessibilityValue updated as the game state changes (for example, "Player hand: Ace of Spades, 10 of Hearts. Total: 21.").

SKLabelNode does not respect Dynamic Type. Any text rendered in SpriteKit reads the preferred font size from the system and scales manually.

How is this guide?

On this page