Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2025-12-17 08:15:52 -06:00
parent 6baf2924bb
commit d2b93a019f
11 changed files with 3768 additions and 2157 deletions

View File

@ -423,11 +423,14 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = Baccarat;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.casino-games";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@ -440,6 +443,7 @@
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.Baccarat;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
@ -455,11 +459,14 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = Baccarat;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.casino-games";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@ -472,6 +479,7 @@
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.Baccarat;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23094" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23084"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="♠️ ♥️" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="iconsLabel">
<rect key="frame" x="146.66666666666666" y="376" width="100" height="50"/>
<constraints>
<constraint firstAttribute="height" constant="50" id="iconHeight"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="40"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="BACCARAT" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="titleLabel">
<rect key="frame" x="97" y="434" width="199" height="41"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="34"/>
<color key="textColor" red="1" green="0.80000000000000004" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" red="0.058823529411764705" green="0.12156862745098039" blue="0.2196078431372549" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="iconsLabel" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="iconsCenterX"/>
<constraint firstItem="iconsLabel" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" constant="-50" id="iconsCenterY"/>
<constraint firstItem="titleLabel" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="titleCenterX"/>
<constraint firstItem="titleLabel" firstAttribute="top" secondItem="iconsLabel" secondAttribute="bottom" constant="8" id="titleTopToIcons"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,130 @@
//
// BrandingPreviewView.swift
// Baccarat
//
// Development view for previewing and exporting app icons and launch screens.
// Access this during development to generate icon assets.
//
import SwiftUI
import CasinoKit
/// Preview view for app branding assets.
/// Use this during development to preview and export icons.
struct BrandingPreviewView: View {
var body: some View {
TabView {
// App Icon Preview
ScrollView {
VStack(spacing: 32) {
Text("App Icon")
.font(.largeTitle.bold())
AppIconView(config: .baccarat, size: 300)
.clipShape(.rect(cornerRadius: 300 * 0.22))
.shadow(radius: 20)
Text("All Sizes")
.font(.title2.bold())
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))], spacing: 20) {
ForEach([180, 120, 87, 60, 40], id: \.self) { size in
VStack {
AppIconView(config: .baccarat, size: CGFloat(size))
.clipShape(.rect(cornerRadius: CGFloat(size) * 0.22))
Text("\(size)px")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
instructionsSection
}
.padding()
}
.tabItem {
Label("Icon", systemImage: "app.fill")
}
// Launch Screen Preview
LaunchScreenView(config: .baccarat)
.tabItem {
Label("Launch", systemImage: "rectangle.portrait.fill")
}
// Other Games Preview
ScrollView {
VStack(spacing: 32) {
Text("Other Game Icons")
.font(.largeTitle.bold())
HStack(spacing: 20) {
VStack {
AppIconView(config: .blackjack, size: 150)
.clipShape(.rect(cornerRadius: 150 * 0.22))
Text("Blackjack")
.font(.caption)
}
VStack {
AppIconView(config: .poker, size: 150)
.clipShape(.rect(cornerRadius: 150 * 0.22))
Text("Poker")
.font(.caption)
}
VStack {
AppIconView(config: .roulette, size: 150)
.clipShape(.rect(cornerRadius: 150 * 0.22))
Text("Roulette")
.font(.caption)
}
}
Text("These show how the same pattern works for other games")
.font(.callout)
.foregroundStyle(.secondary)
}
.padding()
}
.tabItem {
Label("Others", systemImage: "square.grid.2x2")
}
}
}
private var instructionsSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("How to Export Icons")
.font(.headline)
VStack(alignment: .leading, spacing: 8) {
Text("Option 1: Screenshot from Preview")
.font(.subheadline.bold())
Text("• Run the preview in Xcode")
Text("• Screenshot the 1024px icon")
Text("• Use an online tool to generate all sizes")
}
VStack(alignment: .leading, spacing: 8) {
Text("Option 2: Use IconRenderer in Code")
.font(.subheadline.bold())
Text("• Call IconRenderer.renderAppIcon(config: .baccarat)")
Text("• Save the resulting UIImage to files")
Text("• Add to Assets.xcassets/AppIcon")
}
}
.font(.callout)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color.gray.opacity(0.1))
.clipShape(.rect(cornerRadius: 12))
}
}
#Preview {
BrandingPreviewView()
}

