Add branding system with customizable launch screen and app icon
- Add AppIconView with configurable title, subtitle, icon, and colors - Add LaunchScreenView with multiple pattern styles (dots, grid, radial, none) - Add layout styles (iconAboveTitle, titleAboveIcon, iconOnly, titleOnly) - Add IconGeneratorView for exporting 1024px app icons - Add BrandingPreviewView for previewing branding assets - Add AppLaunchView wrapper for animated app launch - Add comprehensive BRANDING_GUIDE.md documentation - Remove all casino-specific references, make fully generic
This commit is contained in:
parent
9212cd4c23
commit
c7c507c8f7
231
Sources/Bedrock/Branding/AppIconView.swift
Normal file
231
Sources/Bedrock/Branding/AppIconView.swift
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
//
|
||||||
|
// AppIconView.swift
|
||||||
|
// Bedrock
|
||||||
|
//
|
||||||
|
// A reusable app icon design that can be customized for any app.
|
||||||
|
// 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.15, green: 0.20, blue: 0.30),
|
||||||
|
secondaryColor: Color = Color(red: 0.08, green: 0.10, blue: 0.18),
|
||||||
|
accentColor: Color = Color(red: 0.4, green: 0.7, blue: 1.0)
|
||||||
|
) {
|
||||||
|
self.title = title
|
||||||
|
self.subtitle = subtitle
|
||||||
|
self.iconSymbol = iconSymbol
|
||||||
|
self.primaryColor = primaryColor
|
||||||
|
self.secondaryColor = secondaryColor
|
||||||
|
self.accentColor = accentColor
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Example Configuration (for previews only)
|
||||||
|
|
||||||
|
/// Example configuration for Bedrock previews.
|
||||||
|
/// Apps should define their own configs in `BrandingConfig.swift`.
|
||||||
|
public static let example = AppIconConfig(
|
||||||
|
title: "MY APP",
|
||||||
|
iconSymbol: "star.fill"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A customizable app icon view for any app.
|
||||||
|
/// Render this view to create your app icon assets.
|
||||||
|
///
|
||||||
|
/// **Important**: This view generates a full-bleed square icon. iOS applies its own
|
||||||
|
/// superellipse mask, so decorative borders are inset to avoid clipping at the edges.
|
||||||
|
public struct AppIconView: View {
|
||||||
|
let config: AppIconConfig
|
||||||
|
let size: CGFloat
|
||||||
|
|
||||||
|
public init(config: AppIconConfig, size: CGFloat = 1024) {
|
||||||
|
self.config = config
|
||||||
|
self.size = size
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size calculations
|
||||||
|
private var iconSize: CGFloat { size * 0.35 }
|
||||||
|
private var subtitleSize: CGFloat { size * 0.25 }
|
||||||
|
|
||||||
|
/// Dynamic title size based on text length.
|
||||||
|
/// Shorter titles get larger fonts, longer titles shrink to fit within the border.
|
||||||
|
private var titleSize: CGFloat {
|
||||||
|
let baseSize = size * 0.12
|
||||||
|
let length = config.title.count
|
||||||
|
|
||||||
|
// Scale factor: full size for ≤6 chars, progressively smaller for longer
|
||||||
|
let scaleFactor: CGFloat = switch length {
|
||||||
|
case ...6: 1.0 // "SELFIE", "CAMERA"
|
||||||
|
case 7: 0.95 // "WEATHER"
|
||||||
|
case 8: 0.85 // "SETTINGS"
|
||||||
|
case 9: 0.75 // "MESSENGER"
|
||||||
|
default: 0.65 // Very long titles
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseSize * scaleFactor
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
ZStack {
|
||||||
|
// Background gradient - full bleed, no rounded corners
|
||||||
|
// iOS will apply its own superellipse mask
|
||||||
|
Rectangle()
|
||||||
|
.fill(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [config.primaryColor, config.secondaryColor],
|
||||||
|
startPoint: .topLeading,
|
||||||
|
endPoint: .bottomTrailing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Subtle pattern overlay
|
||||||
|
DotPatternOverlay(size: size)
|
||||||
|
.opacity(0.06)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
if let subtitle = config.subtitle {
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.system(size: subtitleSize, weight: .black, design: .rounded))
|
||||||
|
.foregroundStyle(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [config.accentColor, config.accentColor.opacity(0.7)],
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dot pattern overlay for the icon background.
|
||||||
|
private struct DotPatternOverlay: View {
|
||||||
|
let size: CGFloat
|
||||||
|
|
||||||
|
private var spacing: CGFloat { size * 0.08 }
|
||||||
|
private var dotRadius: CGFloat { size * 0.012 }
|
||||||
|
|
||||||
|
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 dot = Path(ellipseIn: CGRect(
|
||||||
|
x: x - dotRadius,
|
||||||
|
y: y - dotRadius,
|
||||||
|
width: dotRadius * 2,
|
||||||
|
height: dotRadius * 2
|
||||||
|
))
|
||||||
|
|
||||||
|
context.fill(dot, with: .color(.white))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Previews
|
||||||
|
|
||||||
|
#Preview("App Icon") {
|
||||||
|
AppIconView(config: .example, size: 512)
|
||||||
|
.clipShape(.rect(cornerRadius: 512 * 0.22))
|
||||||
|
.padding()
|
||||||
|
.background(Color.gray)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Title Scaling") {
|
||||||
|
let shortTitle = AppIconConfig(title: "CAMERA", iconSymbol: "camera.fill")
|
||||||
|
let mediumTitle = AppIconConfig(title: "SETTINGS", iconSymbol: "gearshape.fill")
|
||||||
|
let longTitle = AppIconConfig(
|
||||||
|
title: "MESSENGER",
|
||||||
|
subtitle: "PRO",
|
||||||
|
iconSymbol: "message.fill",
|
||||||
|
primaryColor: Color(red: 0.20, green: 0.45, blue: 0.70),
|
||||||
|
secondaryColor: Color(red: 0.10, green: 0.25, blue: 0.45)
|
||||||
|
)
|
||||||
|
|
||||||
|
return HStack(spacing: 20) {
|
||||||
|
VStack {
|
||||||
|
AppIconView(config: shortTitle, size: 180)
|
||||||
|
.clipShape(.rect(cornerRadius: 180 * 0.22))
|
||||||
|
Text("6 chars").font(.caption)
|
||||||
|
}
|
||||||
|
VStack {
|
||||||
|
AppIconView(config: mediumTitle, size: 180)
|
||||||
|
.clipShape(.rect(cornerRadius: 180 * 0.22))
|
||||||
|
Text("8 chars").font(.caption)
|
||||||
|
}
|
||||||
|
VStack {
|
||||||
|
AppIconView(config: longTitle, size: 180)
|
||||||
|
.clipShape(.rect(cornerRadius: 180 * 0.22))
|
||||||
|
Text("9 chars").font(.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.gray)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("All Sizes") {
|
||||||
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))], spacing: 20) {
|
||||||
|
ForEach([180, 120, 87, 60, 40], id: \.self) { size in
|
||||||
|
VStack {
|
||||||
|
AppIconView(config: .example, size: CGFloat(size))
|
||||||
|
.clipShape(.rect(cornerRadius: CGFloat(size) * 0.22))
|
||||||
|
Text("\(size)px").font(.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.gray)
|
||||||
|
}
|
||||||
|
|
||||||
64
Sources/Bedrock/Branding/AppLaunchView.swift
Normal file
64
Sources/Bedrock/Branding/AppLaunchView.swift
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
//
|
||||||
|
// AppLaunchView.swift
|
||||||
|
// Bedrock
|
||||||
|
//
|
||||||
|
// A wrapper view that shows an animated launch screen before transitioning
|
||||||
|
// to the main app content. Use this to create seamless animated launches.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// A wrapper that shows an animated launch screen before the main content.
|
||||||
|
///
|
||||||
|
/// Usage:
|
||||||
|
/// ```swift
|
||||||
|
/// AppLaunchView(config: .baccarat) {
|
||||||
|
/// ContentView()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
public struct AppLaunchView<Content: View>: View {
|
||||||
|
let config: LaunchScreenConfig
|
||||||
|
let content: () -> Content
|
||||||
|
|
||||||
|
@State private var showLaunchScreen = true
|
||||||
|
|
||||||
|
/// Creates a launch wrapper.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - config: The launch screen configuration.
|
||||||
|
/// - content: The main app content to show after the launch animation.
|
||||||
|
public init(
|
||||||
|
config: LaunchScreenConfig,
|
||||||
|
@ViewBuilder content: @escaping () -> Content
|
||||||
|
) {
|
||||||
|
self.config = config
|
||||||
|
self.content = content
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
ZStack {
|
||||||
|
// Main content (always rendered underneath)
|
||||||
|
content()
|
||||||
|
|
||||||
|
// Launch screen overlay
|
||||||
|
if showLaunchScreen {
|
||||||
|
LaunchScreenView(config: config)
|
||||||
|
.transition(.opacity)
|
||||||
|
.zIndex(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
// Wait for launch animation to complete, then fade out
|
||||||
|
try? await Task.sleep(for: .seconds(2.0))
|
||||||
|
withAnimation(.easeOut(duration: 0.5)) {
|
||||||
|
showLaunchScreen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
AppLaunchView(config: .example) {
|
||||||
|
Text("Main App Content")
|
||||||
|
.font(.largeTitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
569
Sources/Bedrock/Branding/BRANDING_GUIDE.md
Normal file
569
Sources/Bedrock/Branding/BRANDING_GUIDE.md
Normal file
@ -0,0 +1,569 @@
|
|||||||
|
# Bedrock Branding Implementation Guide
|
||||||
|
|
||||||
|
A comprehensive guide to implementing the Bedrock branding system (app icon and launch screen) in your iOS app.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Overview](#overview)
|
||||||
|
2. [What's Included](#whats-included)
|
||||||
|
3. [Step 1: Create BrandingConfig.swift](#step-1-create-brandingconfigswift)
|
||||||
|
4. [Step 2: Add Launch Screen to App Entry Point](#step-2-add-launch-screen-to-app-entry-point)
|
||||||
|
5. [Step 3: Set Launch Screen Background Color](#step-3-set-launch-screen-background-color)
|
||||||
|
6. [Step 4: Add Branding Tools to Settings (Optional)](#step-4-add-branding-tools-to-settings-optional)
|
||||||
|
7. [Step 5: Generate Your App Icon](#step-5-generate-your-app-icon)
|
||||||
|
8. [Step 6: Add Icon to Xcode Assets](#step-6-add-icon-to-xcode-assets)
|
||||||
|
9. [Configuration Reference](#configuration-reference)
|
||||||
|
10. [Complete Example](#complete-example)
|
||||||
|
11. [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Bedrock branding system provides a fully customizable app icon and launch screen that can be configured for any type of app. All visual elements are configurable through Swift code.
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
- **Customizable gradients**: Primary and secondary colors for backgrounds
|
||||||
|
- **Configurable icons**: Use any SF Symbols for your app identity
|
||||||
|
- **Multiple pattern styles**: Dots, grid, radial glow, or no pattern
|
||||||
|
- **Layout flexibility**: Icon above title, title above icon, icon only, or title only
|
||||||
|
- **Animated launch**: Smooth fade-in animations with configurable timing
|
||||||
|
- **Icon generator**: Built-in tool to export 1024×1024 PNG for App Store
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's Included
|
||||||
|
|
||||||
|
The branding system consists of these files in `Bedrock/Sources/Bedrock/Branding/`:
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `AppIconView.swift` | Renders the app icon design |
|
||||||
|
| `LaunchScreenView.swift` | Animated launch screen view |
|
||||||
|
| `AppLaunchView.swift` | Wrapper that shows launch screen before main content |
|
||||||
|
| `IconGeneratorView.swift` | Development tool to export icon images |
|
||||||
|
| `IconRenderer.swift` | Utility to render views to images |
|
||||||
|
| `BrandingPreviewView.swift` | Preview tool for icons and launch screens |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Create BrandingConfig.swift
|
||||||
|
|
||||||
|
Create a new Swift file in your app's `Shared/` folder called `BrandingConfig.swift`. This file defines your app's branding.
|
||||||
|
|
||||||
|
### Template
|
||||||
|
|
||||||
|
```swift
|
||||||
|
//
|
||||||
|
// BrandingConfig.swift
|
||||||
|
// YourApp
|
||||||
|
//
|
||||||
|
// App-specific branding configurations for icons and launch screens.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Bedrock
|
||||||
|
|
||||||
|
// MARK: - App Branding Colors
|
||||||
|
|
||||||
|
extension Color {
|
||||||
|
/// Your app's branding colors for icon and launch screen.
|
||||||
|
enum Branding {
|
||||||
|
/// Primary gradient color (top/leading).
|
||||||
|
static let primary = Color(red: 0.3, green: 0.5, blue: 0.8)
|
||||||
|
|
||||||
|
/// Secondary gradient color (bottom/trailing).
|
||||||
|
static let secondary = Color(red: 0.15, green: 0.3, blue: 0.5)
|
||||||
|
|
||||||
|
/// Accent color for icons and highlights.
|
||||||
|
static let accent = Color.white
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - App Icon Configuration
|
||||||
|
|
||||||
|
extension AppIconConfig {
|
||||||
|
/// Your app's icon configuration.
|
||||||
|
static let yourApp = AppIconConfig(
|
||||||
|
title: "YOUR APP",
|
||||||
|
subtitle: nil, // Optional: text below icon
|
||||||
|
iconSymbol: "star.fill", // SF Symbol name
|
||||||
|
primaryColor: Color.Branding.primary,
|
||||||
|
secondaryColor: Color.Branding.secondary,
|
||||||
|
accentColor: Color.Branding.accent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Launch Screen Configuration
|
||||||
|
|
||||||
|
extension LaunchScreenConfig {
|
||||||
|
/// Your app's launch screen configuration.
|
||||||
|
static let yourApp = LaunchScreenConfig(
|
||||||
|
title: "YOUR APP",
|
||||||
|
tagline: "Your tagline here",
|
||||||
|
iconSymbols: ["star.fill"],
|
||||||
|
primaryColor: Color.Branding.primary,
|
||||||
|
secondaryColor: Color.Branding.secondary,
|
||||||
|
accentColor: Color.Branding.accent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Add Launch Screen to App Entry Point
|
||||||
|
|
||||||
|
Update your `@main` App struct to wrap your content with `AppLaunchView`.
|
||||||
|
|
||||||
|
### Before
|
||||||
|
|
||||||
|
```swift
|
||||||
|
@main
|
||||||
|
struct YourApp: App {
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
ContentView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### After
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import SwiftUI
|
||||||
|
import Bedrock
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct YourApp: App {
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
AppLaunchView(config: .yourApp) {
|
||||||
|
ContentView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**What this does:**
|
||||||
|
- Shows an animated launch screen for ~2 seconds
|
||||||
|
- Fades smoothly into your main content
|
||||||
|
- Creates a polished, professional app opening experience
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Set Launch Screen Background Color
|
||||||
|
|
||||||
|
To prevent a white flash before the SwiftUI launch screen appears, you need to set the system launch screen background color to match your branding.
|
||||||
|
|
||||||
|
### 3.1 Create the Color Asset
|
||||||
|
|
||||||
|
Create a folder in your asset catalog:
|
||||||
|
```
|
||||||
|
YourApp/Resources/Assets.xcassets/LaunchBackground.colorset/Contents.json
|
||||||
|
```
|
||||||
|
|
||||||
|
With this content (update RGB values to match your `Color.Branding.primary`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.800",
|
||||||
|
"green" : "0.500",
|
||||||
|
"red" : "0.300"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Update Xcode Project Settings
|
||||||
|
|
||||||
|
In your `project.pbxproj`, add this line after `INFOPLIST_KEY_UILaunchScreen_Generation = YES;` in **both** Debug and Release build configurations:
|
||||||
|
|
||||||
|
```
|
||||||
|
"INFOPLIST_KEY_UILaunchScreen_BackgroundColor" = LaunchBackground;
|
||||||
|
```
|
||||||
|
|
||||||
|
Or in Xcode:
|
||||||
|
1. Select your target → Build Settings
|
||||||
|
2. Search for "Launch Screen"
|
||||||
|
3. Set "Asset Catalog Launch Image Set Name" or add User-Defined setting
|
||||||
|
|
||||||
|
**Important:** After making this change:
|
||||||
|
1. Clean build (Cmd+Shift+K)
|
||||||
|
2. Delete app from simulator/device
|
||||||
|
3. Build and run again
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: Add Branding Tools to Settings (Optional)
|
||||||
|
|
||||||
|
Add debug tools to your settings view for generating and previewing icons during development.
|
||||||
|
|
||||||
|
### Add to SettingsView
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import SwiftUI
|
||||||
|
import Bedrock
|
||||||
|
|
||||||
|
struct SettingsView: View {
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
List {
|
||||||
|
// ... your normal settings ...
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Section("Debug") {
|
||||||
|
NavigationLink("Icon Generator") {
|
||||||
|
IconGeneratorView(config: .yourApp, appName: "YourApp")
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink("Branding Preview") {
|
||||||
|
BrandingPreviewView(
|
||||||
|
iconConfig: .yourApp,
|
||||||
|
launchConfig: .yourApp,
|
||||||
|
appName: "YourApp"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** Wrap in `#if DEBUG` so these tools are excluded from App Store builds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5: Generate Your App Icon
|
||||||
|
|
||||||
|
### Using IconGeneratorView (Recommended)
|
||||||
|
|
||||||
|
1. **Build and run your app** in DEBUG mode
|
||||||
|
2. **Open Settings** → Debug section
|
||||||
|
3. **Tap "Icon Generator"**
|
||||||
|
4. **Tap "Generate & Save Icon"**
|
||||||
|
5. **Wait for confirmation**: "✅ Icon saved to Documents folder!"
|
||||||
|
|
||||||
|
### Retrieve the Icon
|
||||||
|
|
||||||
|
**On Simulator:**
|
||||||
|
1. Open Finder
|
||||||
|
2. Go to: `~/Library/Developer/CoreSimulator/Devices/`
|
||||||
|
3. Find your simulator device folder (sorted by date)
|
||||||
|
4. Navigate to: `data/Containers/Data/Application/[YourApp-UUID]/Documents/`
|
||||||
|
5. Copy `AppIcon.png`
|
||||||
|
|
||||||
|
**On Physical Device:**
|
||||||
|
1. Open **Files** app on your device
|
||||||
|
2. Navigate to: **On My iPhone** → **YourApp**
|
||||||
|
3. Find `AppIcon.png`
|
||||||
|
4. **AirDrop** or **share** to your Mac
|
||||||
|
|
||||||
|
**Alternative (Xcode):**
|
||||||
|
1. Go to **Window** → **Devices and Simulators**
|
||||||
|
2. Select your device/simulator
|
||||||
|
3. Find your app → Click **⚙️ gear** → **Download Container**
|
||||||
|
4. Right-click downloaded file → **Show Package Contents**
|
||||||
|
5. Navigate to `AppData/Documents/` and copy `AppIcon.png`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 6: Add Icon to Xcode Assets
|
||||||
|
|
||||||
|
1. **Open your Xcode project**
|
||||||
|
2. **Navigate to** `Assets.xcassets` → `AppIcon`
|
||||||
|
3. **Drag `AppIcon.png`** into the **1024×1024** slot
|
||||||
|
4. Xcode automatically generates all required sizes
|
||||||
|
|
||||||
|
**Verify:**
|
||||||
|
1. Clean build and run
|
||||||
|
2. Check home screen for new icon
|
||||||
|
3. If unchanged, delete app and reinstall
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Reference
|
||||||
|
|
||||||
|
### AppIconConfig
|
||||||
|
|
||||||
|
| Property | Type | Default | Description |
|
||||||
|
|----------|------|---------|-------------|
|
||||||
|
| `title` | `String` | Required | App name (uppercase recommended) |
|
||||||
|
| `subtitle` | `String?` | `nil` | Optional text below icon |
|
||||||
|
| `iconSymbol` | `String` | Required | SF Symbol name |
|
||||||
|
| `primaryColor` | `Color` | Blue | Top-left gradient color |
|
||||||
|
| `secondaryColor` | `Color` | Dark blue | Bottom-right gradient color |
|
||||||
|
| `accentColor` | `Color` | Light blue | Icon and text highlight color |
|
||||||
|
|
||||||
|
### LaunchScreenConfig
|
||||||
|
|
||||||
|
| Property | Type | Default | Description |
|
||||||
|
|----------|------|---------|-------------|
|
||||||
|
| `title` | `String` | Required | App name displayed on launch |
|
||||||
|
| `subtitle` | `String?` | `nil` | Large text (like "PRO" or version) |
|
||||||
|
| `tagline` | `String?` | `nil` | Small text at bottom of screen |
|
||||||
|
| `iconSymbols` | `[String]` | `["star.fill"]` | Array of SF Symbol names |
|
||||||
|
| `cornerSymbol` | `String?` | `nil` | Symbol for corner decorations (nil = none) |
|
||||||
|
| `decorativeSymbol` | `String?` | `"circle.fill"` | Symbol in decorative line (nil = hide line) |
|
||||||
|
| `patternStyle` | `LaunchPatternStyle` | `.dots` | Background pattern style |
|
||||||
|
| `layoutStyle` | `LaunchLayoutStyle` | `.iconAboveTitle` | Content layout arrangement |
|
||||||
|
| `primaryColor` | `Color` | Blue-gray | Top gradient color |
|
||||||
|
| `secondaryColor` | `Color` | Dark blue | Bottom gradient color |
|
||||||
|
| `accentColor` | `Color` | Light blue | Icons and highlights |
|
||||||
|
| `titleColor` | `Color` | `.white` | Title text color |
|
||||||
|
| `iconSize` | `CGFloat` | `48` | Size of icon symbols |
|
||||||
|
| `titleSize` | `CGFloat` | `42` | Size of title text |
|
||||||
|
| `subtitleSize` | `CGFloat` | `72` | Size of subtitle text |
|
||||||
|
| `iconSpacing` | `CGFloat` | `8` | Spacing between icons |
|
||||||
|
| `animationDuration` | `Double` | `0.6` | Fade-in animation duration |
|
||||||
|
| `showLoadingIndicator` | `Bool` | `false` | Show spinner at bottom |
|
||||||
|
|
||||||
|
### LaunchPatternStyle
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `.none` | Clean background, no pattern |
|
||||||
|
| `.dots` | Subtle dot pattern (default) |
|
||||||
|
| `.grid` | Grid lines pattern |
|
||||||
|
| `.radial` | Radial gradient glow from center |
|
||||||
|
|
||||||
|
### LaunchLayoutStyle
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `.iconAboveTitle` | Icons at top, title below (default) |
|
||||||
|
| `.titleAboveIcon` | Title at top, icons below |
|
||||||
|
| `.iconOnly` | Only show icons, no text |
|
||||||
|
| `.titleOnly` | Only show text, no icons |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Example
|
||||||
|
|
||||||
|
Here's a full example for a camera app:
|
||||||
|
|
||||||
|
### File: `YourApp/Shared/BrandingConfig.swift`
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import SwiftUI
|
||||||
|
import Bedrock
|
||||||
|
|
||||||
|
// MARK: - App Branding Colors
|
||||||
|
|
||||||
|
extension Color {
|
||||||
|
enum Branding {
|
||||||
|
// Vibrant magenta/rose gradient
|
||||||
|
static let primary = Color(red: 0.85, green: 0.25, blue: 0.45)
|
||||||
|
static let secondary = Color(red: 0.45, green: 0.12, blue: 0.35)
|
||||||
|
static let accent = Color.white
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - App Icon Configuration
|
||||||
|
|
||||||
|
extension AppIconConfig {
|
||||||
|
static let myCamera = AppIconConfig(
|
||||||
|
title: "CAMERA",
|
||||||
|
subtitle: "PRO",
|
||||||
|
iconSymbol: "camera.fill",
|
||||||
|
primaryColor: Color.Branding.primary,
|
||||||
|
secondaryColor: Color.Branding.secondary,
|
||||||
|
accentColor: Color.Branding.accent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Launch Screen Configuration
|
||||||
|
|
||||||
|
extension LaunchScreenConfig {
|
||||||
|
static let myCamera = LaunchScreenConfig(
|
||||||
|
title: "CAMERA PRO",
|
||||||
|
tagline: "Capture the Moment",
|
||||||
|
iconSymbols: ["camera.fill", "sparkles"],
|
||||||
|
cornerSymbol: "sparkle", // Sparkles in corners
|
||||||
|
decorativeSymbol: "circle.fill", // Circle in decorative line
|
||||||
|
patternStyle: .radial, // Radial glow effect
|
||||||
|
layoutStyle: .iconAboveTitle,
|
||||||
|
primaryColor: Color.Branding.primary,
|
||||||
|
secondaryColor: Color.Branding.secondary,
|
||||||
|
accentColor: Color.Branding.accent,
|
||||||
|
titleColor: .white,
|
||||||
|
iconSize: 52,
|
||||||
|
titleSize: 38,
|
||||||
|
iconSpacing: 12,
|
||||||
|
animationDuration: 0.6
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### File: `YourApp/App/MyCameraApp.swift`
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import SwiftUI
|
||||||
|
import Bedrock
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct MyCameraApp: App {
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
AppLaunchView(config: .myCamera) {
|
||||||
|
ContentView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### White flash before launch screen
|
||||||
|
|
||||||
|
**Cause:** iOS system launch screen doesn't match your branding colors.
|
||||||
|
|
||||||
|
**Solution:** Follow [Step 3](#step-3-set-launch-screen-background-color) to add `LaunchBackground` color to your asset catalog and configure the project build settings.
|
||||||
|
|
||||||
|
### Can't find types like `AppIconConfig`
|
||||||
|
|
||||||
|
**Solution:** Make sure you have `import Bedrock` at the top of your file.
|
||||||
|
|
||||||
|
### Launch screen doesn't appear
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
- Verify `AppLaunchView` wraps your content in the App struct
|
||||||
|
- Check that you're using the correct config name (e.g., `.myCamera`)
|
||||||
|
- Ensure the config extension is defined in `BrandingConfig.swift`
|
||||||
|
|
||||||
|
### Icon looks different than preview
|
||||||
|
|
||||||
|
**Explanation:** iOS applies a superellipse mask to all app icons.
|
||||||
|
|
||||||
|
**Solution:** Don't add your own rounded corners—iOS does this automatically.
|
||||||
|
|
||||||
|
### "Icon saved" but can't find file
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Open **Files** app on device/simulator
|
||||||
|
2. Navigate to: **On My iPhone/iPad** → **[Your App Name]**
|
||||||
|
3. If folder doesn't exist, the app may need Files access
|
||||||
|
|
||||||
|
**Alternative:** Use Xcode's Devices and Simulators window to download the app container.
|
||||||
|
|
||||||
|
### Icon doesn't update in simulator
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Clean build folder: Product → Clean Build Folder (Cmd+Shift+K)
|
||||||
|
2. Delete app from simulator
|
||||||
|
3. Rebuild and run
|
||||||
|
|
||||||
|
### DEBUG section not showing in settings
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
- Ensure you're running a DEBUG build (not Release)
|
||||||
|
- Check that code is wrapped in `#if DEBUG ... #endif`
|
||||||
|
- Verify your settings view is inside a `NavigationStack`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Color Palette Ideas
|
||||||
|
|
||||||
|
### Professional Blue
|
||||||
|
```swift
|
||||||
|
primaryColor: Color(red: 0.15, green: 0.30, blue: 0.55)
|
||||||
|
secondaryColor: Color(red: 0.08, green: 0.15, blue: 0.30)
|
||||||
|
accentColor: .white
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vibrant Pink/Magenta
|
||||||
|
```swift
|
||||||
|
primaryColor: Color(red: 0.85, green: 0.25, blue: 0.45)
|
||||||
|
secondaryColor: Color(red: 0.45, green: 0.12, blue: 0.35)
|
||||||
|
accentColor: .white
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nature Green
|
||||||
|
```swift
|
||||||
|
primaryColor: Color(red: 0.20, green: 0.55, blue: 0.35)
|
||||||
|
secondaryColor: Color(red: 0.10, green: 0.30, blue: 0.20)
|
||||||
|
accentColor: Color(red: 0.85, green: 0.95, blue: 0.85)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Warm Orange/Gold
|
||||||
|
```swift
|
||||||
|
primaryColor: Color(red: 0.95, green: 0.60, blue: 0.20)
|
||||||
|
secondaryColor: Color(red: 0.70, green: 0.35, blue: 0.10)
|
||||||
|
accentColor: .white
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dark/Minimal
|
||||||
|
```swift
|
||||||
|
primaryColor: Color(red: 0.12, green: 0.12, blue: 0.15)
|
||||||
|
secondaryColor: .black
|
||||||
|
accentColor: .white
|
||||||
|
patternStyle: .none
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SF Symbol Recommendations
|
||||||
|
|
||||||
|
### Photography/Camera
|
||||||
|
- `camera.fill`, `camera.circle.fill`
|
||||||
|
- `photo.fill`, `photo.stack.fill`
|
||||||
|
- `sparkles`, `wand.and.stars`
|
||||||
|
|
||||||
|
### Social/Communication
|
||||||
|
- `message.fill`, `bubble.left.fill`
|
||||||
|
- `person.fill`, `person.2.fill`
|
||||||
|
- `heart.fill`, `star.fill`
|
||||||
|
|
||||||
|
### Productivity
|
||||||
|
- `doc.fill`, `folder.fill`
|
||||||
|
- `checkmark.circle.fill`
|
||||||
|
- `calendar`, `clock.fill`
|
||||||
|
|
||||||
|
### Music/Media
|
||||||
|
- `music.note`, `waveform`
|
||||||
|
- `play.fill`, `headphones`
|
||||||
|
- `mic.fill`, `speaker.wave.2.fill`
|
||||||
|
|
||||||
|
### Utility
|
||||||
|
- `gearshape.fill`, `wrench.fill`
|
||||||
|
- `magnifyingglass`, `location.fill`
|
||||||
|
- `bolt.fill`, `battery.100`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary Checklist
|
||||||
|
|
||||||
|
- [ ] Create `BrandingConfig.swift` with your app's configurations
|
||||||
|
- [ ] Add `AppLaunchView` wrapper to your App entry point
|
||||||
|
- [ ] Create `LaunchBackground.colorset` in asset catalog matching primary color
|
||||||
|
- [ ] Add `INFOPLIST_KEY_UILaunchScreen_BackgroundColor` to project settings
|
||||||
|
- [ ] (Optional) Add debug section to settings with `IconGeneratorView` and `BrandingPreviewView`
|
||||||
|
- [ ] Build and run in DEBUG mode
|
||||||
|
- [ ] Generate icon using Icon Generator tool
|
||||||
|
- [ ] Retrieve icon PNG from device/simulator
|
||||||
|
- [ ] Add 1024×1024 PNG to `Assets.xcassets/AppIcon`
|
||||||
|
- [ ] Clean build and reinstall to verify icon and launch screen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Happy Branding! 🎨✨**
|
||||||
91
Sources/Bedrock/Branding/BrandingPreviewView.swift
Normal file
91
Sources/Bedrock/Branding/BrandingPreviewView.swift
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
//
|
||||||
|
// BrandingPreviewView.swift
|
||||||
|
// Bedrock
|
||||||
|
//
|
||||||
|
// Development view for previewing and exporting app icons and launch screens.
|
||||||
|
// Access this during development to generate icon assets.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Preview view for app branding assets.
|
||||||
|
/// Use this during development to preview icons and launch screens.
|
||||||
|
public struct BrandingPreviewView: View {
|
||||||
|
let iconConfig: AppIconConfig
|
||||||
|
let launchConfig: LaunchScreenConfig
|
||||||
|
let appName: String
|
||||||
|
|
||||||
|
// Development view: fixed sizes acceptable
|
||||||
|
private let largePreviewSize: CGFloat = 300
|
||||||
|
private let iconCornerRadiusRatio: CGFloat = 0.22
|
||||||
|
|
||||||
|
/// Creates a branding preview view.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - iconConfig: The app icon configuration for this app.
|
||||||
|
/// - launchConfig: The launch screen configuration for this app.
|
||||||
|
/// - appName: The app name for display purposes.
|
||||||
|
public init(
|
||||||
|
iconConfig: AppIconConfig,
|
||||||
|
launchConfig: LaunchScreenConfig,
|
||||||
|
appName: String
|
||||||
|
) {
|
||||||
|
self.iconConfig = iconConfig
|
||||||
|
self.launchConfig = launchConfig
|
||||||
|
self.appName = appName
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
TabView {
|
||||||
|
// App Icon Preview
|
||||||
|
ScrollView {
|
||||||
|
VStack(spacing: 32) {
|
||||||
|
Text("App Icon")
|
||||||
|
.font(.largeTitle.bold())
|
||||||
|
|
||||||
|
AppIconView(config: iconConfig, size: largePreviewSize)
|
||||||
|
.clipShape(.rect(cornerRadius: largePreviewSize * iconCornerRadiusRatio))
|
||||||
|
.shadow(radius: 20)
|
||||||
|
|
||||||
|
Text("1024 × 1024px")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
instructionsSection
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.tabItem {
|
||||||
|
Label("Icon", systemImage: "app.fill")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch Screen Preview
|
||||||
|
LaunchScreenView(config: launchConfig)
|
||||||
|
.tabItem {
|
||||||
|
Label("Launch", systemImage: "rectangle.portrait.fill")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var instructionsSection: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text("To Export")
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
Text("Use the Icon Generator in Settings → DEBUG to save the 1024px icon to the Files app, then add it to Xcode's 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(
|
||||||
|
iconConfig: .example,
|
||||||
|
launchConfig: .example,
|
||||||
|
appName: "MyApp"
|
||||||
|
)
|
||||||
|
}
|
||||||
187
Sources/Bedrock/Branding/IconGeneratorView.swift
Normal file
187
Sources/Bedrock/Branding/IconGeneratorView.swift
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
//
|
||||||
|
// IconGeneratorView.swift
|
||||||
|
// Bedrock
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
/// A development view that generates and saves app icon images.
|
||||||
|
/// After running, find the icons in Files app → On My iPhone → [App Name]
|
||||||
|
public struct IconGeneratorView: View {
|
||||||
|
let config: AppIconConfig
|
||||||
|
let appName: String
|
||||||
|
|
||||||
|
@State private var status: String = "Tap the button to generate the icon"
|
||||||
|
@State private var isGenerating = false
|
||||||
|
@State private var generatedIcon: GeneratedIconInfo?
|
||||||
|
|
||||||
|
// Development view: fixed sizes acceptable
|
||||||
|
private let previewSize: CGFloat = 200
|
||||||
|
private let iconCornerRadiusRatio: CGFloat = 0.22
|
||||||
|
|
||||||
|
/// Creates a new icon generator view.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - config: The app icon configuration to use for rendering.
|
||||||
|
/// - appName: The app name for display in instructions (e.g., "SelfieCam", "MyApp").
|
||||||
|
public init(config: AppIconConfig, appName: String) {
|
||||||
|
self.config = config
|
||||||
|
self.appName = appName
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
ScrollView {
|
||||||
|
VStack(spacing: 24) {
|
||||||
|
// Preview
|
||||||
|
AppIconView(config: config, size: previewSize)
|
||||||
|
.clipShape(.rect(cornerRadius: previewSize * iconCornerRadiusRatio))
|
||||||
|
.shadow(radius: 10)
|
||||||
|
|
||||||
|
Text("App Icon Preview")
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
// Generate button
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
await generateIcon()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
if isGenerating {
|
||||||
|
ProgressView()
|
||||||
|
.tint(.white)
|
||||||
|
}
|
||||||
|
Text(isGenerating ? "Generating..." : "Generate & Save Icon")
|
||||||
|
}
|
||||||
|
.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 icon confirmation
|
||||||
|
if let icon = generatedIcon {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.foregroundStyle(.green)
|
||||||
|
Text(icon.filename)
|
||||||
|
.font(.callout.monospaced())
|
||||||
|
Spacer()
|
||||||
|
Text("\(Int(icon.size))px")
|
||||||
|
.font(.callout)
|
||||||
|
.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 → \(appName)")
|
||||||
|
instructionRow(number: 3, text: "Find AppIcon.png (1024×1024)")
|
||||||
|
instructionRow(number: 4, text: "AirDrop or share to your Mac")
|
||||||
|
instructionRow(number: 5, text: "Drag into Xcode's Assets.xcassets/AppIcon")
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
Text("Note: iOS uses a single 1024px icon")
|
||||||
|
.font(.subheadline.bold())
|
||||||
|
Text("Xcode automatically generates all required sizes from the 1024px source.")
|
||||||
|
.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 generateIcon() async {
|
||||||
|
isGenerating = true
|
||||||
|
generatedIcon = nil
|
||||||
|
status = "Generating icon..."
|
||||||
|
|
||||||
|
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||||
|
|
||||||
|
// Render the 1024px icon (the only size needed for modern iOS)
|
||||||
|
let view = AppIconView(config: config, size: 1024)
|
||||||
|
let renderer = ImageRenderer(content: view)
|
||||||
|
renderer.scale = 1.0
|
||||||
|
|
||||||
|
if let uiImage = renderer.uiImage,
|
||||||
|
let data = uiImage.pngData() {
|
||||||
|
let filename = "AppIcon.png"
|
||||||
|
let fileURL = documentsPath.appending(path: filename)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try data.write(to: fileURL)
|
||||||
|
generatedIcon = GeneratedIconInfo(filename: filename, size: 1024)
|
||||||
|
status = "✅ Icon saved to Documents folder!\nOpen Files app to find it."
|
||||||
|
} catch {
|
||||||
|
status = "Error saving icon: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
status = "⚠️ Failed to render icon"
|
||||||
|
}
|
||||||
|
|
||||||
|
isGenerating = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about a generated icon file.
|
||||||
|
public struct GeneratedIconInfo: Identifiable, Sendable {
|
||||||
|
public let id = UUID()
|
||||||
|
public let filename: String
|
||||||
|
public let size: CGFloat
|
||||||
|
|
||||||
|
public init(filename: String, size: CGFloat) {
|
||||||
|
self.filename = filename
|
||||||
|
self.size = size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
IconGeneratorView(config: .example, appName: "MyApp")
|
||||||
|
}
|
||||||
139
Sources/Bedrock/Branding/IconRenderer.swift
Normal file
139
Sources/Bedrock/Branding/IconRenderer.swift
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
//
|
||||||
|
// IconRenderer.swift
|
||||||
|
// Bedrock
|
||||||
|
//
|
||||||
|
// 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: .example)
|
||||||
|
}
|
||||||
|
|
||||||
523
Sources/Bedrock/Branding/LaunchScreenView.swift
Normal file
523
Sources/Bedrock/Branding/LaunchScreenView.swift
Normal file
@ -0,0 +1,523 @@
|
|||||||
|
//
|
||||||
|
// LaunchScreenView.swift
|
||||||
|
// Bedrock
|
||||||
|
//
|
||||||
|
// A reusable launch screen design that can be customized for any app.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// MARK: - Pattern Style
|
||||||
|
|
||||||
|
/// The background pattern style for launch screens.
|
||||||
|
public enum LaunchPatternStyle: Sendable {
|
||||||
|
/// No pattern overlay.
|
||||||
|
case none
|
||||||
|
/// Subtle dot pattern.
|
||||||
|
case dots
|
||||||
|
/// Grid lines pattern.
|
||||||
|
case grid
|
||||||
|
/// Radial gradient overlay for depth.
|
||||||
|
case radial
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Layout Style
|
||||||
|
|
||||||
|
/// The layout style for the launch screen content.
|
||||||
|
public enum LaunchLayoutStyle: Sendable {
|
||||||
|
/// Icon above title (default).
|
||||||
|
case iconAboveTitle
|
||||||
|
/// Title above icon.
|
||||||
|
case titleAboveIcon
|
||||||
|
/// Icon only, no title.
|
||||||
|
case iconOnly
|
||||||
|
/// Title only, no icon.
|
||||||
|
case titleOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Configuration
|
||||||
|
|
||||||
|
/// Configuration for the launch screen appearance.
|
||||||
|
public struct LaunchScreenConfig: Sendable {
|
||||||
|
// MARK: Content
|
||||||
|
public let title: String
|
||||||
|
public let subtitle: String?
|
||||||
|
public let tagline: String?
|
||||||
|
public let iconSymbols: [String]
|
||||||
|
|
||||||
|
// MARK: Decorations
|
||||||
|
public let cornerSymbol: String?
|
||||||
|
public let decorativeSymbol: String?
|
||||||
|
public let patternStyle: LaunchPatternStyle
|
||||||
|
public let layoutStyle: LaunchLayoutStyle
|
||||||
|
|
||||||
|
// MARK: Colors
|
||||||
|
public let primaryColor: Color
|
||||||
|
public let secondaryColor: Color
|
||||||
|
public let accentColor: Color
|
||||||
|
public let titleColor: Color
|
||||||
|
|
||||||
|
// MARK: Sizing
|
||||||
|
public let iconSize: CGFloat
|
||||||
|
public let titleSize: CGFloat
|
||||||
|
public let subtitleSize: CGFloat
|
||||||
|
public let iconSpacing: CGFloat
|
||||||
|
|
||||||
|
// MARK: Animation
|
||||||
|
public let animationDuration: Double
|
||||||
|
public let showLoadingIndicator: Bool
|
||||||
|
|
||||||
|
public init(
|
||||||
|
title: String,
|
||||||
|
subtitle: String? = nil,
|
||||||
|
tagline: String? = nil,
|
||||||
|
iconSymbols: [String] = ["star.fill"],
|
||||||
|
cornerSymbol: String? = nil,
|
||||||
|
decorativeSymbol: String? = "circle.fill",
|
||||||
|
patternStyle: LaunchPatternStyle = .dots,
|
||||||
|
layoutStyle: LaunchLayoutStyle = .iconAboveTitle,
|
||||||
|
primaryColor: Color = Color(red: 0.15, green: 0.20, blue: 0.30),
|
||||||
|
secondaryColor: Color = Color(red: 0.08, green: 0.10, blue: 0.18),
|
||||||
|
accentColor: Color = Color(red: 0.4, green: 0.7, blue: 1.0),
|
||||||
|
titleColor: Color = .white,
|
||||||
|
iconSize: CGFloat = 48,
|
||||||
|
titleSize: CGFloat = 42,
|
||||||
|
subtitleSize: CGFloat = 72,
|
||||||
|
iconSpacing: CGFloat = 8,
|
||||||
|
animationDuration: Double = 0.6,
|
||||||
|
showLoadingIndicator: Bool = false
|
||||||
|
) {
|
||||||
|
self.title = title
|
||||||
|
self.subtitle = subtitle
|
||||||
|
self.tagline = tagline
|
||||||
|
self.iconSymbols = iconSymbols
|
||||||
|
self.cornerSymbol = cornerSymbol
|
||||||
|
self.decorativeSymbol = decorativeSymbol
|
||||||
|
self.patternStyle = patternStyle
|
||||||
|
self.layoutStyle = layoutStyle
|
||||||
|
self.primaryColor = primaryColor
|
||||||
|
self.secondaryColor = secondaryColor
|
||||||
|
self.accentColor = accentColor
|
||||||
|
self.titleColor = titleColor
|
||||||
|
self.iconSize = iconSize
|
||||||
|
self.titleSize = titleSize
|
||||||
|
self.subtitleSize = subtitleSize
|
||||||
|
self.iconSpacing = iconSpacing
|
||||||
|
self.animationDuration = animationDuration
|
||||||
|
self.showLoadingIndicator = showLoadingIndicator
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Example Configuration (for previews only)
|
||||||
|
|
||||||
|
/// Example configuration for Bedrock previews.
|
||||||
|
/// Apps should define their own configs in `BrandingConfig.swift`.
|
||||||
|
public static let example = LaunchScreenConfig(
|
||||||
|
title: "MY APP",
|
||||||
|
tagline: "Your App Tagline",
|
||||||
|
iconSymbols: ["star.fill", "sparkles"]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Launch Screen View
|
||||||
|
|
||||||
|
/// A customizable launch screen view for any app.
|
||||||
|
public struct LaunchScreenView: View {
|
||||||
|
let config: LaunchScreenConfig
|
||||||
|
|
||||||
|
@State private var logoScale: CGFloat = 0.8
|
||||||
|
@State private var logoOpacity: Double = 0
|
||||||
|
@State private var taglineOffset: CGFloat = 20
|
||||||
|
@State private var taglineOpacity: Double = 0
|
||||||
|
|
||||||
|
public init(config: LaunchScreenConfig) {
|
||||||
|
self.config = config
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
ZStack {
|
||||||
|
// Background gradient
|
||||||
|
backgroundGradient
|
||||||
|
|
||||||
|
// Pattern overlay
|
||||||
|
patternOverlay
|
||||||
|
|
||||||
|
// Decorative corner elements (optional)
|
||||||
|
if config.cornerSymbol != nil {
|
||||||
|
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(config.titleColor.opacity(0.6))
|
||||||
|
.tracking(2)
|
||||||
|
.padding(.bottom, 40)
|
||||||
|
.offset(y: taglineOffset)
|
||||||
|
.opacity(taglineOpacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading indicator
|
||||||
|
if config.showLoadingIndicator {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(.circular)
|
||||||
|
.tint(config.accentColor)
|
||||||
|
.scaleEffect(1.2)
|
||||||
|
.padding(.bottom, 60)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.onAppear {
|
||||||
|
withAnimation(.easeOut(duration: config.animationDuration)) {
|
||||||
|
logoScale = 1.0
|
||||||
|
logoOpacity = 1.0
|
||||||
|
}
|
||||||
|
withAnimation(.easeOut(duration: config.animationDuration).delay(config.animationDuration * 0.5)) {
|
||||||
|
taglineOffset = 0
|
||||||
|
taglineOpacity = 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Background
|
||||||
|
|
||||||
|
private var backgroundGradient: some View {
|
||||||
|
LinearGradient(
|
||||||
|
colors: [config.primaryColor, config.secondaryColor],
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var patternOverlay: some View {
|
||||||
|
switch config.patternStyle {
|
||||||
|
case .none:
|
||||||
|
EmptyView()
|
||||||
|
case .dots:
|
||||||
|
dotsPattern.opacity(0.03)
|
||||||
|
case .grid:
|
||||||
|
gridPattern.opacity(0.05)
|
||||||
|
case .radial:
|
||||||
|
radialOverlay.opacity(0.15)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var dotsPattern: some View {
|
||||||
|
Canvas { context, size in
|
||||||
|
let spacing: CGFloat = 50
|
||||||
|
let dotRadius: CGFloat = 3
|
||||||
|
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 dot = Path(ellipseIn: CGRect(
|
||||||
|
x: x - dotRadius,
|
||||||
|
y: y - dotRadius,
|
||||||
|
width: dotRadius * 2,
|
||||||
|
height: dotRadius * 2
|
||||||
|
))
|
||||||
|
|
||||||
|
context.fill(dot, with: .color(.white))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var gridPattern: some View {
|
||||||
|
Canvas { context, size in
|
||||||
|
let spacing: CGFloat = 40
|
||||||
|
let lineWidth: CGFloat = 0.5
|
||||||
|
|
||||||
|
// Vertical lines
|
||||||
|
var x: CGFloat = 0
|
||||||
|
while x < size.width {
|
||||||
|
let line = Path { path in
|
||||||
|
path.move(to: CGPoint(x: x, y: 0))
|
||||||
|
path.addLine(to: CGPoint(x: x, y: size.height))
|
||||||
|
}
|
||||||
|
context.stroke(line, with: .color(.white), lineWidth: lineWidth)
|
||||||
|
x += spacing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal lines
|
||||||
|
var y: CGFloat = 0
|
||||||
|
while y < size.height {
|
||||||
|
let line = Path { path in
|
||||||
|
path.move(to: CGPoint(x: 0, y: y))
|
||||||
|
path.addLine(to: CGPoint(x: size.width, y: y))
|
||||||
|
}
|
||||||
|
context.stroke(line, with: .color(.white), lineWidth: lineWidth)
|
||||||
|
y += spacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var radialOverlay: some View {
|
||||||
|
RadialGradient(
|
||||||
|
colors: [config.accentColor.opacity(0.3), .clear],
|
||||||
|
center: .center,
|
||||||
|
startRadius: 0,
|
||||||
|
endRadius: 300
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Corner Decorations
|
||||||
|
|
||||||
|
private func cornerDecorations(in geometry: GeometryProxy) -> some View {
|
||||||
|
ZStack {
|
||||||
|
// Top-left
|
||||||
|
cornerSymbolView
|
||||||
|
.position(x: 50, y: 80)
|
||||||
|
|
||||||
|
// Top-right
|
||||||
|
cornerSymbolView
|
||||||
|
.rotationEffect(.degrees(90))
|
||||||
|
.position(x: geometry.size.width - 50, y: 80)
|
||||||
|
|
||||||
|
// Bottom-left
|
||||||
|
cornerSymbolView
|
||||||
|
.rotationEffect(.degrees(-90))
|
||||||
|
.position(x: 50, y: geometry.size.height - 80)
|
||||||
|
|
||||||
|
// Bottom-right
|
||||||
|
cornerSymbolView
|
||||||
|
.rotationEffect(.degrees(180))
|
||||||
|
.position(x: geometry.size.width - 50, y: geometry.size.height - 80)
|
||||||
|
}
|
||||||
|
.opacity(0.15)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var cornerSymbolView: some View {
|
||||||
|
Group {
|
||||||
|
if let symbol = config.cornerSymbol {
|
||||||
|
Image(systemName: symbol)
|
||||||
|
.font(.system(size: 30))
|
||||||
|
.foregroundStyle(config.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Logo Section
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var logoSection: some View {
|
||||||
|
switch config.layoutStyle {
|
||||||
|
case .iconAboveTitle:
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
iconRow
|
||||||
|
subtitleView
|
||||||
|
titleView
|
||||||
|
decorativeLineView
|
||||||
|
}
|
||||||
|
case .titleAboveIcon:
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
titleView
|
||||||
|
subtitleView
|
||||||
|
iconRow
|
||||||
|
decorativeLineView
|
||||||
|
}
|
||||||
|
case .iconOnly:
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
iconRow
|
||||||
|
decorativeLineView
|
||||||
|
}
|
||||||
|
case .titleOnly:
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
subtitleView
|
||||||
|
titleView
|
||||||
|
decorativeLineView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var iconRow: some View {
|
||||||
|
HStack(spacing: config.iconSpacing) {
|
||||||
|
ForEach(config.iconSymbols.indices, id: \.self) { index in
|
||||||
|
Image(systemName: config.iconSymbols[index])
|
||||||
|
.font(.system(size: config.iconSize, weight: .bold))
|
||||||
|
.foregroundStyle(config.accentColor)
|
||||||
|
.shadow(color: .black.opacity(0.3), radius: 4, y: 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var subtitleView: some View {
|
||||||
|
if let subtitle = config.subtitle {
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.system(size: config.subtitleSize, weight: .black, design: .rounded))
|
||||||
|
.foregroundStyle(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [config.accentColor, config.accentColor.opacity(0.7)],
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.shadow(color: .black.opacity(0.4), radius: 4, y: 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var titleView: some View {
|
||||||
|
Text(config.title)
|
||||||
|
.font(.system(size: config.titleSize, weight: .black, design: .rounded))
|
||||||
|
.tracking(6)
|
||||||
|
.foregroundStyle(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [config.titleColor, config.titleColor.opacity(0.85)],
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.shadow(color: .black.opacity(0.4), radius: 4, y: 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var decorativeLineView: some View {
|
||||||
|
if let symbol = config.decorativeSymbol {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
decorativeLine
|
||||||
|
|
||||||
|
Image(systemName: symbol)
|
||||||
|
.font(.system(size: 10))
|
||||||
|
.foregroundStyle(config.accentColor.opacity(0.6))
|
||||||
|
|
||||||
|
decorativeLine
|
||||||
|
}
|
||||||
|
.frame(width: 200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if config.layoutStyle != .titleOnly {
|
||||||
|
HStack(spacing: config.iconSpacing) {
|
||||||
|
ForEach(config.iconSymbols.indices, id: \.self) { index in
|
||||||
|
Image(systemName: config.iconSymbols[index])
|
||||||
|
.font(.system(size: config.iconSize, weight: .bold))
|
||||||
|
.foregroundStyle(config.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let subtitle = config.subtitle, config.layoutStyle != .iconOnly {
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.system(size: config.subtitleSize, weight: .black, design: .rounded))
|
||||||
|
.foregroundStyle(config.accentColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.layoutStyle != .iconOnly {
|
||||||
|
Text(config.title)
|
||||||
|
.font(.system(size: config.titleSize, weight: .black, design: .rounded))
|
||||||
|
.tracking(6)
|
||||||
|
.foregroundStyle(config.titleColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ignoresSafeArea()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Previews
|
||||||
|
|
||||||
|
#Preview("Launch Screen - Default") {
|
||||||
|
LaunchScreenView(config: .example)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Launch Screen - Minimal") {
|
||||||
|
let config = LaunchScreenConfig(
|
||||||
|
title: "CAMERA",
|
||||||
|
iconSymbols: ["camera.fill"],
|
||||||
|
patternStyle: .none,
|
||||||
|
primaryColor: Color(red: 0.1, green: 0.1, blue: 0.15),
|
||||||
|
secondaryColor: .black,
|
||||||
|
accentColor: .white
|
||||||
|
)
|
||||||
|
return LaunchScreenView(config: config)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Launch Screen - Radial") {
|
||||||
|
let config = LaunchScreenConfig(
|
||||||
|
title: "SELFIE CAM",
|
||||||
|
tagline: "Look Your Best",
|
||||||
|
iconSymbols: ["camera.fill", "sparkles"],
|
||||||
|
cornerSymbol: "sparkle",
|
||||||
|
patternStyle: .radial,
|
||||||
|
primaryColor: Color(red: 0.85, green: 0.25, blue: 0.45),
|
||||||
|
secondaryColor: Color(red: 0.45, green: 0.12, blue: 0.35),
|
||||||
|
accentColor: .white
|
||||||
|
)
|
||||||
|
return LaunchScreenView(config: config)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Launch Screen - Grid") {
|
||||||
|
let config = LaunchScreenConfig(
|
||||||
|
title: "NOTES",
|
||||||
|
iconSymbols: ["note.text"],
|
||||||
|
patternStyle: .grid,
|
||||||
|
layoutStyle: .iconAboveTitle,
|
||||||
|
primaryColor: Color(red: 0.95, green: 0.85, blue: 0.5),
|
||||||
|
secondaryColor: Color(red: 0.9, green: 0.75, blue: 0.3),
|
||||||
|
accentColor: Color(red: 0.3, green: 0.2, blue: 0.1),
|
||||||
|
titleColor: Color(red: 0.3, green: 0.2, blue: 0.1)
|
||||||
|
)
|
||||||
|
return LaunchScreenView(config: config)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Static Launch") {
|
||||||
|
StaticLaunchScreenView(config: .example)
|
||||||
|
}
|
||||||
@ -1,9 +1,93 @@
|
|||||||
{
|
{
|
||||||
"sourceLanguage" : "en",
|
"sourceLanguage" : "en",
|
||||||
"strings" : {
|
"strings" : {
|
||||||
|
"%lld." : {
|
||||||
|
"comment" : "A numbered list item with a callout number and accompanying text. The first argument is the number of the item. The second argument is the text describing the item.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"%lld%%" : {
|
"%lld%%" : {
|
||||||
"comment" : "A text label showing the current volume percentage. The argument is the volume as a percentage (0.0 to 1.0).",
|
"comment" : "A text label showing the current volume percentage. The argument is the volume as a percentage (0.0 to 1.0).",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"%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
|
||||||
|
},
|
||||||
|
"%lldpx" : {
|
||||||
|
"comment" : "A label displaying the size of the generated app icon. The argument is the size of the icon in pixels.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"1. Use Xcode's preview to screenshot these icons" : {
|
||||||
|
"comment" : "An instruction in the icon export view.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"2. Or use IconRenderer.renderAppIcon() in code" : {
|
||||||
|
"comment" : "An instruction within the icon export view, explaining how to generate 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
|
||||||
|
},
|
||||||
|
"1024 × 1024px" : {
|
||||||
|
"comment" : "A description of the size of the app icon.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"After generating:" : {
|
||||||
|
"comment" : "A heading for the instructions section of the IconGeneratorView.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"App Icon" : {
|
||||||
|
"comment" : "A heading for the app icon preview section.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"App Icon Preview" : {
|
||||||
|
"comment" : "A heading describing the preview of the app icon.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Export Instructions" : {
|
||||||
|
"comment" : "A section header describing how to export app icons.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Generate & Save Icon" : {
|
||||||
|
"comment" : "A button label that triggers icon generation and saving.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Generating..." : {
|
||||||
|
"comment" : "A label indicating that an icon is being generated.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Icon" : {
|
||||||
|
"comment" : "A tab label for the \"Icon\" tab in the branding preview view.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Icon Generator" : {
|
||||||
|
"comment" : "The title of the icon generator view.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Launch" : {
|
||||||
|
"comment" : "A tab label for the launch screen preview.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Note: iOS uses a single 1024px icon" : {
|
||||||
|
"comment" : "A note explaining that iOS uses a single 1024px icon.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Size Variants" : {
|
||||||
|
"comment" : "A heading for the size variants of an app icon.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"To Export" : {
|
||||||
|
"comment" : "A section header explaining how to export branding assets.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Use the Icon Generator in Settings → DEBUG to save the 1024px icon to the Files app, then add it to Xcode's Assets.xcassets/AppIcon." : {
|
||||||
|
"comment" : "Instructions for exporting an app icon.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Xcode automatically generates all required sizes from the 1024px source." : {
|
||||||
|
"comment" : "A footnote explaining that Xcode automatically creates all necessary icon sizes from the original 1024px image.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version" : "1.1"
|
"version" : "1.1"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user