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 @@ + + iOS Developer Toolkit (Swift) + + + 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 = """ + + iOS Developer Toolkit (Swift) + + + + +""" +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")