View File

@ -0,0 +1,192 @@
//
// IconGeneratorView.swift
// Baccarat
//
// Development tool to generate and export app icon images.
// Run this view, tap the button, then find the icons in the Files app.
//
import SwiftUI
import CasinoKit
/// A development view that generates and saves app icon images.
/// After running, find the icons in Files app On My iPhone Baccarat
struct IconGeneratorView: View {
@State private var status: String = "Tap the button to generate icons"
@State private var isGenerating = false
@State private var generatedIcons: [GeneratedIcon] = []
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 24) {
// Preview
AppIconView(config: .baccarat, size: 200)
.clipShape(.rect(cornerRadius: 200 * 0.22))
.shadow(radius: 10)
Text("App Icon Preview")
.font(.headline)
// Generate button
Button {
Task {
await generateIcons()
}
} label: {
HStack {
if isGenerating {
ProgressView()
.tint(.white)
}
Text(isGenerating ? "Generating..." : "Generate & Save Icons")
}
.font(.headline)
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.padding()
.background(isGenerating ? Color.gray : Color.blue)
.clipShape(.rect(cornerRadius: 12))
}
.disabled(isGenerating)
.padding(.horizontal)
// Status
Text(status)
.font(.callout)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
// Generated icons
if !generatedIcons.isEmpty {
VStack(alignment: .leading, spacing: 12) {
Text("Generated Icons:")
.font(.headline)
ForEach(generatedIcons) { icon in
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
Text(icon.filename)
.font(.caption.monospaced())
Spacer()
Text("\(Int(icon.size))px")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
.padding()
.background(Color.green.opacity(0.1))
.clipShape(.rect(cornerRadius: 12))
.padding(.horizontal)
}
// Instructions
instructionsSection
}
.padding(.vertical)
}
.navigationTitle("Icon Generator")
}
}
private var instructionsSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("After generating:")
.font(.headline)
VStack(alignment: .leading, spacing: 8) {
instructionRow(number: 1, text: "Open Files app on your device/simulator")
instructionRow(number: 2, text: "Navigate to: On My iPhone → Baccarat")
instructionRow(number: 3, text: "Find the AppIcon-1024.png file")
instructionRow(number: 4, text: "AirDrop or share to your Mac")
instructionRow(number: 5, text: "Drag into Xcode's Assets.xcassets/AppIcon")
}
Divider()
Text("Alternative: Use an online tool")
.font(.subheadline.bold())
Text("Upload the 1024px icon to appicon.co or makeappicon.com to generate all sizes automatically.")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding()
.background(Color.gray.opacity(0.1))
.clipShape(.rect(cornerRadius: 12))
.padding(.horizontal)
}
private func instructionRow(number: Int, text: String) -> some View {
HStack(alignment: .top, spacing: 8) {
Text("\(number).")
.font(.callout.bold())
.foregroundStyle(.blue)
Text(text)
.font(.callout)
}
}
@MainActor
private func generateIcons() async {
isGenerating = true
generatedIcons = []
status = "Generating icons..."
let sizes: [(CGFloat, String)] = [
(1024, "AppIcon-1024"),
(180, "AppIcon-180"),
(120, "AppIcon-120"),
(87, "AppIcon-87"),
(80, "AppIcon-80"),
(60, "AppIcon-60"),
(40, "AppIcon-40")
]
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
for (size, name) in sizes {
// Render the icon
let view = AppIconView(config: .baccarat, size: size)
let renderer = ImageRenderer(content: view)
renderer.scale = 1.0
if let uiImage = renderer.uiImage,
let data = uiImage.pngData() {
let filename = "\(name).png"
let fileURL = documentsPath.appending(path: filename)
do {
try data.write(to: fileURL)
generatedIcons.append(GeneratedIcon(filename: filename, size: size))
} catch {
status = "Error saving \(filename): \(error.localizedDescription)"
}
}
// Small delay for UI feedback
try? await Task.sleep(for: .milliseconds(100))
}
if generatedIcons.count == sizes.count {
status = "✅ All icons saved to Documents folder!\nOpen Files app to find them."
} else {
status = "⚠️ Some icons failed to generate"
}
isGenerating = false
}
}
struct GeneratedIcon: Identifiable {
let id = UUID()
let filename: String
let size: CGFloat
}
#Preview {
IconGeneratorView()
}

