23 KiB
CasinoKit
A reusable Swift Package for building casino card games with SwiftUI. This package provides common components, themes, and utilities shared across casino game apps.
Requirements
- iOS 17.0+
- Swift 6.0+
- Xcode 16.0+
Installation
This package is included as a local package in the workspace. To use in another project:
- Copy the
CasinoKitfolder to your project - In Xcode: File → Add Package Dependencies → Add Local
- Select the
CasinoKitfolder
Features
🎴 Cards
CardView - A playing card with flip animation support.
import CasinoKit
// Face-up card
CardView(card: Card(suit: .hearts, rank: .ace), faceUp: true)
// Face-down card
CardView(card: card, faceUp: false)
// Empty placeholder (dotted outline)
CardPlaceholderView()
Card Model
let card = Card(suit: .spades, rank: .king)
print(card.displayValue) // "K"
print(card.suit.symbol) // "♠"
🎰 Chips
ChipView - A casino chip with denomination display.
ChipView(denomination: .hundred, size: 60, isSelected: true)
ChipSelectorView - Horizontal chip selector.
@State var selectedChip: ChipDenomination = .hundred
ChipSelectorView(
denominations: ChipDenomination.allCases,
selectedDenomination: $selectedChip
)
ChipStackView - Stacked chips showing bet amount.
ChipStackView(amount: 500, chipColor: .red)
Chip Denominations (with standard casino colors)
.one($1) - White/Light Blue.five($5) - Red.twentyFive($25) - Green.hundred($100) - Black.fiveHundred($500) - Purple.thousand($1,000) - Yellow.fiveThousand($5,000) - Brown.tenThousand($10,000) - Gray (custom).twentyFiveThousand($25,000) - Teal (plaque style).hundredThousand($100,000) - Burgundy (VIP)
📋 Sheets & Popups
SheetContainerView - Consistent modal sheet styling.
SheetContainerView(
title: "Settings",
content: {
SheetSection(title: "DISPLAY", icon: "eye") {
Toggle("Dark Mode", isOn: $darkMode)
}
SheetSection(title: "SOUND", icon: "speaker.wave.2") {
Slider(value: $volume)
}
},
onCancel: { dismiss() },
onDone: { save(); dismiss() },
doneButtonText: String(localized: "Done"),
cancelButtonText: String(localized: "Cancel")
)
SheetSection - Styled section within sheets.
SheetSection(title: "SECTION TITLE", icon: "star.fill") {
// Your content
}
🎓 Onboarding & Tutorials
WelcomeSheet - First-launch welcome screen with features list.
WelcomeSheet(
gameName: "Blackjack",
features: [
WelcomeFeature(
icon: "target",
title: "Beat the Dealer",
description: "Get closer to 21 than the dealer without going over"
),
WelcomeFeature(
icon: "lightbulb.fill",
title: "Learn Strategy",
description: "Built-in hints show optimal plays"
)
],
onStartTutorial: {
// Enable tutorial mode
gameState.onboarding.startTutorialMode()
},
onStartPlaying: {
// Skip to game
gameState.onboarding.completeWelcome()
}
)
Sherpa Walkthrough - Guided spotlight walkthrough (re-exported from Sherpa package).
// 1. Define walkthrough steps
enum MyTags: SherpaTags {
case bettingZone
case dealButton
func makeCallout() -> Callout {
switch self {
case .bettingZone:
return .localizedLabeled("walkthrough.bettingZone", systemImage: "hand.tap.fill")
case .dealButton:
return .localizedLabeled("walkthrough.dealButton", systemImage: "play.fill")
}
}
}
// 2. Wrap app in SherpaContainerView (in @main App)
SherpaContainerView(configuration: .default) {
ContentView()
}
// 3. Tag views and activate walkthrough
struct GameTableView: View, SherpaDelegate {
@State private var isWalkthroughActive = false
var body: some View {
VStack {
BettingZone()
.sherpaTag(MyTags.bettingZone)
DealButton()
.sherpaTag(MyTags.dealButton)
}
.sherpa(isActive: isWalkthroughActive, tags: MyTags.self, delegate: self)
}
func onWalkthroughComplete(sherpa: Sherpa) {
isWalkthroughActive = false
onboarding.completeWelcome()
}
}
OnboardingState - Track onboarding completion status.
let onboarding = OnboardingState(gameIdentifier: "blackjack")
// Check if user has seen welcome
if !onboarding.hasCompletedWelcome {
showWelcome = true
}
// Mark welcome/walkthrough as completed
onboarding.completeWelcome()
// Skip onboarding entirely
onboarding.skipOnboarding()
// Reset onboarding (for testing)
onboarding.reset()
PulsingModifier - Draw attention to interactive elements.
Button("Deal", action: deal)
.pulsing(
isActive: shouldHighlight,
color: .white,
scale: 1.3
)
🎲 Game Table Components
TableBackgroundView - Casino felt background with pattern.
TableBackgroundView() // Default casino green
TableBackgroundView(feltColor: .blue, edgeColor: .darkBlue) // Custom
TopBarView - Balance display and toolbar buttons.
TopBarView(
balance: 10_500,
secondaryInfo: "411",
secondaryIcon: "rectangle.portrait.on.rectangle.portrait.fill",
onSettings: { showSettings = true },
onHelp: { showRules = true },
onStats: { showStats = true }
)
TopBarButton - Add custom buttons to the toolbar.
// Add game-specific buttons at the front of the toolbar
TopBarView(
balance: 10_500,
leadingButtons: [
TopBarButton(icon: "arrow.counterclockwise", accessibilityLabel: "Reset Game") {
resetGame()
},
TopBarButton(icon: "creditcard", accessibilityLabel: "Buy Chips") {
showBuyChips = true
}
],
onSettings: { showSettings = true },
onHelp: { showRules = true },
onStats: { showStats = true }
)
HandDisplayView - Generic card hand display with labels.
HandDisplayView(
cards: [card1, card2],
cardsFaceUp: [true, true],
isWinner: true,
label: "PLAYER",
value: 21,
valueColor: .blue,
maxCards: 5 // Reserve space for up to 5 cards
)
BettingZone - Tappable betting area with chip badge.
BettingZone(
label: "PLAYER",
payoutInfo: "1:1",
betAmount: 500,
backgroundColor: .blue.opacity(0.2),
onTap: { placeBet(.player) }
)
ActionButton - Styled action buttons.
ActionButton("Deal", icon: "play.fill", style: .primary) { deal() }
ActionButton("Clear", icon: "xmark.circle", style: .destructive) { clear() }
ActionButton("Stand", style: .secondary, isEnabled: canStand) { stand() }
🎉 Effects & Overlays
ConfettiView - Win celebration effect.
if playerWon {
ConfettiView() // Colorful falling confetti
ConfettiView(colors: [.yellow, .gold], count: 30) // Customized
}
GameOverView - Game over modal when out of chips.
GameOverView(
roundsPlayed: 25,
additionalStats: [
("Biggest Win", "$5,000"),
("Blackjacks", "3")
],
onPlayAgain: { resetGame() }
)
ValueBadge - Circular numeric badge.
ValueBadge(value: 21, color: .blue) // Hand value display
⚙️ Settings Components
SettingsToggle - Toggle with title and subtitle.
SettingsToggle(
title: "Sound Effects",
subtitle: "Chips, cards, and results",
isOn: $settings.soundEnabled
)
SpeedPicker - Fast/Normal/Slow animation speed.
SpeedPicker(speed: $settings.dealingSpeed)
VolumePicker - Volume slider with icons.
VolumePicker(volume: $settings.soundVolume)
BalancePicker - Starting balance grid.
BalancePicker(
balance: $settings.startingBalance,
options: [1_000, 5_000, 10_000, 25_000, 50_000, 100_000]
)
TableLimits - Betting limit presets.
let limits = TableLimits.medium
print(limits.minBet) // 25
print(limits.maxBet) // 5000
print(limits.displayName) // "Medium Stakes"
🎨 Branding & Icons
AppIconView - Generate app icons with consistent styling.
// Use a preset
AppIconView(config: .baccarat, size: 1024)
// Custom configuration
let config = AppIconConfig(
title: "BLACKJACK",
subtitle: "21",
iconSymbol: "suit.club.fill",
primaryColor: Color(red: 0.1, green: 0.2, blue: 0.35),
secondaryColor: Color(red: 0.05, green: 0.12, blue: 0.25),
accentColor: .yellow
)
AppIconView(config: config, size: 1024)
Preset Configurations:
.baccarat- Spade symbol.blackjack- Club symbol with "21" subtitle.poker- Diamond symbol, red accent.roulette- Grid symbol, red theme
LaunchScreenView - Animated splash screen.
LaunchScreenView(config: .baccarat)
IconRenderer - Render views to images.
// Render single icon
let image = IconRenderer.renderAppIcon(config: .baccarat, size: 1024)
// Render all iOS sizes
let allImages = IconRenderer.renderAllSizes(config: .baccarat)
🔊 Audio & Haptics
SoundManager - Manages game sounds and haptic feedback.
// Access shared instance
let sound = SoundManager.shared
// Configure settings
sound.soundEnabled = true
sound.hapticsEnabled = true
sound.volume = 1.0
// Play sounds
sound.play(.chipPlace)
sound.play(.cardDeal)
sound.play(.win)
// Convenience methods (include haptics)
sound.playChipPlace() // Chip sound + light haptic
sound.playCardFlip() // Card sound + light haptic
sound.playWin() // Win sound + success haptic
sound.playLose() // Lose sound + error haptic
sound.playGameOver() // Game over sound + error haptic
Available Sounds:
| Sound | Description |
|---|---|
.chipPlace |
Placing a bet |
.chipStack |
Stacking chips |
.cardDeal |
Dealing a card |
.cardFlip |
Flipping a card |
.cardShuffle |
Shuffling deck |
.win |
Player wins |
.lose |
Player loses |
.push |
Tie/push |
.bigWin |
Large payout |
.buttonTap |
UI tap |
.newRound |
Starting new round |
.clearBets |
Clearing bets |
.gameOver |
Out of chips |
Custom Sound Files:
The manager uses iOS system sounds as fallback. To use custom sounds:
- Add
.mp3files named:chip_place.mp3,card_deal.mp3,win.mp3, etc. - Add them to your app bundle's Resources folder
- Files are automatically detected and used
Haptic Methods:
sound.hapticLight() // Light tap
sound.hapticMedium() // Medium tap
sound.hapticHeavy() // Heavy tap
sound.hapticSuccess() // Success notification
sound.hapticError() // Error notification
sound.hapticWarning() // Warning notification
📊 Session Management
GameSession - Track play sessions with common and game-specific stats.
// Create a game-specific stats type
struct MyGameStats: GameSpecificStats {
var specialWins: Int = 0
init() {}
var displayItems: [StatDisplayItem] {
[StatDisplayItem(icon: "star.fill", iconColor: .yellow,
label: "Special Wins", value: "\(specialWins)")]
}
}
// Create session type alias
typealias MyGameSession = GameSession<MyGameStats>
SessionManagedGame Protocol - Add session management to your game state.
@Observable
class GameState: SessionManagedGame {
typealias Stats = MyGameStats
var currentSession: MyGameSession?
var sessionHistory: [MyGameSession] = []
// Record round results
func completeRound(winnings: Int, bet: Int) {
recordSessionRound(
winnings: winnings,
betAmount: bet,
outcome: winnings > 0 ? .win : .lose
) { stats in
if wasSpecialWin {
stats.specialWins += 1
}
}
}
}
Session UI Components:
// End session button
EndSessionButton {
state.showEndSessionConfirmation = true
}
// Current session header with live stats
CurrentSessionHeader(
duration: session.duration,
roundsPlayed: session.roundsPlayed,
netResult: session.netResult,
onEndSession: { /* confirm */ }
)
// Session row for history list
SessionSummaryRow(
styleDisplayName: "Vegas Strip",
duration: session.duration,
roundsPlayed: session.roundsPlayed,
netResult: session.netResult,
startTime: session.startTime,
isActive: false,
endReason: .manualEnd
)
SessionFormatter - Format session data for display.
SessionFormatter.formatDuration(3600) // "01h 00min"
SessionFormatter.formatMoney(500) // "$500"
SessionFormatter.formatMoney(-250) // "-$250"
SessionFormatter.formatPercent(65.5) // "65.5%"
For detailed documentation, see SESSION_SYSTEM.md.
💾 Cloud Storage
CloudSyncManager - Saves game data locally and syncs with iCloud.
// 1. Define your game's data structure
struct MyGameData: PersistableGameData {
static let gameIdentifier = "mygame"
var roundsPlayed: Int { rounds.count }
var lastModified: Date
static var empty: MyGameData {
MyGameData(rounds: [], balance: 10000, lastModified: Date())
}
var rounds: [RoundData]
var balance: Int
}
// 2. Create sync manager
let persistence = CloudSyncManager<MyGameData>()
// 3. Save data (auto-syncs to iCloud)
var data = persistence.data
data.balance = 5000
persistence.save(data)
// 4. Data is automatically loaded on init
print(persistence.data.balance)
// 5. Check sync status
if persistence.iCloudAvailable {
print("Last sync: \(persistence.lastSyncDate)")
}
// 6. Force sync
persistence.sync()
// 7. Listen for changes from other devices
persistence.onCloudDataReceived = { newData in
print("Got \(newData.roundsPlayed) rounds from iCloud")
}
// 8. Reset all data
persistence.reset()
PersistableGameData Protocol:
public protocol PersistableGameData: Codable, Sendable {
static var gameIdentifier: String { get } // e.g., "baccarat"
var roundsPlayed: Int { get } // For conflict resolution
var lastModified: Date { get set } // Updated automatically
static var empty: Self { get } // Default/new game state
}
Features:
- 📱 Local Storage - Always saved to UserDefaults
- ☁️ iCloud Sync - Automatic sync when signed in
- 🔄 Conflict Resolution - Uses
roundsPlayedto pick newer data - 📢 Change Notifications - Callbacks when data changes from other devices
- 🔒 Privacy - Uses Apple ID, no Game Center required
📈 Analytics
AnalyticsService - Lightweight event tracking with a configurable endpoint.
let analytics: AnalyticsTracking = AnalyticsService()
await analytics.track(
AnalyticsEvent(
name: "round_started",
properties: [
"game": .string("blackjack"),
"tableLimit": .int(25)
]
)
)
Configure an endpoint later:
await analytics.updateEndpoint(URL(string: "https://example.com/analytics"))
No-op tracker for disabled analytics:
let analytics: AnalyticsTracking = NoOpAnalyticsTracker()
🎨 Design System
CasinoDesign - Shared design constants.
// Spacing
CasinoDesign.Spacing.small // 8
CasinoDesign.Spacing.medium // 12
CasinoDesign.Spacing.large // 16
// Corner Radius
CasinoDesign.CornerRadius.small // 8
CasinoDesign.CornerRadius.medium // 12
CasinoDesign.CornerRadius.large // 16
// Font Sizes (base values for @ScaledMetric)
CasinoDesign.BaseFontSize.small // 12
CasinoDesign.BaseFontSize.body // 14
CasinoDesign.BaseFontSize.large // 20
// Opacity
CasinoDesign.Opacity.subtle // 0.05
CasinoDesign.Opacity.light // 0.2
CasinoDesign.Opacity.medium // 0.5
CasinoDesign.Opacity.heavy // 0.8
// Animation
CasinoDesign.Animation.quick // 0.2
CasinoDesign.Animation.standard // 0.3
CasinoDesign.Animation.springDuration // 0.4
Color.Sheet - Sheet/popup colors.
Color.Sheet.background // Dark background
Color.Sheet.sectionFill // Section card fill
Color.Sheet.accent // Yellow accent
Color.Sheet.secondaryText // Muted text
Color.Sheet.cancelText // Cancel button text
🌍 Localization
CasinoKit includes localization for:
- English (en)
- Spanish - Mexico (es-MX)
- French - Canada (fr-CA)
Localized Strings:
- Card names (Ace, King, Queen, etc.)
- Suit names (Hearts, Diamonds, Clubs, Spades)
- Chip-related strings
- Accessibility labels
Adding Localizations:
The package uses String Catalogs (.xcstrings). Edit:
CasinoKit/Sources/CasinoKit/Resources/Localizable.xcstrings
Usage in a New Game
1. Import the Package
import SwiftUI
import CasinoKit
2. Create Your Game View
struct BlackjackTableView: View {
@State private var deck = Deck(numberOfDecks: 6)
@State private var selectedChip: ChipDenomination = .hundred
var body: some View {
VStack {
// Player's hand
HStack {
ForEach(playerCards) { card in
CardView(card: card, faceUp: true)
}
}
// Chip selector
ChipSelectorView(
denominations: ChipDenomination.allCases,
selectedDenomination: $selectedChip
)
}
}
}
3. Create Settings Sheet
struct SettingsView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
SheetContainerView(title: "Settings") {
SheetSection(title: "GAME OPTIONS", icon: "gearshape") {
// Your settings
}
} onDone: {
dismiss()
}
}
}
4. Generate App Icon
// In a preview or development view
#Preview {
let config = AppIconConfig(
title: "BLACKJACK",
subtitle: "21",
iconSymbol: "suit.club.fill"
)
return AppIconView(config: config, size: 512)
}
Screenshot the preview and add to your Assets.xcassets.
File Structure
CasinoKit/
├── Package.swift
├── README.md
├── GAME_TEMPLATE.md # Guide for creating new games
├── SESSION_SYSTEM.md # Session tracking documentation
├── Sources/CasinoKit/
│ ├── CasinoKit.swift
│ ├── Exports.swift
│ ├── Models/
│ │ ├── Card.swift
│ │ ├── Deck.swift
│ │ ├── ChipDenomination.swift
│ │ ├── TableLimits.swift # Betting limit presets
│ │ ├── OnboardingState.swift # Onboarding tracking
│ │ └── Session/
│ │ ├── GameSession.swift # Generic session with stats
│ │ ├── GameSessionProtocol.swift # Session protocols
│ │ └── SessionFormatter.swift # Formatting utilities
│ ├── Views/
│ │ ├── Cards/
│ │ │ ├── CardView.swift
│ │ │ └── HandDisplayView.swift # Generic hand display
│ │ ├── Chips/
│ │ │ ├── ChipView.swift
│ │ │ ├── ChipSelectorView.swift
│ │ │ ├── ChipStackView.swift
│ │ │ └── ChipOnTableView.swift
│ │ ├── Sheets/
│ │ │ └── SheetContainerView.swift
│ │ ├── Onboarding/
│ │ │ ├── WelcomeSheet.swift # First-launch welcome
│ │ │ └── PulsingModifier.swift # Attention-grabbing pulse
│ │ ├── Branding/
│ │ │ ├── AppIconView.swift
│ │ │ ├── LaunchScreenView.swift
│ │ │ └── IconRenderer.swift
│ │ ├── Effects/
│ │ │ └── ConfettiView.swift # Win celebration
│ │ ├── Overlays/
│ │ │ └── GameOverView.swift # Game over modal
│ │ ├── Table/
│ │ │ └── TableBackgroundView.swift # Felt background
│ │ ├── Bars/
│ │ │ └── TopBarView.swift # Balance & toolbar
│ │ ├── Badges/
│ │ │ └── ValueBadge.swift # Numeric badge
│ │ ├── Buttons/
│ │ │ └── ActionButton.swift # Deal/Hit/Stand buttons
│ │ ├── Zones/
│ │ │ └── BettingZone.swift # Tappable betting area
│ │ ├── Settings/
│ │ │ └── SettingsComponents.swift # Toggle, pickers
│ │ └── Session/
│ │ └── SessionViews.swift # Session UI components
│ ├── Audio/
│ │ └── SoundManager.swift
│ ├── Analytics/
│ │ ├── Models/
│ │ │ ├── AnalyticsEvent.swift
│ │ │ └── AnalyticsValue.swift
│ │ ├── Protocols/
│ │ │ └── AnalyticsTracking.swift
│ │ └── Services/
│ │ ├── AnalyticsService.swift
│ │ └── NoOpAnalyticsTracker.swift
│ ├── Storage/
│ │ └── CloudSyncManager.swift # iCloud persistence
│ ├── Theme/
│ │ ├── CasinoTheme.swift
│ │ └── CasinoDesign.swift
│ └── Resources/
│ ├── Localizable.xcstrings
│ └── Sounds/
│ └── (audio files)
└── Tests/CasinoKitTests/
└── CasinoKitTests.swift
Best Practices
Design Constants
Always use CasinoDesign constants instead of magic numbers:
// ✅ Good
.padding(CasinoDesign.Spacing.medium)
.opacity(CasinoDesign.Opacity.heavy)
// ❌ Bad
.padding(12)
.opacity(0.8)
Localization
Always pass localized strings for button text:
SheetContainerView(
title: String(localized: "Settings"),
content: { ... },
onDone: { dismiss() },
doneButtonText: String(localized: "Done")
)
Accessibility
All components include VoiceOver support:
- Cards announce suit and rank
- Chips announce denomination
- Interactive elements have labels and hints
Dynamic Type
Use @ScaledMetric with base font sizes:
@ScaledMetric(relativeTo: .body)
private var fontSize: CGFloat = CasinoDesign.BaseFontSize.body
Apps Using CasinoKit
- Blackjack - Classic 21 with basic strategy hints and card counting
- Baccarat - The classic casino card game with road maps
Version History
-
1.1.0 - Session system
- Generic
GameSession<Stats>for tracking play sessions SessionManagedGameprotocol for easy integration- Session UI components (header, summary rows, end button)
SessionFormatterfor consistent data display- Aggregated statistics across sessions
- Generic
-
1.0.0 - Initial release
- Card and Chip components
- Sheet container views
- App icon and launch screen generators
- Localization support (EN, ES-MX, FR-CA)
License
This package is for personal use in your casino game projects.