diff --git a/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_1024.png b/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_1024.png
index 6909a19..0db29ff 100644
Binary files a/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_1024.png and b/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_1024.png differ
diff --git a/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_128.png b/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_128.png
index dfb353d..3278199 100644
Binary files a/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_128.png and b/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_128.png differ
diff --git a/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_16.png b/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_16.png
index f388968..4e86904 100644
Binary files a/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_16.png and b/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_16.png differ
diff --git a/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_256.png b/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_256.png
index 9d0ef2b..64bb640 100644
Binary files a/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_256.png and b/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_256.png differ
diff --git a/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_32.png b/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_32.png
index b983480..1aff65d 100644
Binary files a/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_32.png and b/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_32.png differ
diff --git a/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_512.png b/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_512.png
index f813e2b..1eb1390 100644
Binary files a/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_512.png and b/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_512.png differ
diff --git a/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_64.png b/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_64.png
index c27c73a..7db2b48 100644
Binary files a/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_64.png and b/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_64.png differ
diff --git a/App/iOSDeveloperToolkit/Assets.xcassets/Logo.imageset/logo.png b/App/iOSDeveloperToolkit/Assets.xcassets/Logo.imageset/logo.png
index 1c84fe7..da72bd2 100644
Binary files a/App/iOSDeveloperToolkit/Assets.xcassets/Logo.imageset/logo.png and b/App/iOSDeveloperToolkit/Assets.xcassets/Logo.imageset/logo.png differ
diff --git a/App/iOSDeveloperToolkit/Views/DeviceViews.swift b/App/iOSDeveloperToolkit/Views/DeviceViews.swift
index 129b3b2..3618320 100644
--- a/App/iOSDeveloperToolkit/Views/DeviceViews.swift
+++ b/App/iOSDeveloperToolkit/Views/DeviceViews.swift
@@ -10,6 +10,19 @@ struct OverviewView: View {
var body: some View {
WorkspacePage(workspace: .overview) {
+ HStack(spacing: 14) {
+ Image("Logo")
+ .resizable()
+ .interpolation(.high)
+ .frame(width: 64, height: 64)
+ .accessibilityHidden(true)
+ VStack(alignment: .leading, spacing: 2) {
+ Text(ToolkitVersion.applicationName).font(.title2.bold())
+ Text("Version \(ToolkitVersion.current)").font(.callout).foregroundStyle(.secondary)
+ }
+ }
+ .accessibilityElement(children: .combine)
+ .accessibilityIdentifier("overview-logo")
TargetHeader()
if let device = model.selectedDevice {
NextStepCard(device: device)
diff --git a/README.md b/README.md
index 6630c58..c973014 100644
--- a/README.md
+++ b/README.md
@@ -362,6 +362,11 @@ scripts/build-restore-helpers.sh
open --env IDT_RESTORE_HELPERS="$PWD/build-output/restore-helpers/out/bin" "iOS Developer Toolkit (Swift).app"
```
+The app icon and logo come from one image, `docs/brand/logo-source.png`:
+`xcrun swift scripts/generate-icons.swift` writes the app icon at every size, the in-app logo, and
+`docs/brand/` (`icon-1024.png`, `iOSDeveloperToolkitSwift.icns`, `logo-1024.png`, `logo-512.png`,
+`logo.svg`, and the repository's `social-preview.png`).
+
The Xcode project is generated from `project.yml` with [XcodeGen](https://github.com/yonaskolb/XcodeGen)
and committed, so you only need XcodeGen when you change the project structure (`xcodegen generate`).
diff --git a/Tests/ToolkitFeaturesTests/RealSimulatorTests.swift b/Tests/ToolkitFeaturesTests/RealSimulatorTests.swift
index f5b84d9..b8ca232 100644
--- a/Tests/ToolkitFeaturesTests/RealSimulatorTests.swift
+++ b/Tests/ToolkitFeaturesTests/RealSimulatorTests.swift
@@ -60,7 +60,13 @@ struct RealSimulatorTests {
return lines
}
try await Task.sleep(for: .seconds(2))
- _ = try await client.launch(bundleIdentifier: "com.apple.Preferences", on: target, terminateExisting: true)
+ // Launching Settings only produces log traffic; the app launch itself is tested below with
+ // the fixture app. On a heavily loaded runner simctl can take longer than the app allows.
+ do {
+ _ = try await client.launch(bundleIdentifier: "com.apple.Preferences", on: target, terminateExisting: true)
+ } catch let error as ToolkitError where error.kind == .timedOut {
+ print("[simulator e2e] logs: launching Settings timed out on this machine (\(error.message)); the stream is checked without it")
+ }
try await Task.sleep(for: .seconds(4))
collector.cancel()
let lines = (try? await collector.value) ?? 0
diff --git a/docs/brand/iOSDeveloperToolkitSwift.icns b/docs/brand/iOSDeveloperToolkitSwift.icns
new file mode 100644
index 0000000..4d10981
Binary files /dev/null and b/docs/brand/iOSDeveloperToolkitSwift.icns differ
diff --git a/docs/brand/icon-1024.png b/docs/brand/icon-1024.png
new file mode 100644
index 0000000..0db29ff
Binary files /dev/null and b/docs/brand/icon-1024.png differ
diff --git a/docs/brand/logo-1024.png b/docs/brand/logo-1024.png
new file mode 100644
index 0000000..da72bd2
Binary files /dev/null and b/docs/brand/logo-1024.png differ
diff --git a/docs/brand/logo-512.png b/docs/brand/logo-512.png
new file mode 100644
index 0000000..d174b24
Binary files /dev/null and b/docs/brand/logo-512.png differ
diff --git a/docs/brand/logo-source.png b/docs/brand/logo-source.png
new file mode 100644
index 0000000..6e1c75d
Binary files /dev/null and b/docs/brand/logo-source.png differ
diff --git a/docs/brand/logo.svg b/docs/brand/logo.svg
new file mode 100644
index 0000000..12b3204
--- /dev/null
+++ b/docs/brand/logo.svg
@@ -0,0 +1,5 @@
+
diff --git a/docs/brand/social-preview.png b/docs/brand/social-preview.png
new file mode 100644
index 0000000..187d373
Binary files /dev/null and b/docs/brand/social-preview.png differ
diff --git a/scripts/generate-icons.swift b/scripts/generate-icons.swift
new file mode 100644
index 0000000..d34a677
--- /dev/null
+++ b/scripts/generate-icons.swift
@@ -0,0 +1,162 @@
+// Generates every icon and logo file of iOS Developer Toolkit (Swift) from one source image.
+//
+// xcrun swift scripts/generate-icons.swift [docs/brand/logo-source.png]
+//
+// The source is the rounded-square logo with transparent corners. Writes:
+// App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_{16…1024}.png
+// the app icon (Dock, Finder, About), on Apple's macOS icon grid: the tile is 824/1024 of
+// the canvas, centered, so it matches the size of other apps in the Dock
+// App/iOSDeveloperToolkit/Assets.xcassets/Logo.imageset/logo.png the tile edge to edge (1024)
+// docs/brand/icon-1024.png, docs/brand/iOSDeveloperToolkitSwift.icns the app icon as files
+// docs/brand/logo-1024.png, docs/brand/logo-512.png the logo edge to edge
+// docs/brand/logo.svg the logo as SVG (the 512-pixel logo embedded in a clipped SVG)
+// docs/brand/social-preview.png 1280×640 image for the repository's social preview
+// The tile's interior is made fully opaque (the source is slightly translucent, which shows as
+// grey patches on light backgrounds); the anti-aliased edge keeps its transparency.
+import AppKit
+import CoreGraphics
+import CoreText
+import Foundation
+import ImageIO
+import UniformTypeIdentifiers
+
+func fail(_ message: String) -> Never {
+ FileHandle.standardError.write(Data("generate-icons: \(message)\n".utf8))
+ exit(1)
+}
+
+let root = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
+let sourceURL = root.appendingPathComponent(CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : "docs/brand/logo-source.png")
+guard let source = CGImageSourceCreateWithURL(sourceURL as CFURL, nil),
+ let original = CGImageSourceCreateImageAtIndex(source, 0, nil) else { fail("cannot read \(sourceURL.path)") }
+let sRGB = CGColorSpace(name: CGColorSpace.sRGB)!
+
+func context(_ width: Int, _ height: Int) -> CGContext {
+ guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: 0,
+ space: sRGB, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { fail("no context") }
+ context.interpolationQuality = .high
+ return context
+}
+
+// 1. Read the pixels, find the tile, and make its interior opaque.
+let width = original.width, height = original.height
+let reader = context(width, height)
+reader.draw(original, in: CGRect(x: 0, y: 0, width: width, height: height))
+let pixels = reader.data!.bindMemory(to: UInt8.self, capacity: reader.bytesPerRow * height)
+let stride = reader.bytesPerRow
+var minX = width, minY = height, maxX = -1, maxY = -1
+for y in 0.. 64 {
+ minX = min(minX, x); maxX = max(maxX, x); minY = min(minY, y); maxY = max(maxY, y)
+ }
+}
+guard maxX > minX, maxY > minY else { fail("the source has no visible tile") }
+var translucent = 0
+for y in minY...maxY {
+ for x in minX...maxX {
+ let i = y * stride + x * 4
+ let alpha = Int(pixels[i + 3])
+ guard alpha > 0, alpha < 255 else { continue }
+ // Interior pixels (nearly opaque) become opaque; edge pixels are scaled up slightly.
+ let target = alpha >= 200 ? 255 : min(255, alpha * 255 / 252)
+ if alpha < 200 && x > minX + 40 && x < maxX - 40 && y > minY + 40 && y < maxY - 40 { translucent += 1 }
+ for channel in 0..<3 {
+ let straight = Int(pixels[i + channel]) * 255 / alpha
+ pixels[i + channel] = UInt8(min(255, straight * target / 255))
+ }
+ pixels[i + 3] = UInt8(target)
+ }
+}
+if translucent > 0 { print("note: \(translucent) clearly translucent pixels inside the tile were kept as they are") }
+let fixed = reader.makeImage()!
+// Core Graphics' origin is bottom-left; the crop rectangle uses image (top-left) coordinates.
+guard let tile = fixed.cropping(to: CGRect(x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1)) else { fail("crop failed") }
+print("tile: \(tile.width)×\(tile.height) at (\(minX), \(minY)) in a \(width)×\(height) source")
+
+// 2. Renderers.
+func render(_ size: Int, tileScale: CGFloat) -> CGImage {
+ let canvas = context(size, size)
+ let side = CGFloat(size) * tileScale
+ let aspect = CGFloat(tile.width) / CGFloat(tile.height)
+ let rect = CGRect(x: (CGFloat(size) - side * min(1, aspect)) / 2, y: (CGFloat(size) - side / max(1, aspect)) / 2,
+ width: side * min(1, aspect), height: side / max(1, aspect))
+ canvas.draw(tile, in: rect)
+ return canvas.makeImage()!
+}
+
+func pngData(_ image: CGImage) -> Data {
+ let data = NSMutableData()
+ guard let destination = CGImageDestinationCreateWithData(data, UTType.png.identifier as CFString, 1, nil) else { fail("no PNG encoder") }
+ CGImageDestinationAddImage(destination, image, nil)
+ guard CGImageDestinationFinalize(destination) else { fail("PNG encoding failed") }
+ return data as Data
+}
+
+func write(_ image: CGImage, to path: String) {
+ let url = root.appendingPathComponent(path)
+ try? FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
+ do { try pngData(image).write(to: url, options: .atomic) } catch { fail("cannot write \(path): \(error)") }
+ print("wrote \(path) (\(image.width)×\(image.height))")
+}
+
+// Apple's macOS icon grid: an 824-point body on a 1024-point canvas.
+let gridScale: CGFloat = 824.0 / 1024.0
+
+// 3. App icon, logo, and brand files.
+let appIcon = "App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset"
+for size in [16, 32, 64, 128, 256, 512, 1024] {
+ write(render(size, tileScale: gridScale), to: "\(appIcon)/icon_\(size).png")
+}
+write(render(1024, tileScale: 1), to: "App/iOSDeveloperToolkit/Assets.xcassets/Logo.imageset/logo.png")
+write(render(1024, tileScale: gridScale), to: "docs/brand/icon-1024.png")
+write(render(1024, tileScale: 1), to: "docs/brand/logo-1024.png")
+let logo512 = render(512, tileScale: 1)
+write(logo512, to: "docs/brand/logo-512.png")
+
+// .icns through iconutil (Apple's tool), from a temporary .iconset.
+let iconset = FileManager.default.temporaryDirectory.appendingPathComponent("idt-\(UUID().uuidString).iconset")
+try? FileManager.default.createDirectory(at: iconset, withIntermediateDirectories: true)
+for (name, size) in [("16x16", 16), ("16x16@2x", 32), ("32x32", 32), ("32x32@2x", 64), ("128x128", 128), ("128x128@2x", 256),
+ ("256x256", 256), ("256x256@2x", 512), ("512x512", 512), ("512x512@2x", 1024)] {
+ try? pngData(render(size, tileScale: gridScale)).write(to: iconset.appendingPathComponent("icon_\(name).png"))
+}
+let iconutil = Process()
+iconutil.executableURL = URL(fileURLWithPath: "/usr/bin/iconutil")
+iconutil.arguments = ["-c", "icns", iconset.path, "-o", root.appendingPathComponent("docs/brand/iOSDeveloperToolkitSwift.icns").path]
+try iconutil.run()
+iconutil.waitUntilExit()
+try? FileManager.default.removeItem(at: iconset)
+guard iconutil.terminationStatus == 0 else { fail("iconutil failed") }
+print("wrote docs/brand/iOSDeveloperToolkitSwift.icns")
+
+// SVG: the logo embedded as PNG, clipped to its rounded square so it scales cleanly anywhere.
+let radius = Int((Double(512) * 0.235).rounded())
+let svg = """
+
+
+"""
+try Data(svg.utf8).write(to: root.appendingPathComponent("docs/brand/logo.svg"), options: .atomic)
+print("wrote docs/brand/logo.svg")
+
+// Social preview (GitHub recommends 1280×640): the logo and the name on the logo's own dark blue.
+let social = context(1280, 640)
+social.setFillColor(CGColor(srgbRed: 0.075, green: 0.094, blue: 0.125, alpha: 1))
+social.fill(CGRect(x: 0, y: 0, width: 1280, height: 640))
+social.draw(render(420, tileScale: 1), in: CGRect(x: 110, y: 110, width: 420, height: 420))
+func drawText(_ text: String, size: CGFloat, weight: NSFont.Weight, color: CGColor, at point: CGPoint) {
+ let font = NSFont.systemFont(ofSize: size, weight: weight)
+ let line = CTLineCreateWithAttributedString(NSAttributedString(string: text, attributes: [.font: font, .foregroundColor: NSColor(cgColor: color)!]))
+ social.textPosition = point
+ CTLineDraw(line, social)
+}
+let white = CGColor(srgbRed: 0.96, green: 0.97, blue: 0.99, alpha: 1)
+let grey = CGColor(srgbRed: 0.62, green: 0.68, blue: 0.76, alpha: 1)
+drawText("iOS Developer", size: 64, weight: .bold, color: white, at: CGPoint(x: 590, y: 372))
+drawText("Toolkit (Swift)", size: 64, weight: .bold, color: white, at: CGPoint(x: 590, y: 292))
+drawText("A native macOS app and CLI for iPhone, iPad,", size: 26, weight: .regular, color: grey, at: CGPoint(x: 592, y: 226))
+drawText("and simulators.", size: 26, weight: .regular, color: grey, at: CGPoint(x: 592, y: 190))
+write(social.makeImage()!, to: "docs/brand/social-preview.png")