378
CasinoKit/README.md Normal file
View File

@ -0,0 +1,378 @@
# 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:
1. Copy the `CasinoKit` folder to your project
2. In Xcode: File → Add Package Dependencies → Add Local
3. Select the `CasinoKit` folder
## Features
### 🎴 Cards
**CardView** - A playing card with flip animation support.
```swift
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**
```swift
let card = Card(suit: .spades, rank: .king)
print(card.displayValue) // "K"
print(card.suit.symbol) // "♠"
```
### 🎰 Chips
**ChipView** - A casino chip with denomination display.
```swift
ChipView(denomination: .hundred, size: 60, isSelected: true)
```
**ChipSelectorView** - Horizontal chip selector.
```swift
@State var selectedChip: ChipDenomination = .hundred
ChipSelectorView(
denominations: ChipDenomination.allCases,
selectedDenomination: $selectedChip
)
```
**ChipStackView** - Stacked chips showing bet amount.
```swift
ChipStackView(amount: 500, chipColor: .red)
```
**Chip Denominations**
- `.one` (1)
- `.five` (5)
- `.twentyFive` (25)
- `.hundred` (100)
- `.fiveHundred` (500)
- `.thousand` (1000)
### 📋 Sheets & Popups
**SheetContainerView** - Consistent modal sheet styling.
```swift
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.
```swift
SheetSection(title: "SECTION TITLE", icon: "star.fill") {
// Your content
}
```
### 🎨 Branding & Icons
**AppIconView** - Generate app icons with consistent styling.
```swift
// 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.
```swift
LaunchScreenView(config: .baccarat)
```
**IconRenderer** - Render views to images.
```swift
// Render single icon
let image = IconRenderer.renderAppIcon(config: .baccarat, size: 1024)
// Render all iOS sizes
let allImages = IconRenderer.renderAllSizes(config: .baccarat)
```
### 🎨 Design System
**CasinoDesign** - Shared design constants.
```swift
// 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.
```swift
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
```swift
import SwiftUI
import CasinoKit
```
### 2. Create Your Game View
```swift
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
```swift
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
```swift
// 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
├── Sources/CasinoKit/
│ ├── CasinoKit.swift
│ ├── Exports.swift
│ ├── Models/
│ │ ├── Card.swift
│ │ ├── Deck.swift
│ │ └── ChipDenomination.swift
│ ├── Views/
│ │ ├── Cards/
│ │ │ └── CardView.swift
│ │ ├── Chips/
│ │ │ ├── ChipView.swift
│ │ │ ├── ChipSelectorView.swift
│ │ │ ├── ChipStackView.swift
│ │ │ └── ChipOnTableView.swift
│ │ ├── Sheets/
│ │ │ └── SheetContainerView.swift
│ │ └── Branding/
│ │ ├── AppIconView.swift
│ │ ├── LaunchScreenView.swift
│ │ └── IconRenderer.swift
│ ├── Theme/
│ │ ├── CasinoTheme.swift
│ │ └── CasinoDesign.swift
│ └── Resources/
│ └── Localizable.xcstrings
└── Tests/CasinoKitTests/
└── CasinoKitTests.swift
```
## Best Practices
### Design Constants
Always use `CasinoDesign` constants instead of magic numbers:
```swift
// ✅ Good
.padding(CasinoDesign.Spacing.medium)
.opacity(CasinoDesign.Opacity.heavy)
// ❌ Bad
.padding(12)
.opacity(0.8)
```
### Localization
Always pass localized strings for button text:
```swift
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:
```swift
@ScaledMetric(relativeTo: .body)
private var fontSize: CGFloat = CasinoDesign.BaseFontSize.body
```
## Apps Using CasinoKit
- **Baccarat** - The classic casino card game
## Version History
- **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.

