From 635751f9066635cb09020f81aa94c4bd69560806 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 27 Jan 2026 13:29:12 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- .../Bedrock/Branding/IconGeneratorView.swift | 109 +++++++++++------- 1 file changed, 68 insertions(+), 41 deletions(-) diff --git a/Sources/Bedrock/Branding/IconGeneratorView.swift b/Sources/Bedrock/Branding/IconGeneratorView.swift index f529cec..5a19574 100644 --- a/Sources/Bedrock/Branding/IconGeneratorView.swift +++ b/Sources/Bedrock/Branding/IconGeneratorView.swift @@ -14,9 +14,20 @@ 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 status: String = "Tap the button to generate all icon variants" @State private var isGenerating = false - @State private var generatedIcon: GeneratedIconInfo? + @State private var generatedIcons: [GeneratedIconInfo] = [] + + // Standard iOS icon sizes (points) + private let iconSizes: [(size: CGFloat, scales: [Int], name: String)] = [ + (20, [2, 3], "Notification"), + (29, [2, 3], "Settings"), + (40, [2, 3], "Spotlight"), + (60, [2, 3], "App"), + (76, [1, 2], "iPad App"), + (83.5, [2], "iPad Pro App"), + (1024, [1], "Marketing") + ] // Development view: fixed sizes acceptable private let previewSize: CGFloat = 200 @@ -46,7 +57,7 @@ public struct IconGeneratorView: View { // Generate button Button { Task { - await generateIcon() + await generateIcons() } } label: { HStack { @@ -54,7 +65,7 @@ public struct IconGeneratorView: View { ProgressView() .tint(.white) } - Text(isGenerating ? "Generating..." : "Generate & Save Icon") + Text(isGenerating ? "Generating..." : "Generate & Save All Icons") } .font(.headline) .foregroundStyle(.white) @@ -73,20 +84,31 @@ public struct IconGeneratorView: View { .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) + // Generated icons list + if !generatedIcons.isEmpty { + VStack(alignment: .leading, spacing: 8) { + Text("Generated Files:") + .font(.headline) + .padding(.horizontal) + + 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(.horizontal) + .padding(.vertical, 4) + .background(Color.green.opacity(0.05)) + } } - .padding() - .background(Color.green.opacity(0.1)) + .padding(.vertical) + .background(Color.green.opacity(0.05)) .clipShape(.rect(cornerRadius: 12)) .padding(.horizontal) } @@ -108,16 +130,16 @@ public struct IconGeneratorView: View { 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: 3, text: "Find the generated PNG files") 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") + Text("Note: Multiple sizes generated") .font(.subheadline.bold()) - Text("Xcode automatically generates all required sizes from the 1024px source.") + Text("While modern iOS can use a single 1024px icon, providing specific sizes ensures the best quality for notifications, settings, and spotlight.") .font(.caption) .foregroundStyle(.secondary) } @@ -138,35 +160,40 @@ public struct IconGeneratorView: View { } @MainActor - private func generateIcon() async { + private func generateIcons() async { #if canImport(UIKit) isGenerating = true - generatedIcon = nil - status = "Generating icon..." + generatedIcons = [] + status = "Generating icons..." 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)" + for iconDef in iconSizes { + for scale in iconDef.scales { + let pixelSize = iconDef.size * CGFloat(scale) + let filename = scale == 1 ? "AppIcon-\(iconDef.size)px.png" : "AppIcon-\(iconDef.size)px@\(scale)x.png" + + let view = AppIconView(config: config, size: pixelSize) + let renderer = ImageRenderer(content: view) + renderer.scale = 1.0 // We handle scale by pixel size + + if let uiImage = renderer.uiImage, + let data = uiImage.pngData() { + let fileURL = documentsPath.appending(path: filename) + + do { + try data.write(to: fileURL) + generatedIcons.append(GeneratedIconInfo(filename: filename, size: pixelSize)) + } catch { + status = "Error saving \(filename): \(error.localizedDescription)" + isGenerating = false + return + } + } } - } else { - status = "⚠️ Failed to render icon" } + status = "✅ \(generatedIcons.count) icons saved to Documents folder!\nOpen Files app to find them." isGenerating = false #else status = "⚠️ Icon generation is only available on iOS"