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

This commit is contained in:
Matt Bruce 2026-01-27 13:29:12 -06:00
parent 657ca6bc5c
commit 635751f906

View File

@ -14,9 +14,20 @@ public struct IconGeneratorView: View {
let config: AppIconConfig let config: AppIconConfig
let appName: String 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 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 // Development view: fixed sizes acceptable
private let previewSize: CGFloat = 200 private let previewSize: CGFloat = 200
@ -46,7 +57,7 @@ public struct IconGeneratorView: View {
// Generate button // Generate button
Button { Button {
Task { Task {
await generateIcon() await generateIcons()
} }
} label: { } label: {
HStack { HStack {
@ -54,7 +65,7 @@ public struct IconGeneratorView: View {
ProgressView() ProgressView()
.tint(.white) .tint(.white)
} }
Text(isGenerating ? "Generating..." : "Generate & Save Icon") Text(isGenerating ? "Generating..." : "Generate & Save All Icons")
} }
.font(.headline) .font(.headline)
.foregroundStyle(.white) .foregroundStyle(.white)
@ -73,20 +84,31 @@ public struct IconGeneratorView: View {
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.padding(.horizontal) .padding(.horizontal)
// Generated icon confirmation // Generated icons list
if let icon = generatedIcon { if !generatedIcons.isEmpty {
HStack { VStack(alignment: .leading, spacing: 8) {
Image(systemName: "checkmark.circle.fill") Text("Generated Files:")
.foregroundStyle(.green) .font(.headline)
Text(icon.filename) .padding(.horizontal)
.font(.callout.monospaced())
Spacer() ForEach(generatedIcons) { icon in
Text("\(Int(icon.size))px") HStack {
.font(.callout) Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.secondary) .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() .padding(.vertical)
.background(Color.green.opacity(0.1)) .background(Color.green.opacity(0.05))
.clipShape(.rect(cornerRadius: 12)) .clipShape(.rect(cornerRadius: 12))
.padding(.horizontal) .padding(.horizontal)
} }
@ -108,16 +130,16 @@ public struct IconGeneratorView: View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
instructionRow(number: 1, text: "Open Files app on your device/simulator") instructionRow(number: 1, text: "Open Files app on your device/simulator")
instructionRow(number: 2, text: "Navigate to: On My iPhone → \(appName)") 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: 4, text: "AirDrop or share to your Mac")
instructionRow(number: 5, text: "Drag into Xcode's Assets.xcassets/AppIcon") instructionRow(number: 5, text: "Drag into Xcode's Assets.xcassets/AppIcon")
} }
Divider() Divider()
Text("Note: iOS uses a single 1024px icon") Text("Note: Multiple sizes generated")
.font(.subheadline.bold()) .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) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
@ -138,35 +160,40 @@ public struct IconGeneratorView: View {
} }
@MainActor @MainActor
private func generateIcon() async { private func generateIcons() async {
#if canImport(UIKit) #if canImport(UIKit)
isGenerating = true isGenerating = true
generatedIcon = nil generatedIcons = []
status = "Generating icon..." status = "Generating icons..."
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
// Render the 1024px icon (the only size needed for modern iOS) for iconDef in iconSizes {
let view = AppIconView(config: config, size: 1024) for scale in iconDef.scales {
let renderer = ImageRenderer(content: view) let pixelSize = iconDef.size * CGFloat(scale)
renderer.scale = 1.0 let filename = scale == 1 ? "AppIcon-\(iconDef.size)px.png" : "AppIcon-\(iconDef.size)px@\(scale)x.png"
if let uiImage = renderer.uiImage, let view = AppIconView(config: config, size: pixelSize)
let data = uiImage.pngData() { let renderer = ImageRenderer(content: view)
let filename = "AppIcon.png" renderer.scale = 1.0 // We handle scale by pixel size
let fileURL = documentsPath.appending(path: filename)
do { if let uiImage = renderer.uiImage,
try data.write(to: fileURL) let data = uiImage.pngData() {
generatedIcon = GeneratedIconInfo(filename: filename, size: 1024) let fileURL = documentsPath.appending(path: filename)
status = "✅ Icon saved to Documents folder!\nOpen Files app to find it."
} catch { do {
status = "Error saving icon: \(error.localizedDescription)" 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 isGenerating = false
#else #else
status = "⚠️ Icon generation is only available on iOS" status = "⚠️ Icon generation is only available on iOS"