Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
6baf2924bb
commit
d2b93a019f
@ -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;
|
||||
|
||||
49
Baccarat/LaunchScreen.storyboard
Normal file
49
Baccarat/LaunchScreen.storyboard
Normal 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
130
Baccarat/Views/BrandingPreviewView.swift
Normal file
130
Baccarat/Views/BrandingPreviewView.swift
Normal 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()
|
||||
}
|
||||
|
||||
192
Baccarat/Views/IconGeneratorView.swift
Normal file
192
Baccarat/Views/IconGeneratorView.swift
Normal 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
378
CasinoKit/README.md
Normal 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.
|
||||
|
||||
@ -21,6 +21,11 @@
|
||||
// - ChipStackView, ChipOnTableView
|
||||
// - SheetContainerView, SheetSection
|
||||
|
||||
// MARK: - Branding
|
||||
// - AppIconView, AppIconConfig
|
||||
// - LaunchScreenView, LaunchScreenConfig, StaticLaunchScreenView
|
||||
// - IconRenderer, IconExportView
|
||||
|
||||
// MARK: - Theme
|
||||
// - CasinoTheme (protocol)
|
||||
// - DefaultCasinoTheme
|
||||
|
||||
@ -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" : {
|
||||
|
||||
218
CasinoKit/Sources/CasinoKit/Views/Branding/AppIconView.swift
Normal file
218
CasinoKit/Sources/CasinoKit/Views/Branding/AppIconView.swift
Normal 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)
|
||||
}
|
||||
|
||||
139
CasinoKit/Sources/CasinoKit/Views/Branding/IconRenderer.swift
Normal file
139
CasinoKit/Sources/CasinoKit/Views/Branding/IconRenderer.swift
Normal 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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user