View File

@ -21,6 +21,11 @@
// - ChipStackView, ChipOnTableView
// - SheetContainerView, SheetSection
// MARK: - Branding
// - AppIconView, AppIconConfig
// - LaunchScreenView, LaunchScreenConfig, StaticLaunchScreenView
// - IconRenderer, IconExportView
// MARK: - Theme
// - CasinoTheme (protocol)
// - DefaultCasinoTheme

View File

@ -45,6 +45,21 @@
}
}
},
"%lldpt" : {
"comment" : "A caption below an app icon that shows its size in points. The argument is the size of the icon in points.",
"isCommentAutoGenerated" : true
},
"1. Use Xcode's preview to screenshot these icons" : {
},
"2. Or use IconRenderer.renderAppIcon() in code" : {
"comment" : "An instruction in the Icon Export View explaining how to generate app icons using code.",
"isCommentAutoGenerated" : true
},
"3. Add generated images to Assets.xcassets/AppIcon" : {
"comment" : "Instructions for adding generated app icon images to Xcode's asset catalog.",
"isCommentAutoGenerated" : true
},
"Ace" : {
"localizations" : {
"en" : {
@ -67,6 +82,10 @@
}
}
},
"App Icon Preview" : {
"comment" : "A title for the preview section of the icon export view.",
"isCommentAutoGenerated" : true
},
"Card face down" : {
"localizations" : {
"en" : {
@ -221,6 +240,10 @@
}
}
},
"Export Instructions" : {
"comment" : "A section header describing how to export app icons.",
"isCommentAutoGenerated" : true
},
"Five" : {
"localizations" : {
"en" : {
@ -485,6 +508,10 @@
}
}
},
"Size Variants" : {
"comment" : "A heading for the different sizes of the app icon previews.",
"isCommentAutoGenerated" : true
},
"Spades" : {
"localizations" : {
"en" : {

View File

@ -0,0 +1,218 @@
//
// AppIconView.swift
// CasinoKit
//
// A reusable app icon design that can be customized for different casino games.
// Render this view to an image for use as your app icon.
//
import SwiftUI
/// Configuration for the app icon appearance.
public struct AppIconConfig: Sendable {
public let title: String
public let subtitle: String?
public let iconSymbol: String
public let primaryColor: Color
public let secondaryColor: Color
public let accentColor: Color
public init(
title: String,
subtitle: String? = nil,
iconSymbol: String,
primaryColor: Color = Color(red: 0.1, green: 0.2, blue: 0.35),
secondaryColor: Color = Color(red: 0.05, green: 0.12, blue: 0.25),
accentColor: Color = .yellow
) {
self.title = title
self.subtitle = subtitle
self.iconSymbol = iconSymbol
self.primaryColor = primaryColor
self.secondaryColor = secondaryColor
self.accentColor = accentColor
}
// MARK: - Preset Configurations
/// Baccarat game icon configuration.
public static let baccarat = AppIconConfig(
title: "BACCARAT",
iconSymbol: "suit.spade.fill"
)
/// Blackjack game icon configuration.
public static let blackjack = AppIconConfig(
title: "BLACKJACK",
subtitle: "21",
iconSymbol: "suit.club.fill"
)
/// Poker game icon configuration.
public static let poker = AppIconConfig(
title: "POKER",
iconSymbol: "suit.diamond.fill",
accentColor: .red
)
/// Roulette game icon configuration.
public static let roulette = AppIconConfig(
title: "ROULETTE",
iconSymbol: "circle.grid.3x3.fill",
primaryColor: Color(red: 0.4, green: 0.1, blue: 0.1),
secondaryColor: Color(red: 0.25, green: 0.05, blue: 0.05)
)
}
/// A customizable app icon view for casino games.
/// Render this view to create your app icon assets.
public struct AppIconView: View {
let config: AppIconConfig
let size: CGFloat
public init(config: AppIconConfig, size: CGFloat = 1024) {
self.config = config
self.size = size
}
private var cornerRadius: CGFloat { size * 0.22 }
private var iconSize: CGFloat { size * 0.35 }
private var titleSize: CGFloat { size * 0.12 }
private var subtitleSize: CGFloat { size * 0.25 }
private var borderWidth: CGFloat { size * 0.02 }
public var body: some View {
ZStack {
// Background gradient
RoundedRectangle(cornerRadius: cornerRadius)
.fill(
LinearGradient(
colors: [config.primaryColor, config.secondaryColor],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
// Subtle pattern overlay
DiamondPatternOverlay(size: size)
.opacity(0.08)
// Gold border
RoundedRectangle(cornerRadius: cornerRadius)
.strokeBorder(
LinearGradient(
colors: [
config.accentColor,
config.accentColor.opacity(0.6),
config.accentColor
],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: borderWidth
)
// Content
VStack(spacing: size * 0.03) {
// Icon symbol
Image(systemName: config.iconSymbol)
.font(.system(size: iconSize, weight: .bold))
.foregroundStyle(
LinearGradient(
colors: [config.accentColor, config.accentColor.opacity(0.8)],
startPoint: .top,
endPoint: .bottom
)
)
.shadow(color: .black.opacity(0.3), radius: size * 0.02, y: size * 0.01)
// Subtitle (e.g., "21" for Blackjack)
if let subtitle = config.subtitle {
Text(subtitle)
.font(.system(size: subtitleSize, weight: .black, design: .rounded))
.foregroundStyle(
LinearGradient(
colors: [config.accentColor, .orange],
startPoint: .top,
endPoint: .bottom
)
)
.shadow(color: .black.opacity(0.5), radius: size * 0.01)
}
// Title
Text(config.title)
.font(.system(size: titleSize, weight: .black, design: .rounded))
.tracking(size * 0.005)
.foregroundStyle(
LinearGradient(
colors: [.white, .white.opacity(0.85)],
startPoint: .top,
endPoint: .bottom
)
)
.shadow(color: .black.opacity(0.5), radius: size * 0.01)
}
}
.frame(width: size, height: size)
}
}
/// Diamond pattern overlay for the icon background.
private struct DiamondPatternOverlay: View {
let size: CGFloat
private var spacing: CGFloat { size * 0.08 }
private var diamondSize: CGFloat { size * 0.03 }
var body: some View {
Canvas { context, canvasSize in
let rows = Int(canvasSize.height / spacing) + 1
let cols = Int(canvasSize.width / spacing) + 1
for row in 0..<rows {
for col in 0..<cols {
let offset: CGFloat = row % 2 == 0 ? 0 : spacing / 2
let x = CGFloat(col) * spacing + offset
let y = CGFloat(row) * spacing
let diamond = Path { path in
path.move(to: CGPoint(x: x, y: y - diamondSize))
path.addLine(to: CGPoint(x: x + diamondSize, y: y))
path.addLine(to: CGPoint(x: x, y: y + diamondSize))
path.addLine(to: CGPoint(x: x - diamondSize, y: y))
path.closeSubpath()
}
context.fill(diamond, with: .color(.white))
}
}
}
}
}
// MARK: - Preview
#Preview("Baccarat Icon") {
AppIconView(config: .baccarat, size: 512)
.padding()
.background(Color.gray)
}
#Preview("Blackjack Icon") {
AppIconView(config: .blackjack, size: 512)
.padding()
.background(Color.gray)
}
#Preview("All Icons") {
HStack(spacing: 20) {
AppIconView(config: .baccarat, size: 200)
AppIconView(config: .blackjack, size: 200)
AppIconView(config: .poker, size: 200)
AppIconView(config: .roulette, size: 200)
}
.padding()
.background(Color.gray)
}

View File

@ -0,0 +1,139 @@
//
// IconRenderer.swift
// CasinoKit
//
// Utility to render SwiftUI views to images for app icons.
//
import SwiftUI
/// Utility to render SwiftUI views to images.
@MainActor
public struct IconRenderer {
/// Standard iOS app icon sizes.
public static let iOSIconSizes: [CGFloat] = [
1024, // App Store
180, // iPhone @3x
120, // iPhone @2x
167, // iPad Pro @2x
152, // iPad @2x
76, // iPad @1x
40, // Spotlight @2x
60, // Spotlight @3x
29, // Settings @1x
58, // Settings @2x
87 // Settings @3x
]
/// Renders an app icon view to a UIImage.
/// - Parameters:
/// - config: The app icon configuration.
/// - size: The size to render at (default 1024 for App Store).
/// - Returns: A rendered UIImage.
public static func renderAppIcon(config: AppIconConfig, size: CGFloat = 1024) -> UIImage? {
let view = AppIconView(config: config, size: size)
let renderer = ImageRenderer(content: view)
renderer.scale = 1.0
return renderer.uiImage
}
/// Renders app icons at all standard iOS sizes.
/// - Parameter config: The app icon configuration.
/// - Returns: Dictionary of size to UIImage.
public static func renderAllSizes(config: AppIconConfig) -> [CGFloat: UIImage] {
var images: [CGFloat: UIImage] = [:]
for size in iOSIconSizes {
if let image = renderAppIcon(config: config, size: size) {
images[size] = image
}
}
return images
}
/// Renders a launch screen to a UIImage.
/// - Parameters:
/// - config: The launch screen configuration.
/// - size: The size to render at.
/// - Returns: A rendered UIImage.
public static func renderLaunchScreen(config: LaunchScreenConfig, size: CGSize) -> UIImage? {
let view = StaticLaunchScreenView(config: config)
.frame(width: size.width, height: size.height)
let renderer = ImageRenderer(content: view)
renderer.scale = 1.0
return renderer.uiImage
}
}
// MARK: - Icon Export View
/// A development view for previewing and exporting app icons.
/// Add this to your app during development to easily export icons.
public struct IconExportView: View {
let config: AppIconConfig
@State private var exportedMessage: String?
public init(config: AppIconConfig) {
self.config = config
}
public var body: some View {
ScrollView {
VStack(spacing: 24) {
Text("App Icon Preview")
.font(.title.bold())
// Large preview
AppIconView(config: config, size: 256)
.clipShape(.rect(cornerRadius: 256 * 0.22))
.shadow(radius: 10)
// Size variants
Text("Size Variants")
.font(.headline)
LazyVGrid(columns: [
GridItem(.adaptive(minimum: 100))
], spacing: 16) {
ForEach([180, 120, 87, 60, 40], id: \.self) { size in
VStack {
AppIconView(config: config, size: CGFloat(size))
.clipShape(.rect(cornerRadius: CGFloat(size) * 0.22))
Text("\(size)pt")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
// Export instructions
Text("Export Instructions")
.font(.headline)
.padding(.top)
VStack(alignment: .leading, spacing: 8) {
Text("1. Use Xcode's preview to screenshot these icons")
Text("2. Or use IconRenderer.renderAppIcon() in code")
Text("3. Add generated images to Assets.xcassets/AppIcon")
}
.font(.callout)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color.gray.opacity(0.1))
.clipShape(.rect(cornerRadius: 12))
if let message = exportedMessage {
Text(message)
.foregroundStyle(.green)
}
}
.padding()
}
}
}
#Preview("Icon Export") {
IconExportView(config: .baccarat)
}

View File

@ -0,0 +1,351 @@
//
// LaunchScreenView.swift
// CasinoKit
//
// A reusable launch screen design that can be customized for different casino games.
//
import SwiftUI
/// Configuration for the launch screen appearance.
public struct LaunchScreenConfig: Sendable {
public let title: String
public let subtitle: String?
public let tagline: String?
public let iconSymbols: [String]
public let primaryColor: Color
public let secondaryColor: Color
public let accentColor: Color
public let showLoadingIndicator: Bool
public init(
title: String,
subtitle: String? = nil,
tagline: String? = nil,
iconSymbols: [String] = ["suit.spade.fill", "suit.heart.fill"],
primaryColor: Color = Color(red: 0.05, green: 0.12, blue: 0.22),
secondaryColor: Color = Color(red: 0.02, green: 0.06, blue: 0.12),
accentColor: Color = .yellow,
showLoadingIndicator: Bool = false
) {
self.title = title
self.subtitle = subtitle
self.tagline = tagline
self.iconSymbols = iconSymbols
self.primaryColor = primaryColor
self.secondaryColor = secondaryColor
self.accentColor = accentColor
self.showLoadingIndicator = showLoadingIndicator
}
// MARK: - Preset Configurations
/// Baccarat game launch screen configuration.
public static let baccarat = LaunchScreenConfig(
title: "BACCARAT",
tagline: "The Classic Casino Card Game",
iconSymbols: ["suit.spade.fill", "suit.heart.fill"]
)
/// Blackjack game launch screen configuration.
public static let blackjack = LaunchScreenConfig(
title: "BLACKJACK",
subtitle: "21",
tagline: "Beat the Dealer",
iconSymbols: ["suit.club.fill", "suit.diamond.fill"]
)
/// Poker game launch screen configuration.
public static let poker = LaunchScreenConfig(
title: "POKER",
tagline: "Texas Hold'em",
iconSymbols: ["suit.diamond.fill", "suit.club.fill"],
accentColor: .red
)
}
/// A customizable launch screen view for casino games.
public struct LaunchScreenView: View {
let config: LaunchScreenConfig
@State private var logoScale: CGFloat = 0.8
@State private var logoOpacity: Double = 0
@State private var titleOffset: CGFloat = 20
@State private var titleOpacity: Double = 0
public init(config: LaunchScreenConfig) {
self.config = config
}
public var body: some View {
GeometryReader { geometry in
ZStack {
// Background gradient
backgroundGradient
// Pattern overlay
patternOverlay
.opacity(0.05)
// Decorative corner elements
cornerDecorations(in: geometry)
// Main content
VStack(spacing: 0) {
Spacer()
// Logo section
logoSection
.scaleEffect(logoScale)
.opacity(logoOpacity)
Spacer()
// Bottom tagline
if let tagline = config.tagline {
Text(tagline)
.font(.system(size: 14, weight: .medium, design: .rounded))
.foregroundStyle(.white.opacity(0.6))
.tracking(2)
.padding(.bottom, 40)
.offset(y: titleOffset)
.opacity(titleOpacity)
}
// Loading indicator
if config.showLoadingIndicator {
ProgressView()
.progressViewStyle(.circular)
.tint(config.accentColor)
.scaleEffect(1.2)
.padding(.bottom, 60)
}
}
}
}
.ignoresSafeArea()
.onAppear {
withAnimation(.easeOut(duration: 0.6)) {
logoScale = 1.0
logoOpacity = 1.0
}
withAnimation(.easeOut(duration: 0.6).delay(0.3)) {
titleOffset = 0
titleOpacity = 1.0
}
}
}
// MARK: - Background
private var backgroundGradient: some View {
LinearGradient(
colors: [
config.primaryColor,
config.secondaryColor
],
startPoint: .top,
endPoint: .bottom
)
}
private var patternOverlay: some View {
Canvas { context, size in
let spacing: CGFloat = 40
let diamondSize: CGFloat = 8
let rows = Int(size.height / spacing) + 1
let cols = Int(size.width / spacing) + 1
for row in 0..<rows {
for col in 0..<cols {
let offset: CGFloat = row % 2 == 0 ? 0 : spacing / 2
let x = CGFloat(col) * spacing + offset
let y = CGFloat(row) * spacing
let diamond = Path { path in
path.move(to: CGPoint(x: x, y: y - diamondSize))
path.addLine(to: CGPoint(x: x + diamondSize, y: y))
path.addLine(to: CGPoint(x: x, y: y + diamondSize))
path.addLine(to: CGPoint(x: x - diamondSize, y: y))
path.closeSubpath()
}
context.fill(diamond, with: .color(.white))
}
}
}
}
// MARK: - Corner Decorations
private func cornerDecorations(in geometry: GeometryProxy) -> some View {
ZStack {
// Top-left
cornerSymbol
.position(x: 50, y: 80)
// Top-right
cornerSymbol
.rotationEffect(.degrees(90))
.position(x: geometry.size.width - 50, y: 80)
// Bottom-left
cornerSymbol
.rotationEffect(.degrees(-90))
.position(x: 50, y: geometry.size.height - 80)
// Bottom-right
cornerSymbol
.rotationEffect(.degrees(180))
.position(x: geometry.size.width - 50, y: geometry.size.height - 80)
}
.opacity(0.15)
}
private var cornerSymbol: some View {
Image(systemName: "suit.spade.fill")
.font(.system(size: 30))
.foregroundStyle(config.accentColor)
}
// MARK: - Logo Section
private var logoSection: some View {
VStack(spacing: 16) {
// Card suit icons
HStack(spacing: -8) {
ForEach(config.iconSymbols.indices, id: \.self) { index in
Image(systemName: config.iconSymbols[index])
.font(.system(size: 48, weight: .bold))
.foregroundStyle(iconColor(for: index))
.shadow(color: .black.opacity(0.3), radius: 4, y: 2)
}
}
// Subtitle (e.g., "21" for Blackjack)
if let subtitle = config.subtitle {
Text(subtitle)
.font(.system(size: 72, weight: .black, design: .rounded))
.foregroundStyle(
LinearGradient(
colors: [config.accentColor, .orange],
startPoint: .top,
endPoint: .bottom
)
)
.shadow(color: .black.opacity(0.4), radius: 4, y: 2)
}
// Title
Text(config.title)
.font(.system(size: 42, weight: .black, design: .rounded))
.tracking(6)
.foregroundStyle(
LinearGradient(
colors: [config.accentColor, .orange],
startPoint: .top,
endPoint: .bottom
)
)
.shadow(color: .black.opacity(0.4), radius: 4, y: 2)
// Decorative line
HStack(spacing: 12) {
decorativeLine
Image(systemName: "diamond.fill")
.font(.system(size: 10))
.foregroundStyle(config.accentColor.opacity(0.6))
decorativeLine
}
.frame(width: 200)
}
}
private func iconColor(for index: Int) -> Color {
let symbol = config.iconSymbols[index]
if symbol.contains("heart") || symbol.contains("diamond") {
return .red
}
return .white
}
private var decorativeLine: some View {
Rectangle()
.fill(
LinearGradient(
colors: [.clear, config.accentColor.opacity(0.4), .clear],
startPoint: .leading,
endPoint: .trailing
)
)
.frame(height: 1)
}
}
// MARK: - Static Launch Screen (for LaunchScreen.storyboard alternative)
/// A static version of the launch screen without animations.
/// Use this if you need to render a static image.
public struct StaticLaunchScreenView: View {
let config: LaunchScreenConfig
public init(config: LaunchScreenConfig) {
self.config = config
}
public var body: some View {
GeometryReader { geometry in
ZStack {
// Background
LinearGradient(
colors: [config.primaryColor, config.secondaryColor],
startPoint: .top,
endPoint: .bottom
)
// Logo
VStack(spacing: 16) {
HStack(spacing: -8) {
ForEach(config.iconSymbols.indices, id: \.self) { index in
let symbol = config.iconSymbols[index]
Image(systemName: symbol)
.font(.system(size: 48, weight: .bold))
.foregroundStyle(
symbol.contains("heart") || symbol.contains("diamond") ? .red : .white
)
}
}
if let subtitle = config.subtitle {
Text(subtitle)
.font(.system(size: 72, weight: .black, design: .rounded))
.foregroundStyle(config.accentColor)
}
Text(config.title)
.font(.system(size: 42, weight: .black, design: .rounded))
.tracking(6)
.foregroundStyle(config.accentColor)
}
}
}
.ignoresSafeArea()
}
}
// MARK: - Preview
#Preview("Baccarat Launch") {
LaunchScreenView(config: .baccarat)
}
#Preview("Blackjack Launch") {
LaunchScreenView(config: .blackjack)
}
#Preview("Static Launch") {
StaticLaunchScreenView(config: .baccarat)
}