Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
6baf2924bb
commit
d2b93a019f
@ -423,11 +423,14 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = Baccarat;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.casino-games";
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
@ -440,6 +443,7 @@
|
|||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.Baccarat;
|
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.Baccarat;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
@ -455,11 +459,14 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = Baccarat;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.casino-games";
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
@ -472,6 +479,7 @@
|
|||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.Baccarat;
|
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.Baccarat;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
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
|
// - ChipStackView, ChipOnTableView
|
||||||
// - SheetContainerView, SheetSection
|
// - SheetContainerView, SheetSection
|
||||||
|
|
||||||
|
// MARK: - Branding
|
||||||
|
// - AppIconView, AppIconConfig
|
||||||
|
// - LaunchScreenView, LaunchScreenConfig, StaticLaunchScreenView
|
||||||
|
// - IconRenderer, IconExportView
|
||||||
|
|
||||||
// MARK: - Theme
|
// MARK: - Theme
|
||||||
// - CasinoTheme (protocol)
|
// - CasinoTheme (protocol)
|
||||||
// - DefaultCasinoTheme
|
// - 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" : {
|
"Ace" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"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" : {
|
"Card face down" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -221,6 +240,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Export Instructions" : {
|
||||||
|
"comment" : "A section header describing how to export app icons.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Five" : {
|
"Five" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -485,6 +508,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Size Variants" : {
|
||||||
|
"comment" : "A heading for the different sizes of the app icon previews.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Spades" : {
|
"Spades" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"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