diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 3d96702..9358ce0 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,8 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Security vulnerability
- url: https://github.com/hideouts-io/iOS-Developer-Toolkit/security/advisories/new
+ url: https://github.com/hideouts-io/iOS-Developer-Toolkit-Swift/security/advisories/new
about: Report vulnerabilities privately. Do not open a public issue.
- name: Questions and usage help
- url: https://github.com/hideouts-io/iOS-Developer-Toolkit/discussions
+ url: https://github.com/hideouts-io/iOS-Developer-Toolkit-Swift/discussions
about: Ask setup, compatibility, and workflow questions in Discussions.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index aad7383..72c6b67 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -83,7 +83,7 @@ jobs:
- name: Render every page at the default and minimum window sizes
run: |
scripts/check-layout.sh \
- "$RUNNER_TEMP/dd/Build/Products/Debug/iOS Developer Toolkit.app/Contents/MacOS/iOS Developer Toolkit" \
+ "$RUNNER_TEMP/dd/Build/Products/Debug/iOS Developer Toolkit (Swift).app/Contents/MacOS/iOS Developer Toolkit (Swift)" \
"$RUNNER_TEMP/layout"
- uses: actions/upload-artifact@v7
if: always()
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d1c6d47..686f94c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -53,8 +53,8 @@ jobs:
run: |
sha256sum -c SHA256SUMS.txt
version="${GITHUB_REF_NAME#v}"
- zip="release-assets/iOS-Developer-Toolkit-$version-macOS-universal.zip"
- sbom="release-assets/iOS-Developer-Toolkit-$version.spdx.json"
+ zip="release-assets/iOS-Developer-Toolkit-Swift-$version-macOS-universal.zip"
+ sbom="release-assets/iOS-Developer-Toolkit-Swift-$version.spdx.json"
test -f "../$zip" && test -f "../$sbom"
echo "zip=$zip" >> "$GITHUB_OUTPUT"
echo "sbom=$sbom" >> "$GITHUB_OUTPUT"
diff --git a/.gitignore b/.gitignore
index 1d95755..94663ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,7 @@ mvt-analysis-*/
location-logs/
Location Logs/
iOS Developer Toolkit Location Logs/
+iOS Developer Toolkit (Swift) Location Logs/
Live Logs/
Generated Routes/
location-events.jsonl
diff --git a/App/UITests/SmokeUITests.swift b/App/UITests/SmokeUITests.swift
index ede84f7..72524a0 100644
--- a/App/UITests/SmokeUITests.swift
+++ b/App/UITests/SmokeUITests.swift
@@ -41,7 +41,7 @@ final class SmokeUITests: XCTestCase {
}
func testEveryWorkspaceOpens() throws {
- let workspaces = ["overview", "device", "readiness", "apps", "installApp", "location", "liveLogs", "actions", "backup", "evidence", "externalTools", "activity", "help", "safety"]
+ let workspaces = ["overview", "device", "developerImage", "readiness", "apps", "installApp", "location", "liveLogs", "actions", "backup", "evidence", "externalTools", "activity", "help", "safety"]
for workspace in workspaces {
let item = app.descendants(matching: .any)["sidebar-\(workspace)"]
XCTAssertTrue(item.waitForExistence(timeout: 5), "Missing sidebar item \(workspace)")
@@ -64,8 +64,13 @@ final class SmokeUITests: XCTestCase {
}
func testDeveloperImageCardShowsStateAndBlocksDemoMount() throws {
+ // The Device page summarizes the state and opens the Developer Image page.
app.descendants(matching: .any)["sidebar-device"].click()
+ let open = app.buttons["open-developer-image"]
+ XCTAssertTrue(open.waitForExistence(timeout: 10), "The Device page does not link to the Developer Image page")
+ open.click()
XCTAssertTrue(app.descendants(matching: .any)["ddi-state"].waitForExistence(timeout: 10), "The developer image state is not shown")
+ XCTAssertTrue(app.descendants(matching: .any)["ddi-mechanism"].exists, "The mount method choice is missing")
let mount = app.buttons["mount-ddi"]
XCTAssertTrue(mount.waitForExistence(timeout: 5))
XCTAssertFalse(mount.isEnabled, "Mounting must be disabled in Demo Mode")
diff --git a/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_1024.png b/App/iOSDeveloperToolkit/Assets.xcassets/AppIcon.appiconset/icon_1024.png
index 25642c3..6909a19 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 ebd7394..dfb353d 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 9861ea0..f388968 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 a34da70..9d0ef2b 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 3c5ada3..b983480 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 18361f5..f813e2b 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 d7866ba..c27c73a 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 bbc8054..1c84fe7 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/Info.plist b/App/iOSDeveloperToolkit/Info.plist
index 0fd848c..b5e5f0b 100644
--- a/App/iOSDeveloperToolkit/Info.plist
+++ b/App/iOSDeveloperToolkit/Info.plist
@@ -5,7 +5,7 @@
CFBundleDevelopmentRegion
en
CFBundleDisplayName
- iOS Developer Toolkit
+ iOS Developer Toolkit (Swift)
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
diff --git a/App/iOSDeveloperToolkit/Model/DeveloperImageModel.swift b/App/iOSDeveloperToolkit/Model/DeveloperImageModel.swift
index 91c1204..2a00ed7 100644
--- a/App/iOSDeveloperToolkit/Model/DeveloperImageModel.swift
+++ b/App/iOSDeveloperToolkit/Model/DeveloperImageModel.swift
@@ -56,7 +56,7 @@ final class DeveloperImageModel {
let failure = LockedValue(nil)
working.insert(device.id)
defer { working.remove(device.id) }
- let result = await app.run("Mount developer image", workspace: .device, target: target, transport: mechanism.label) { operation in
+ let result = await app.run("Mount developer image", workspace: .developerImage, target: target, transport: mechanism.label) { operation in
do {
return try await manager.mount(target, mechanism: mechanism, userFolders: folders) { progress in
operation.report(progress.step, progress: progress.fraction)
@@ -82,7 +82,7 @@ final class DeveloperImageModel {
let folders = folders
working.insert(device.id)
defer { working.remove(device.id) }
- if let result = await app.run("Unmount developer image", workspace: .device, target: target, transport: "mobile_image_mounter UnmountImage", { _ in
+ if let result = await app.run("Unmount developer image", workspace: .developerImage, target: target, transport: "mobile_image_mounter UnmountImage", { _ in
try await manager.unmount(target, userFolders: folders)
}) {
statuses[device.id] = result
diff --git a/App/iOSDeveloperToolkit/Model/FeatureModels.swift b/App/iOSDeveloperToolkit/Model/FeatureModels.swift
index e0fb7ab..860db24 100644
--- a/App/iOSDeveloperToolkit/Model/FeatureModels.swift
+++ b/App/iOSDeveloperToolkit/Model/FeatureModels.swift
@@ -177,7 +177,7 @@ final class InstallModel {
@Observable
@MainActor
final class BackupModel {
- var destination = URL.documents.appendingPathComponent("iOS Developer Toolkit Backups")
+ var destination = URL.documents.appendingPathComponent("\(ToolkitVersion.applicationName) Backups")
var forceFullBackup = false
var requireEncryption = true
var encryptionEnabled: Bool?
@@ -265,7 +265,7 @@ final class BackupModel {
@Observable
@MainActor
final class EvidenceModel {
- var outputRoot = URL.documents.appendingPathComponent("iOS Developer Toolkit Cases")
+ var outputRoot = URL.documents.appendingPathComponent("\(ToolkitVersion.applicationName) Cases")
var options = CollectionOptions()
var caseTitle = ""
var casePurpose = ""
diff --git a/App/iOSDeveloperToolkit/Model/LocationModel.swift b/App/iOSDeveloperToolkit/Model/LocationModel.swift
index b14b8a9..c3c40af 100644
--- a/App/iOSDeveloperToolkit/Model/LocationModel.swift
+++ b/App/iOSDeveloperToolkit/Model/LocationModel.swift
@@ -32,7 +32,7 @@ final class LocationModel {
var lastSimulatedTarget: DeviceTarget?
private var playbackTask: Task?
- let evidenceDirectory = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Documents/iOS Developer Toolkit Location Logs")
+ let evidenceDirectory = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Documents/\(ToolkitVersion.applicationName) Location Logs")
init() {
// Screenshot mode shows sample places so personal saved places never appear in docs.
diff --git a/App/iOSDeveloperToolkit/Model/ScreenshotHarness.swift b/App/iOSDeveloperToolkit/Model/ScreenshotHarness.swift
index 36101d6..086aa51 100644
--- a/App/iOSDeveloperToolkit/Model/ScreenshotHarness.swift
+++ b/App/iOSDeveloperToolkit/Model/ScreenshotHarness.swift
@@ -5,7 +5,7 @@ import ToolkitFeatures
/// Renders the app's own window for documentation and GUI verification:
///
-/// "iOS Developer Toolkit" -capture-screenshots
+/// "iOS Developer Toolkit (Swift)" -capture-screenshots
/// [-demo-mode] [-ui-testing] [-window-size WxH] [-only overview,apps]
/// [-populate-demo YES] [-select-booted-simulator YES] [-start-simulator-log YES]
/// [-scroll-fraction 0.0–1.0] [-show-sheet reconnect|shortcuts|advanced] [-select-physical-device YES]
diff --git a/App/iOSDeveloperToolkit/Views/ContentView.swift b/App/iOSDeveloperToolkit/Views/ContentView.swift
index 9c62fa5..53179bc 100644
--- a/App/iOSDeveloperToolkit/Views/ContentView.swift
+++ b/App/iOSDeveloperToolkit/Views/ContentView.swift
@@ -61,6 +61,7 @@ struct WorkspaceView: View {
switch workspace {
case .overview: OverviewView()
case .device: DeviceDetailView()
+ case .developerImage: DeveloperImageView()
case .readiness: ReadinessView()
case .apps: AppsView()
case .installApp: InstallAppView()
diff --git a/App/iOSDeveloperToolkit/Views/DeveloperImageView.swift b/App/iOSDeveloperToolkit/Views/DeveloperImageView.swift
new file mode 100644
index 0000000..6b1a8ec
--- /dev/null
+++ b/App/iOSDeveloperToolkit/Views/DeveloperImageView.swift
@@ -0,0 +1,254 @@
+import DeviceKit
+import SwiftUI
+import ToolkitCore
+import ToolkitFeatures
+
+// MARK: - Developer Image
+
+/// Apple's developer image (Developer Disk Image): its state on the selected device, mounting and
+/// unmounting, how it is mounted, and which images this Mac has.
+struct DeveloperImageView: View {
+ @Environment(AppModel.self) private var model
+ @State private var confirmation: PendingConfirmation?
+ @State private var inventory: DeveloperImageHostInventory?
+
+ var body: some View {
+ WorkspacePage(workspace: .developerImage) {
+ TargetHeader()
+ if let device = model.selectedDevice {
+ switch device.kind {
+ case .simulator:
+ Card(title: "Not needed for simulators", systemImage: "checkmark.circle") {
+ Text("Simulators include developer services, so no developer image is mounted. Choose an iPhone or iPad to use this page.")
+ .font(.callout)
+ .fixedSize(horizontal: false, vertical: true)
+ }
+ case .physical, .demo:
+ DeveloperImageStatusCard(device: device, confirmation: $confirmation)
+ mountMethod
+ imagesOnThisMac(device)
+ if let mounted = model.developerImage.status(for: device)?.mountedImages, !mounted.isEmpty {
+ mountedImages(mounted)
+ }
+ about(device)
+ }
+ } else {
+ ContentUnavailableView("No Device Selected", systemImage: "externaldrive.badge.questionmark", description: Text("Connect an iPhone or iPad by USB, unlock it, and tap Trust. Then choose it in the toolbar."))
+ .frame(maxWidth: .infinity, minHeight: 240)
+ }
+ }
+ .sheet(item: $confirmation) { pending in
+ ConfirmationSheet(title: pending.title, detail: pending.detail, requirement: pending.requirement, target: pending.target, commandPreview: nil, onConfirm: pending.action)
+ }
+ .task(id: model.selectedDevice?.id) {
+ // Reading the state never changes the device.
+ if let device = model.selectedDevice, device.kind != .simulator, model.developerImage.status(for: device) == nil {
+ await model.developerImage.refresh(device, app: model)
+ }
+ }
+ .task(id: model.developerImage.folders) { reloadInventory() }
+ }
+
+ private var mountMethod: some View {
+ let images = model.developerImage
+ return Card(title: "How to mount", systemImage: "gearshape.2", subtitle: "Used by Mount Developer Image. If one way fails, try another.") {
+ Picker("Mount with", selection: Binding(get: { images.mechanism }, set: { images.mechanism = $0 })) {
+ ForEach(DeveloperImageMechanism.allCases, id: \.self) { mechanism in
+ VStack(alignment: .leading, spacing: 2) {
+ Text(mechanism.label)
+ Text(mechanism.explanation)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ .fixedSize(horizontal: false, vertical: true)
+ }
+ .tag(mechanism)
+ }
+ }
+ .pickerStyle(.radioGroup)
+ .labelsHidden()
+ .accessibilityIdentifier("ddi-mechanism")
+ }
+ }
+
+ private func imagesOnThisMac(_ device: Device) -> some View {
+ let images = model.developerImage
+ return Card(title: "Images on this Mac", systemImage: "internaldrive", subtitle: "iOS 17 and later use the personalized image that Xcode installs. iOS 16 and earlier need the image for that exact iOS version, from Xcode or a folder you add.") {
+ if let inventory {
+ if inventory.personalized.isEmpty && inventory.legacy.isEmpty {
+ Label("No developer images found. Install Xcode and open it once, or add a folder that contains one.", systemImage: "exclamationmark.triangle")
+ .font(.callout)
+ .foregroundStyle(.orange)
+ .fixedSize(horizontal: false, vertical: true)
+ }
+ ForEach(Array(inventory.personalized.enumerated()), id: \.offset) { _, source in
+ InfoRow("Personalized image", source.displayName + (source.supportedProductTypes.isEmpty ? "" : " · \(source.supportedProductTypes.count) device models"))
+ }
+ ForEach(Array(inventory.legacy.enumerated()), id: \.offset) { _, source in
+ InfoRow("iOS \(source.version)", source.displayName)
+ }
+ } else {
+ ProgressView().controlSize(.small)
+ }
+ if !images.folders.isEmpty {
+ VStack(alignment: .leading, spacing: 4) {
+ Text("Folders you added").font(.callout.weight(.semibold))
+ ForEach(images.folders, id: \.self) { folder in
+ HStack {
+ Text(folder.path).font(.callout.monospaced()).lineLimit(1).truncationMode(.middle)
+ Spacer()
+ Button("Remove") { images.removeFolder(folder) }.controlSize(.small)
+ }
+ }
+ }
+ }
+ HStack {
+ Button("Add Image Folder…") {
+ if let folder = FilePanels.chooseFolder(title: "Choose a folder that contains a developer image", canCreate: false) {
+ model.statusMessage = images.addFolder(folder)
+ Task { await images.refresh(device, app: model) }
+ }
+ }
+ Button("Update This Mac's Images from Xcode") { runAction("host-ddis-update", device) }
+ .disabled(!device.supportsCoreDevice)
+ .help("Asks Xcode to download or update its developer images (devicectl manage ddis update)")
+ }
+ }
+ }
+
+ private func mountedImages(_ mounted: [MountedImage]) -> some View {
+ Card(title: "Mounted on the device", systemImage: "externaldrive.connected.to.line.below") {
+ ForEach(Array(mounted.enumerated()), id: \.offset) { _, image in
+ InfoRow(image.imageType ?? "Image", image.mountPath ?? (image.isMounted == true ? "Mounted" : "Not mounted"), monospaced: image.mountPath != nil)
+ }
+ }
+ }
+
+ private func about(_ device: Device) -> some View {
+ Card(title: "About developer images", systemImage: "info.circle") {
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Screenshots, location simulation, launching apps, and Instruments need Apple's developer image mounted on the device. Logs, backups, diagnostics, app lists, and packet capture do not.")
+ Text("The image stays mounted until the device restarts. Mounting needs the device unlocked, and on iOS 16 and later it needs Developer Mode on.")
+ Text("On iOS 17 and later, Apple personalizes the image for the device before it is mounted, just as Xcode does: this Mac sends the device's chip, board, and ECID with a one-time nonce to Apple's signing server.")
+ }
+ .font(.callout)
+ .fixedSize(horizontal: false, vertical: true)
+ HStack {
+ if device.kind == .physical && device.developerMode != .enabled {
+ Button("How to Turn On Developer Mode…") { model.isDeveloperModeGuidePresented = true }
+ }
+ Button("Open Readiness Check") { model.workspace = .readiness }
+ }
+ }
+ }
+
+ private func reloadInventory() {
+ let folders = model.developerImage.folders
+ let coreDevice = model.selectedDevice?.supportsCoreDevice ?? false
+ Task.detached(priority: .userInitiated) {
+ let found = DeveloperImageHostInventory.discover(userFolders: folders, coreDeviceAvailable: coreDevice)
+ await MainActor.run { inventory = found }
+ }
+ }
+
+ private func runAction(_ id: String, _ device: Device) {
+ guard let action = ActionCatalog.descriptor(id) else { return }
+ let executor = model.executor
+ let target = device.target
+ Task {
+ if let result = await model.run(action.title, workspace: .developerImage, target: target, transport: action.mechanism, { _ in try await executor.execute(action, target: target, values: [:]) }) {
+ model.statusMessage = result.summary
+ reloadInventory()
+ }
+ }
+ }
+}
+
+/// The developer image's state on one device, with Check Again, Mount, and Unmount.
+struct DeveloperImageStatusCard: View {
+ @Environment(AppModel.self) private var model
+ let device: Device
+ @Binding var confirmation: PendingConfirmation?
+
+ var body: some View {
+ let images = model.developerImage
+ let status = images.status(for: device)
+ let busy = images.isBusy(device)
+ Card(title: "On \(device.name)", systemImage: "externaldrive.badge.checkmark") {
+ VStack(alignment: .leading, spacing: 10) {
+ HStack(spacing: 8) {
+ if let status {
+ Label(status.state.label, systemImage: status.state.symbolName)
+ .font(.callout.weight(.semibold))
+ .foregroundStyle(Self.color(for: status.state))
+ .accessibilityIdentifier("ddi-state")
+ Text(status.headline).font(.callout)
+ } else {
+ Text("Not checked yet.").font(.callout).foregroundStyle(.secondary)
+ }
+ Spacer(minLength: 0)
+ if busy { ProgressView().controlSize(.small) }
+ }
+ if let status {
+ Text(status.explanation)
+ .font(.callout)
+ .foregroundStyle(.secondary)
+ .fixedSize(horizontal: false, vertical: true)
+ if let remediation = status.remediation {
+ Label(remediation, systemImage: "lightbulb")
+ .font(.callout)
+ .fixedSize(horizontal: false, vertical: true)
+ }
+ DisclosureGroup("Details") {
+ VStack(alignment: .leading, spacing: 4) {
+ ForEach(Array(status.detailRows.filter { $0.0 != "State" && $0.0 != "Next step" }.enumerated()), id: \.offset) { _, row in
+ InfoRow(row.0, row.1, monospaced: row.0 == "Chip / board")
+ }
+ if let technical = status.technicalDetail {
+ InfoRow("Technical detail", technical, monospaced: true)
+ }
+ }
+ .padding(.top, 4)
+ }
+ .font(.callout)
+ }
+ HStack {
+ Button("Check Again") { Task { await images.refresh(device, app: model) } }
+ .disabled(busy)
+ Button("Mount Developer Image") {
+ confirmation = PendingConfirmation(title: "Mount the developer image", detail: Self.mountDetail(status, device: device, mechanism: images.mechanism), requirement: .make(for: .deviceChange, target: device.target), target: device.target) {
+ Task { await images.mount(device, app: model) }
+ }
+ }
+ .disabled(busy || device.kind != .physical || !(status?.state.canMount ?? false))
+ .accessibilityIdentifier("mount-ddi")
+ Button("Unmount") {
+ confirmation = PendingConfirmation(title: "Unmount the developer image", detail: "Developer services stop until the image is mounted again. Restarting the device has the same effect.", requirement: .make(for: .deviceChange, target: device.target), target: device.target) {
+ Task { await images.unmount(device, app: model) }
+ }
+ }
+ .disabled(busy || status?.state != .mounted || !device.supportsLockdownServices)
+ .accessibilityIdentifier("unmount-ddi")
+ }
+ }
+ }
+ }
+
+ static func color(for state: DeveloperImageState) -> Color {
+ switch state {
+ case .mounted: return .green
+ case .notRequired: return .secondary
+ case .available, .personalizationRequired: return .blue
+ case .missing, .incompatible, .blocked: return .orange
+ case .failed: return .red
+ }
+ }
+
+ static func mountDetail(_ status: DeveloperImageStatus?, device: Device, mechanism: DeveloperImageMechanism) -> String {
+ var lines = ["The developer image will be uploaded to \(device.name) and mounted (\(mechanism.label)). Keep the device unlocked and connected until it finishes."]
+ if status?.requiredKind == .personalized && status?.state == .personalizationRequired {
+ lines.append("Apple personalizes the image for this device first: this Mac sends the device's chip, board, and ECID with a one-time nonce to Apple's signing server (gs.apple.com), as Xcode does. An internet connection is required.")
+ }
+ if let host = status?.hostImage { lines.append("Image: \(host).") }
+ return lines.joined(separator: "\n\n")
+ }
+}
diff --git a/App/iOSDeveloperToolkit/Views/DeviceViews.swift b/App/iOSDeveloperToolkit/Views/DeviceViews.swift
index e90fd3b..129b3b2 100644
--- a/App/iOSDeveloperToolkit/Views/DeviceViews.swift
+++ b/App/iOSDeveloperToolkit/Views/DeviceViews.swift
@@ -193,110 +193,25 @@ struct DeviceDetailView: View {
}
private func developerServices(_ device: Device) -> some View {
- let images = model.developerImage
- let status = images.status(for: device)
- let busy = images.isBusy(device)
- return Card(title: "Developer image", systemImage: "externaldrive.badge.checkmark", subtitle: "Screenshots, location simulation, launching apps, and Instruments need Apple's developer image (Developer Disk Image) mounted on the device.") {
- VStack(alignment: .leading, spacing: 10) {
- HStack(spacing: 8) {
- if let status {
- Label(status.state.label, systemImage: status.state.symbolName)
- .font(.callout.weight(.semibold))
- .foregroundStyle(Self.color(for: status.state))
- .accessibilityIdentifier("ddi-state")
- Text(status.headline).font(.callout)
- } else {
- Text("Not checked yet.").font(.callout).foregroundStyle(.secondary)
- }
- Spacer(minLength: 0)
- if busy { ProgressView().controlSize(.small) }
- }
+ let status = model.developerImage.status(for: device)
+ return Card(title: "Developer image", systemImage: "externaldrive.badge.checkmark", subtitle: "Screenshots, location simulation, launching apps, and Instruments need Apple's developer image mounted on the device.") {
+ HStack(spacing: 8) {
if let status {
- Text(status.explanation)
- .font(.callout)
- .foregroundStyle(.secondary)
- .fixedSize(horizontal: false, vertical: true)
- if let remediation = status.remediation {
- Label(remediation, systemImage: "lightbulb")
- .font(.callout)
- .fixedSize(horizontal: false, vertical: true)
- }
- DisclosureGroup("Details") {
- VStack(alignment: .leading, spacing: 4) {
- ForEach(Array(status.detailRows.filter { $0.0 != "State" && $0.0 != "Next step" }.enumerated()), id: \.offset) { _, row in
- InfoRow(row.0, row.1, monospaced: row.0 == "Chip / board")
- }
- if let technical = status.technicalDetail {
- InfoRow("Technical detail", technical, monospaced: true)
- }
- }
- .padding(.top, 4)
- }
- .font(.callout)
- }
- HStack {
- Button("Check Again") { Task { await images.refresh(device, app: model) } }
- .disabled(busy)
- Button("Mount Developer Image") {
- confirmation = PendingConfirmation(title: "Mount the developer image", detail: Self.mountDetail(status, device: device), requirement: .make(for: .deviceChange, target: device.target), target: device.target) {
- Task { await images.mount(device, app: model) }
- }
- }
- .disabled(busy || device.kind != .physical || !(status?.state.canMount ?? false))
- .accessibilityIdentifier("mount-ddi")
- Button("Unmount") {
- confirmation = PendingConfirmation(title: "Unmount the developer image", detail: "Developer services stop until the image is mounted again. Restarting the device has the same effect.", requirement: .make(for: .deviceChange, target: device.target), target: device.target) {
- Task { await images.unmount(device, app: model) }
- }
- }
- .disabled(busy || status?.state != .mounted || !device.supportsLockdownServices)
- Menu("Options") {
- Picker("Mount with", selection: Binding(get: { images.mechanism }, set: { images.mechanism = $0 })) {
- ForEach(DeveloperImageMechanism.allCases, id: \.self) { Text($0.label).tag($0) }
- }
- Divider()
- Button("Add Image Folder…") {
- if let folder = FilePanels.chooseFolder(title: "Choose a folder that contains a developer image", canCreate: false) {
- model.statusMessage = images.addFolder(folder)
- Task { await images.refresh(device, app: model) }
- }
- }
- if !images.folders.isEmpty {
- Menu("Remove Image Folder") {
- ForEach(images.folders, id: \.self) { folder in
- Button(folder.path) { images.removeFolder(folder) }
- }
- }
- }
- Divider()
- Button("Update This Mac's Images from Xcode") { runAction("host-ddis-update", device) }
- .disabled(!device.supportsCoreDevice)
- }
- .fixedSize()
+ Label(status.state.label, systemImage: status.state.symbolName)
+ .font(.callout.weight(.semibold))
+ .foregroundStyle(DeveloperImageStatusCard.color(for: status.state))
+ .accessibilityIdentifier("ddi-summary")
+ Text(status.headline).font(.callout).lineLimit(2)
+ } else {
+ Text("Not checked yet.").font(.callout).foregroundStyle(.secondary)
}
+ Spacer(minLength: 8)
+ Button("Open Developer Image") { model.workspace = .developerImage }
+ .accessibilityIdentifier("open-developer-image")
}
}
}
- static func color(for state: DeveloperImageState) -> Color {
- switch state {
- case .mounted: return .green
- case .notRequired: return .secondary
- case .available, .personalizationRequired: return .blue
- case .missing, .incompatible, .blocked: return .orange
- case .failed: return .red
- }
- }
-
- static func mountDetail(_ status: DeveloperImageStatus?, device: Device) -> String {
- var lines = ["The developer image will be uploaded to \(device.name) and mounted. Keep the device unlocked and connected until it finishes."]
- if status?.requiredKind == .personalized && status?.state == .personalizationRequired {
- lines.append("Apple personalizes the image for this device first: this Mac sends the device's chip, board, and ECID with a one-time nonce to Apple's signing server (gs.apple.com), as Xcode does. An internet connection is required.")
- }
- if let host = status?.hostImage { lines.append("Image: \(host).") }
- return lines.joined(separator: "\n\n")
- }
-
private func handoffs(_ device: Device) -> some View {
Card(title: "Apple developer tools", systemImage: "arrow.up.forward.app") {
HStack {
diff --git a/App/iOSDeveloperToolkit/iOSDeveloperToolkitApp.swift b/App/iOSDeveloperToolkit/iOSDeveloperToolkitApp.swift
index 6737499..5748364 100644
--- a/App/iOSDeveloperToolkit/iOSDeveloperToolkitApp.swift
+++ b/App/iOSDeveloperToolkit/iOSDeveloperToolkitApp.swift
@@ -8,7 +8,7 @@ struct IOSDeveloperToolkitApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate
var body: some Scene {
- WindowGroup("iOS Developer Toolkit", id: "main") {
+ WindowGroup(ToolkitVersion.applicationName, id: "main") {
ContentView()
.environment(model)
.frame(minWidth: 900, minHeight: 560)
diff --git a/CITATION.cff b/CITATION.cff
index f418596..0aef5cb 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -4,8 +4,8 @@ title: "iOS Developer Toolkit"
type: software
authors:
- name: "hideouts-io"
-repository-code: "https://github.com/hideouts-io/iOS-Developer-Toolkit"
-url: "https://github.com/hideouts-io/iOS-Developer-Toolkit/releases/latest"
+repository-code: "https://github.com/hideouts-io/iOS-Developer-Toolkit-Swift"
+url: "https://github.com/hideouts-io/iOS-Developer-Toolkit-Swift/releases/latest"
license: MIT
version: 1.0.0
abstract: "A native Swift/SwiftUI macOS app for iPhone, iPad, and simulator development and diagnostics: device readiness, live Unified and classic logs, location simulation, app installation, encrypted backups, packet capture, and hashed evidence collection."
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index be59498..9891001 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -36,7 +36,7 @@ This Code of Conduct applies within project spaces and when an individual offici
## Enforcement
-Report abusive, harassing, or otherwise unacceptable behavior privately through the repository's [private report form](https://github.com/hideouts-io/iOS-Developer-Toolkit/security/advisories/new) and begin the summary with `Code of Conduct`. Reports will be reviewed by the project maintainer. The maintainer will respect the reporter's privacy and safety.
+Report abusive, harassing, or otherwise unacceptable behavior privately through the repository's [private report form](https://github.com/hideouts-io/iOS-Developer-Toolkit-Swift/security/advisories/new) and begin the summary with `Code of Conduct`. Reports will be reviewed by the project maintainer. The maintainer will respect the reporter's privacy and safety.
Enforcement may include a private warning, removal of content, a temporary participation restriction, or a permanent ban, depending on impact and pattern of behavior.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 98293cb..51a99ea 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -22,7 +22,7 @@ questions, and private vulnerability reporting for security issues.
Requirements: macOS 14 or later and Xcode 16 or later (Swift 6).
```bash
-git clone https://github.com/hideouts-io/iOS-Developer-Toolkit.git
+git clone https://github.com/hideouts-io/iOS-Developer-Toolkit-Swift.git
cd iOS-Developer-Toolkit
swift build
swift test
@@ -71,7 +71,7 @@ Also, when relevant:
automation) and render every page at the default and minimum sizes, as CI does:
```bash
- scripts/check-layout.sh "…/iOS Developer Toolkit.app/Contents/MacOS/iOS Developer Toolkit" /tmp/layout
+ scripts/check-layout.sh "…/iOS Developer Toolkit (Swift).app/Contents/MacOS/iOS Developer Toolkit (Swift)" /tmp/layout
```
It fails if any page is squeezed, overflows the window, or does not render; the PNGs are in
diff --git a/MIGRATION.md b/MIGRATION.md
index 577a693..4addde2 100644
--- a/MIGRATION.md
+++ b/MIGRATION.md
@@ -9,8 +9,9 @@ itself) · 🟡 migrated and tested against the protocol-accurate fake device or
needs physical-device verification · 🔁 replaced by a different Apple-supported mechanism · ❌ not
migrated (see §6) or removed
-> The Python app is not discontinued: it continues as the 0.x line on the `python` branch (see
-> the README). This document covers how its features map to the Swift app (1.x) on `main`.
+> The Python app is not discontinued: it continues as **iOS Developer Toolkit** in
+> [hideouts-io/iOS-Developer-Toolkit](https://github.com/hideouts-io/iOS-Developer-Toolkit). This document covers how its features map to
+> this Swift app, **iOS Developer Toolkit (Swift)**.
## 1. Audit of the Python application (v0.3.4)
diff --git a/README.md b/README.md
index 3e745fe..99b4a41 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,13 @@
-# iOS Developer Toolkit
+# iOS Developer Toolkit (Swift)
-
+
**A native macOS app for working with iPhones, iPads, and simulators — device information, live logs, location simulation, app installs, backups, packet capture, readiness checks, and documented evidence collection.**
-[](https://github.com/hideouts-io/iOS-Developer-Toolkit/actions/workflows/ci.yml)
-[](https://github.com/hideouts-io/iOS-Developer-Toolkit/releases/latest)
+[](https://github.com/hideouts-io/iOS-Developer-Toolkit-Swift/actions/workflows/ci.yml)
+[](https://github.com/hideouts-io/iOS-Developer-Toolkit-Swift/releases/latest)


[](LICENSE)
@@ -114,8 +114,8 @@ The Command Line Tools alone are not enough for the Xcode column: `devicectl`, `
### Download a release
-1. Download `iOS-Developer-Toolkit-VERSION-macOS-universal.zip` and `SHA256SUMS.txt` from the
- [latest release](https://github.com/hideouts-io/iOS-Developer-Toolkit/releases/latest).
+1. Download `iOS-Developer-Toolkit-Swift-VERSION-macOS-universal.zip` and `SHA256SUMS.txt` from the
+ [latest release](https://github.com/hideouts-io/iOS-Developer-Toolkit-Swift/releases/latest).
2. Verify the download:
```bash
@@ -125,16 +125,16 @@ The Command Line Tools alone are not enough for the Xcode column: `devicectl`, `
Optionally verify GitHub's build attestation:
```bash
- gh attestation verify iOS-Developer-Toolkit-VERSION-macOS-universal.zip --repo hideouts-io/iOS-Developer-Toolkit
+ gh attestation verify iOS-Developer-Toolkit-Swift-VERSION-macOS-universal.zip --repo hideouts-io/iOS-Developer-Toolkit-Swift
```
-3. Unzip it and move **iOS Developer Toolkit.app** to `/Applications`.
+3. Unzip it and move **iOS Developer Toolkit (Swift).app** to `/Applications`.
4. Release builds are ad-hoc signed and not notarized, so macOS asks for confirmation the first
time. Control-click the app, choose **Open**, and confirm. If there is no Open button, go to
**System Settings › Privacy & Security** and choose **Open Anyway**. Do not disable Gatekeeper.
Nothing else needs to be installed. The release also contains `idt`, the command-line tool, at
-`iOS Developer Toolkit.app/Contents/MacOS/idt`.
+`iOS Developer Toolkit (Swift).app/Contents/MacOS/idt`.
### Build it yourself
@@ -150,7 +150,7 @@ See [Build from source](#build-from-source).
4. **Turn on Developer Mode if needed** (iOS 16+): *Settings › Privacy & Security › Developer
Mode*, then restart and confirm. **Device › Developer Mode Guide** walks through it.
5. **Mount the developer image** for screenshots, location simulation, and launching apps: the
- **Developer image** card on the Device page shows its state; click **Mount Developer Image**.
+ **Developer Image** page (in the sidebar, under Device) shows its state; click **Mount Developer Image**.
See [Developer images](#developer-images).
6. **Clean up** when finished: stop streams and clear simulated locations. Quitting the app
clears a location this session simulated.
@@ -160,17 +160,19 @@ No device handy? Turn on **Device › Demo Mode** to explore every workspace wit
## Workspaces
- **Overview** — status of the selected device, a suggested next step, and entry points.
-- **Device** — identity and status with explanations, the developer image (state, mount,
- unmount), Apple developer tool handoffs (open a project in Xcode, open an `.xcresult` or
+- **Device** — identity and status with explanations, a summary of the developer image, Apple developer tool handoffs (open a project in Xcode, open an `.xcresult` or
`.trace`, list Remote Virtual Interfaces), simulator controls, raw records, and connection
diagnostics.
+- **Developer Image** — the developer image on the selected device (state, details, mount,
+ unmount), how to mount it (automatic, Xcode's device service, or the built-in client), the
+ images on this Mac and folders you add, and what is mounted on the device.
- **Readiness Check** — the read-only prerequisite check, a copyable report, and a local history
of tested devices that can be exported as sanitized JSON or Markdown.
- **Apps** — installed apps with search, sort, sizes, launch, and confirmed removal.
- **Install App** — inspect an `.ipa` (or choose a simulator `.app`), see whether the device is in
the provisioning profile, then install.
- **Location Lab** — coordinates, map, saved places (stored only on this Mac), routes, GPX playback.
- Location events are appended to `~/Documents/iOS Developer Toolkit Location Logs/location-events.jsonl`.
+ Location events are appended to `~/Documents/iOS Developer Toolkit (Swift) Location Logs/location-events.jsonl`.
- **Live Logs** — several concurrent streams; pop out any stream into its own window.
- **Actions** — the guided action catalog and Advanced Mode.
- **Backup** — encryption status and setup, full or incremental backups.
@@ -200,8 +202,8 @@ that the device answering is the one you selected.
## Developer images
Screenshots, location simulation, launching apps, and Instruments need Apple's developer image
-(Developer Disk Image) mounted on the device. The **Developer image** card on the Device page
-checks it automatically and shows one of these states:
+(Developer Disk Image) mounted on the device. The **Developer Image** page (sidebar, under Device)
+checks it automatically and shows one of these states; the Device page shows a summary:
[](docs/screenshots/developer-image.png)
@@ -229,9 +231,9 @@ with a one-time nonce, exactly as Xcode does. The confirmation says so before an
**iOS 16 and earlier** use `DeveloperDiskImage.dmg` and its `.signature` for the exact iOS
`major.minor` version. Current Xcode versions no longer include them; add a folder that contains
them (for example an older Xcode's `Platforms/iPhoneOS.platform/DeviceSupport/16.4`) with
-**Options › Add Image Folder…**.
+**Add Image Folder…** on the Developer Image page.
-**How it mounts** (Options › Mount with): *Automatic* uses Xcode's device service (`devicectl`)
+**How it mounts** (Developer Image › How to mount): *Automatic* uses Xcode's device service (`devicectl`)
when it can reach the device on iOS 17 and later, and otherwise the built-in client, which talks
to the device's image-mounter service over USB and works without Xcode's device service. The app
never downloads images from third parties.
@@ -287,7 +289,7 @@ device could not be identified.
| “This device has not trusted this Mac” | Unlock the device and reconnect it; tap **Trust**. If no prompt appears, reset *Settings › General › Transfer or Reset › Reset › Reset Location & Privacy*. |
| Developer Mode is missing on the device | Connect it and open Xcode › *Window › Devices and Simulators* once. |
| Developer features say they need Xcode | Install Xcode, open it once, and check *Xcode › Settings › Locations › Command Line Tools*. |
-| The developer image will not mount | Read the Developer image card: it names the problem (Developer Mode, lock, missing or incompatible image) and the fix. On iOS 17 and later keep the Mac online (Apple personalizes the image); if one route fails, switch **Options › Mount with**. |
+| The developer image will not mount | Read the Developer Image page: it names the problem (Developer Mode, lock, missing or incompatible image) and the fix. On iOS 17 and later keep the Mac online (Apple personalizes the image); if one route fails, switch **How to mount** on the Developer Image page. |
| A backup stops with “must stay unlocked” | Unlock the device and keep it awake until the backup finishes. |
| An `.ipa` cannot be installed | Check the inspection: the signature must be valid and the profile must include the device. |
| Live logs are very busy | Filter the view or pause it; capture continues in the background. |
@@ -300,7 +302,7 @@ More detail: [docs/troubleshooting.md](docs/troubleshooting.md).
Requirements: macOS 14+, Xcode 16 or later (Swift 6).
```bash
-git clone https://github.com/hideouts-io/iOS-Developer-Toolkit.git
+git clone https://github.com/hideouts-io/iOS-Developer-Toolkit-Swift.git
cd iOS-Developer-Toolkit
swift test # unit and integration tests
@@ -326,7 +328,7 @@ xcodebuild -project iOSDeveloperToolkit.xcodeproj -scheme iOSDeveloperToolkit \
Documentation screenshots are produced by the app itself:
```bash
-"iOS Developer Toolkit.app/Contents/MacOS/iOS Developer Toolkit" \
+"iOS Developer Toolkit (Swift).app/Contents/MacOS/iOS Developer Toolkit (Swift)" \
-demo-mode YES -populate-demo YES -window-size 1180x700 -capture-screenshots ~/Desktop/shots
```
@@ -338,18 +340,17 @@ See [docs/architecture.md](docs/architecture.md).
## Project status and limitations
-Version 1.0 is a complete rewrite of the earlier Python/PySide6 app (0.3.x), which depended on
+Version 1.0 is a native rewrite of the Python/PySide6 app ([iOS Developer Toolkit](https://github.com/hideouts-io/iOS-Developer-Toolkit), 0.3.x), which depends on
`pymobiledevice3`. See [MIGRATION.md](MIGRATION.md) for the feature-by-feature mapping. Workspace
profiles exported by 0.3.x can be imported in **Settings › Profiles**; the preview explains how each
setting carries over.
-### The Python app (0.x)
+### The Python app
-The Python/PySide6 app is still maintained, as a separate line: it is developed on the
-[`python`](https://github.com/hideouts-io/iOS-Developer-Toolkit/tree/python) branch and released as
-`v0.x` ([0.x releases](https://github.com/hideouts-io/iOS-Developer-Toolkit/releases?q=v0.&expanded=true),
-with separate Apple silicon and Intel builds). It uses `pymobiledevice3` and Python; this app (1.x)
-needs neither. Workspace profiles move between them: 1.x imports profiles exported by 0.x.
+The Python/PySide6 app, **iOS Developer Toolkit**, is maintained separately in
+[hideouts-io/iOS-Developer-Toolkit](https://github.com/hideouts-io/iOS-Developer-Toolkit), with its releases. It uses `pymobiledevice3` and
+Python; this app needs neither. The two install side by side and keep their data in separate
+folders, and this app imports workspace profiles exported by the Python app.
- The native lockdown services (logs, packet capture, backup, diagnostics, app installation over
USB, developer-image checking and mounting including Apple personalization) are tested end to
diff --git a/SECURITY.md b/SECURITY.md
index 40cce93..4ef4bed 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -10,7 +10,7 @@
## Report a vulnerability privately
-Use [GitHub private vulnerability reporting](https://github.com/hideouts-io/iOS-Developer-Toolkit/security/advisories/new).
+Use [GitHub private vulnerability reporting](https://github.com/hideouts-io/iOS-Developer-Toolkit-Swift/security/advisories/new).
Do not disclose a suspected vulnerability in a public issue, discussion, pull request, log,
screenshot, or evidence archive.
diff --git a/SOURCE_AVAILABILITY.md b/SOURCE_AVAILABILITY.md
index 5328e75..b6153c5 100644
--- a/SOURCE_AVAILABILITY.md
+++ b/SOURCE_AVAILABILITY.md
@@ -1,12 +1,12 @@
# Source availability
Every published release of iOS Developer Toolkit is built from a Git tag in
-[this public repository](https://github.com/hideouts-io/iOS-Developer-Toolkit) by the release
+[this public repository](https://github.com/hideouts-io/iOS-Developer-Toolkit-Swift) by the release
workflow, with a GitHub build-provenance attestation that links the archive to that workflow run
and commit. GitHub provides source archives for each tag, or:
```bash
-git clone --branch vVERSION --depth 1 https://github.com/hideouts-io/iOS-Developer-Toolkit.git
+git clone --branch vVERSION --depth 1 https://github.com/hideouts-io/iOS-Developer-Toolkit-Swift.git
```
The app is written in Swift. Its third-party dependencies are Swift packages resolved from their
diff --git a/SUPPORT.md b/SUPPORT.md
index 5e453f6..d132b1d 100644
--- a/SUPPORT.md
+++ b/SUPPORT.md
@@ -1,6 +1,6 @@
# Support
-Use [GitHub Discussions](https://github.com/hideouts-io/iOS-Developer-Toolkit/discussions) for
+Use [GitHub Discussions](https://github.com/hideouts-io/iOS-Developer-Toolkit-Swift/discussions) for
installation, pairing, Developer Mode, developer services, compatibility, and workflow questions.
Before asking:
diff --git a/Sources/DeviceKit/DeveloperImage/DeveloperImageManager.swift b/Sources/DeviceKit/DeveloperImage/DeveloperImageManager.swift
index c56cb4a..a4f0635 100644
--- a/Sources/DeviceKit/DeveloperImage/DeveloperImageManager.swift
+++ b/Sources/DeviceKit/DeveloperImage/DeveloperImageManager.swift
@@ -70,6 +70,18 @@ public enum DeveloperImageMechanism: String, Sendable, Hashable, Codable, CaseIt
case .native: return "Built-in (image mounter over USB)"
}
}
+
+ /// What choosing this mechanism means, in plain language.
+ public var explanation: String {
+ switch self {
+ case .automatic:
+ return "Uses Xcode's device service when it can reach the device, otherwise the built-in client. Recommended."
+ case .coreDevice:
+ return "Asks Xcode to prepare the device, exactly as Xcode does. Needs Xcode, and a device that Xcode can see."
+ case .native:
+ return "The app's own image-mounter client over USB, with no Xcode device service involved. On iOS 17 and later, Apple personalizes the image first, so this Mac must be online."
+ }
+ }
}
/// Facts about the device that decide which image it needs.
diff --git a/Sources/DeviceKit/Services/MobileBackup2.swift b/Sources/DeviceKit/Services/MobileBackup2.swift
index d27196e..9e742f6 100644
--- a/Sources/DeviceKit/Services/MobileBackup2.swift
+++ b/Sources/DeviceKit/Services/MobileBackup2.swift
@@ -125,7 +125,7 @@ public enum MobileBackup2 {
"GUID": .string(UUID().uuidString.replacingOccurrences(of: "-", with: "")),
"Last Backup Date": .date(Date()),
"iTunes Version": "12.13.0",
- "Backup Tool": "iOS Developer Toolkit",
+ "Backup Tool": .string(ToolkitVersion.applicationName),
]
let mapping = [
"Device Name": "DeviceName", "Display Name": "DeviceName", "Product Type": "ProductType",
diff --git a/Sources/ToolkitCore/ToolkitVersion.swift b/Sources/ToolkitCore/ToolkitVersion.swift
index 8e917f6..fcc8cd5 100644
--- a/Sources/ToolkitCore/ToolkitVersion.swift
+++ b/Sources/ToolkitCore/ToolkitVersion.swift
@@ -4,5 +4,7 @@ import Foundation
/// the app's Info.plist.
public enum ToolkitVersion {
public static let current = "1.0.0"
- public static let applicationName = "iOS Developer Toolkit"
+ /// The app's name. It also names its folders (caches, Application Support, and the default
+ /// Documents folders), which keeps them apart from the Python app's "iOS Developer Toolkit" folders.
+ public static let applicationName = "iOS Developer Toolkit (Swift)"
}
diff --git a/Sources/ToolkitFeatures/Evidence/CaseWorkflow.swift b/Sources/ToolkitFeatures/Evidence/CaseWorkflow.swift
index 670ea5d..6b79989 100644
--- a/Sources/ToolkitFeatures/Evidence/CaseWorkflow.swift
+++ b/Sources/ToolkitFeatures/Evidence/CaseWorkflow.swift
@@ -60,7 +60,7 @@ public enum CaseWorkflow {
let folder = try createCaseFolder(in: root, target: target, at: now)
let document: [String: Any] = [
"schema_version": 2,
- "application": "iOS Developer Toolkit",
+ "application": ToolkitVersion.applicationName,
"case": try JSONSerialization.jsonObject(with: JSONOutput.encode(intake)),
"limitations": [
"The intake records the operator acknowledgement; it does not establish chain of custody.",
diff --git a/Sources/ToolkitFeatures/Evidence/EvidenceCollector.swift b/Sources/ToolkitFeatures/Evidence/EvidenceCollector.swift
index 973ef34..cb0bbd6 100644
--- a/Sources/ToolkitFeatures/Evidence/EvidenceCollector.swift
+++ b/Sources/ToolkitFeatures/Evidence/EvidenceCollector.swift
@@ -79,7 +79,7 @@ public enum CollectionOutcome: String, Codable, Sendable {
public struct CollectionManifest: Codable, Sendable {
public var schemaVersion = 2
- public var application = "iOS Developer Toolkit"
+ public var application = ToolkitVersion.applicationName
public var applicationVersion: String
public var targetUDID: String
public var targetName: String
diff --git a/Sources/ToolkitFeatures/LiveLogs/LogCapture.swift b/Sources/ToolkitFeatures/LiveLogs/LogCapture.swift
index bd04381..a7cacd6 100644
--- a/Sources/ToolkitFeatures/LiveLogs/LogCapture.swift
+++ b/Sources/ToolkitFeatures/LiveLogs/LogCapture.swift
@@ -172,7 +172,7 @@ public actor LogCapture {
private var closed = false
public static func defaultDirectory(home: URL = FileManager.default.homeDirectoryForCurrentUser) -> URL {
- home.appendingPathComponent("Library/Caches/iOS Developer Toolkit/Live Logs")
+ home.appendingPathComponent("Library/Caches/\(ToolkitVersion.applicationName)/Live Logs")
}
public init(kind: LogStreamKind, target: DeviceTarget, directory: URL = LogCapture.defaultDirectory(), now: Date = Date()) throws {
diff --git a/Sources/ToolkitFeatures/Location/LocationLab.swift b/Sources/ToolkitFeatures/Location/LocationLab.swift
index 0c61115..23b53f5 100644
--- a/Sources/ToolkitFeatures/Location/LocationLab.swift
+++ b/Sources/ToolkitFeatures/Location/LocationLab.swift
@@ -171,7 +171,7 @@ public enum LocationLab {
formatter.formatOptions = [.withInternetDateTime]
var lines = [
#""#,
- #""#,
+ #""#,
" Toolkit QA route",
]
for (index, point) in points.enumerated() {
@@ -224,7 +224,7 @@ public enum LocationLab {
// MARK: Saved locations (same JSON schema as earlier releases)
public static func savedLocationsURL(home: URL = FileManager.default.homeDirectoryForCurrentUser) -> URL {
- home.appendingPathComponent("Library/Application Support/iOS Developer Toolkit/locations.json")
+ home.appendingPathComponent("Library/Application Support/\(ToolkitVersion.applicationName)/locations.json")
}
public static func validateLocationName(_ name: String) throws -> String {
diff --git a/Sources/ToolkitFeatures/Readiness/CompatibilityStore.swift b/Sources/ToolkitFeatures/Readiness/CompatibilityStore.swift
index 4283dee..4a87792 100644
--- a/Sources/ToolkitFeatures/Readiness/CompatibilityStore.swift
+++ b/Sources/ToolkitFeatures/Readiness/CompatibilityStore.swift
@@ -38,11 +38,11 @@ public struct CompatibilityObservation: Codable, Sendable, Hashable, Identifiabl
}
}
-/// Append-only local history at ~/Library/Application Support/iOS Developer Toolkit/Compatibility.
+/// Append-only local history at ~/Library/Application Support//Compatibility.
public struct CompatibilityStore: Sendable {
public let url: URL
- public init(url: URL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Application Support/iOS Developer Toolkit/Compatibility/observations-v2.jsonl")) {
+ public init(url: URL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Application Support/\(ToolkitVersion.applicationName)/Compatibility/observations-v2.jsonl")) {
self.url = url
}
@@ -101,7 +101,7 @@ public struct CompatibilityStore: Sendable {
var lines = [
"# Real-device readiness report",
"",
- "Generated \(ISO8601.string(Date())) by iOS Developer Toolkit \(ToolkitVersion.current). Observed results only; untested devices and builds are not predicted. Names, identifiers, and fingerprints are omitted.",
+ "Generated \(ISO8601.string(Date())) by \(ToolkitVersion.applicationName) \(ToolkitVersion.current). Observed results only; untested devices and builds are not predicted. Names, identifiers, and fingerprints are omitted.",
"",
"| Model | iOS | Build | Connection | " + rows.map(\.title).joined(separator: " | ") + " |",
"|" + String(repeating: "---|", count: 4 + rows.count),
diff --git a/Sources/ToolkitFeatures/Shared/SupportBundle.swift b/Sources/ToolkitFeatures/Shared/SupportBundle.swift
index cde05af..b089693 100644
--- a/Sources/ToolkitFeatures/Shared/SupportBundle.swift
+++ b/Sources/ToolkitFeatures/Shared/SupportBundle.swift
@@ -32,7 +32,7 @@ public struct SupportBundleContext: Sendable {
/// Builds a local, reviewable ZIP for bug reports. Nothing is uploaded.
public enum SupportBundle {
public static let readme = """
- iOS Developer Toolkit sanitized support bundle
+ iOS Developer Toolkit (Swift) sanitized support bundle
This archive was generated locally and is never uploaded by the application. It contains app and
macOS version information, aggregate readiness states, sanitized status summaries, the toolchain
diff --git a/Sources/ToolkitFeatures/Shared/Workspace.swift b/Sources/ToolkitFeatures/Shared/Workspace.swift
index c43240b..def47d4 100644
--- a/Sources/ToolkitFeatures/Shared/Workspace.swift
+++ b/Sources/ToolkitFeatures/Shared/Workspace.swift
@@ -6,6 +6,7 @@ import Foundation
public enum Workspace: String, CaseIterable, Codable, Sendable, Identifiable {
case overview
case device
+ case developerImage
case readiness
case apps
case installApp
@@ -25,6 +26,7 @@ public enum Workspace: String, CaseIterable, Codable, Sendable, Identifiable {
switch self {
case .overview: return "Overview"
case .device: return "Device"
+ case .developerImage: return "Developer Image"
case .readiness: return "Readiness Check"
case .apps: return "Apps"
case .installApp: return "Install App"
@@ -44,6 +46,7 @@ public enum Workspace: String, CaseIterable, Codable, Sendable, Identifiable {
switch self {
case .overview: return "square.grid.2x2"
case .device: return "iphone"
+ case .developerImage: return "opticaldiscdrive"
case .readiness: return "checklist"
case .apps: return "app.badge"
case .installApp: return "square.and.arrow.down.on.square"
@@ -63,6 +66,7 @@ public enum Workspace: String, CaseIterable, Codable, Sendable, Identifiable {
switch self {
case .overview: return "What you can do and where to start"
case .device: return "Identity, trust, Developer Mode, and developer services"
+ case .developerImage: return "Check, mount, and unmount Apple's developer image"
case .readiness: return "Check every prerequisite before you start"
case .apps: return "Installed apps: search, sizes, launch, and remove"
case .installApp: return "Inspect an .ipa or .app, then install it"
@@ -89,7 +93,7 @@ public enum Workspace: String, CaseIterable, Codable, Sendable, Identifiable {
public var group: Group {
switch self {
case .overview: return .start
- case .device, .readiness, .apps, .installApp: return .device
+ case .device, .developerImage, .readiness, .apps, .installApp: return .device
case .location, .liveLogs, .actions: return .develop
case .backup, .evidence, .externalTools: return .data
case .activity, .help, .safety: return .reference
diff --git a/Sources/idt/IDT.swift b/Sources/idt/IDT.swift
index 050fdba..66bf326 100644
--- a/Sources/idt/IDT.swift
+++ b/Sources/idt/IDT.swift
@@ -8,7 +8,7 @@ import ToolkitFeatures
struct IDT: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "idt",
- abstract: "iOS Developer Toolkit command-line tools.",
+ abstract: "iOS Developer Toolkit (Swift) command-line tools.",
discussion: "Device identifiers are always explicit: commands that touch a device require --udid.",
version: ToolkitVersion.current,
subcommands: [Devices.self, Collect.self, InspectIPA.self, Readiness.self, Toolchain.self, DDI.self]
diff --git a/Tests/ToolkitFeaturesTests/SharedFeatureTests.swift b/Tests/ToolkitFeaturesTests/SharedFeatureTests.swift
index 93b4721..22526bb 100644
--- a/Tests/ToolkitFeaturesTests/SharedFeatureTests.swift
+++ b/Tests/ToolkitFeaturesTests/SharedFeatureTests.swift
@@ -364,3 +364,23 @@ struct ReadinessShortcutTests {
#expect(prepared.risk == .readOnly)
}
}
+
+@Suite("Developer Image page")
+struct DeveloperImagePageTests {
+ @Test func hasItsOwnPageNextToDevice() {
+ let all = Workspace.allCases
+ #expect(all.firstIndex(of: .developerImage) == all.firstIndex(of: .device)! + 1)
+ #expect(Workspace.developerImage.group == .device)
+ #expect(Workspace.developerImage.title == "Developer Image")
+ #expect(Workspace(rawValue: "developerImage") == .developerImage)
+ // It is reachable by keyboard like every other page.
+ #expect(KeyboardShortcutReference.sections.flatMap(\.entries).contains { $0.title == "Developer Image" })
+ }
+
+ @Test func everyMountMethodIsExplained() {
+ let explanations = DeveloperImageMechanism.allCases.map(\.explanation)
+ #expect(Set(explanations).count == DeveloperImageMechanism.allCases.count)
+ #expect(explanations.allSatisfy { $0.count > 40 })
+ #expect(DeveloperImageMechanism.native.explanation.contains("online"))
+ }
+}
diff --git a/docs/PHYSICAL_DEVICE_TEST_PROTOCOL.md b/docs/PHYSICAL_DEVICE_TEST_PROTOCOL.md
index 727d978..4f5cac0 100644
--- a/docs/PHYSICAL_DEVICE_TEST_PROTOCOL.md
+++ b/docs/PHYSICAL_DEVICE_TEST_PROTOCOL.md
@@ -58,7 +58,7 @@ of issues and pull requests. Report results as **passed**, **failed**, **not app
## Stage 3 — with Xcode and Developer Mode
-1. **Developer image** card: note the state and details (iOS, build, model, chip/board). Expect
+1. **Developer Image** page: note the state and details (iOS, build, model, chip/board). Expect
*Personalization required* or *Available* on iOS 17+, *Available* or *Missing* on iOS 16 and earlier.
2. **Mount Developer Image** with *Mount with: Built-in*. Expect *Mounted*. Record whether Apple
personalization was needed. Then **Mount Developer Image** again: nothing should be uploaded.
diff --git a/docs/release-verification.md b/docs/release-verification.md
index 323337b..66b0380 100644
--- a/docs/release-verification.md
+++ b/docs/release-verification.md
@@ -7,9 +7,9 @@ a GitHub-hosted macOS runner:
| File | Contents |
|---|---|
-| `iOS-Developer-Toolkit-VERSION-macOS-universal.zip` | The app (arm64 + x86_64), ad-hoc signed with the hardened runtime, with `idt` in `Contents/MacOS` and dependency licenses in `Contents/Resources/Licenses` |
+| `iOS-Developer-Toolkit-Swift-VERSION-macOS-universal.zip` | The app (arm64 + x86_64), ad-hoc signed with the hardened runtime, with `idt` in `Contents/MacOS` and dependency licenses in `Contents/Resources/Licenses` |
| `SHA256SUMS.txt` | SHA-256 of every release file |
-| `iOS-Developer-Toolkit-VERSION.spdx.json` | SPDX 2.3 SBOM of the Swift package dependencies, generated from `Package.resolved` |
+| `iOS-Developer-Toolkit-Swift-VERSION.spdx.json` | SPDX 2.3 SBOM of the Swift package dependencies, generated from `Package.resolved` |
GitHub build-provenance and SBOM attestations are published for the ZIP. The app is **not**
notarized by Apple.
@@ -18,15 +18,15 @@ notarized by Apple.
```bash
shasum -a 256 -c SHA256SUMS.txt --ignore-missing
-gh attestation verify iOS-Developer-Toolkit-VERSION-macOS-universal.zip --repo hideouts-io/iOS-Developer-Toolkit
+gh attestation verify iOS-Developer-Toolkit-Swift-VERSION-macOS-universal.zip --repo hideouts-io/iOS-Developer-Toolkit-Swift
```
After unzipping:
```bash
-codesign --verify --deep --strict --verbose=2 "iOS Developer Toolkit.app"
-codesign --display --verbose=2 "iOS Developer Toolkit.app" # expect: Signature=adhoc, flags=0x10002(adhoc,runtime)
-lipo -archs "iOS Developer Toolkit.app/Contents/MacOS/iOS Developer Toolkit" # expect: x86_64 arm64
+codesign --verify --deep --strict --verbose=2 "iOS Developer Toolkit (Swift).app"
+codesign --display --verbose=2 "iOS Developer Toolkit (Swift).app" # expect: Signature=adhoc, flags=0x10002(adhoc,runtime)
+lipo -archs "iOS Developer Toolkit (Swift).app/Contents/MacOS/iOS Developer Toolkit (Swift)" # expect: x86_64 arm64
```
Then open the app with Control-click › **Open** (or **Open Anyway** in *System Settings › Privacy &
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index de79c76..97cd777 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -9,8 +9,8 @@
| Listed, but “not trusted” | Pairing | Unlock, reconnect, tap **Trust** (**Device › Reconnect a Device…** walks through this and watches for the device for 30 seconds). If no prompt appears: *Settings › General › Transfer or Reset › Reset › Reset Location & Privacy* |
| Developer features unavailable | Developer Mode | *Settings › Privacy & Security › Developer Mode*. If the switch is missing, connect the device to Xcode once |
| “Needs Xcode” | Xcode | Install Xcode, open it once, and select it in *Xcode › Settings › Locations › Command Line Tools*. Run **Tool Reference › Toolchain Check** |
-| Developer image not mounted | Developer image | The **Developer image** card on the Device page names the problem and the fix. Keep the device unlocked and on USB; on iOS 17 and later keep the Mac online (Apple personalizes the image). If one route fails, try the other under **Options › Mount with** |
-| Developer image “Missing” or “Incompatible” | Host image | iOS 17+: update Xcode and open it once (it installs `/Library/Developer/DeveloperDiskImages/iOS_DDI`). iOS 16 and earlier: add a folder with `DeveloperDiskImage.dmg` and `.signature` for the exact version (**Options › Add Image Folder…**) |
+| Developer image not mounted | Developer image | The **Developer Image** page names the problem and the fix. Keep the device unlocked and on USB; on iOS 17 and later keep the Mac online (Apple personalizes the image). If one route fails, try the other under **How to mount** |
+| Developer image “Missing” or “Incompatible” | Host image | iOS 17+: update Xcode and open it once (it installs `/Library/Developer/DeveloperDiskImages/iOS_DDI`). iOS 16 and earlier: add a folder with `DeveloperDiskImage.dmg` and `.signature` for the exact version (**Add Image Folder…** on the Developer Image page) |
| Network device missing | CoreDevice | Pair it with Xcode over USB first, keep it on the same network, and refresh (⌘R) |
| Simulator missing | simctl | Install a simulator runtime in *Xcode › Settings › Components* |
| “Safari Web Inspector did not answer” | Web Inspector | Turn on *Settings › Apps › Safari › Advanced › Web Inspector* (Settings › Safari › Advanced before iOS 18). If it is on, wait ten seconds: the device accepts a new inspection session only about every ten seconds |
@@ -46,5 +46,5 @@ the technical details are in **Help › Diagnostic Log**.
log show --last 10m --predicate 'subsystem == "io.hideouts.iOSDeveloperToolkit"' --info
```
-Ask in [GitHub Discussions](https://github.com/hideouts-io/iOS-Developer-Toolkit/discussions)
+Ask in [GitHub Discussions](https://github.com/hideouts-io/iOS-Developer-Toolkit-Swift/discussions)
and follow [SUPPORT.md](../SUPPORT.md) before sharing any output.
diff --git a/iOSDeveloperToolkit.xcodeproj/project.pbxproj b/iOSDeveloperToolkit.xcodeproj/project.pbxproj
index 3e9ad4b..f41ca66 100644
--- a/iOSDeveloperToolkit.xcodeproj/project.pbxproj
+++ b/iOSDeveloperToolkit.xcodeproj/project.pbxproj
@@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
0046784D38773E756D16B381 /* DeviceViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 192082EDB2AA8A31B4DE9A43 /* DeviceViews.swift */; };
0E51C7116150795B48ED38A0 /* ReadinessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7E525F882B8476846CFA326 /* ReadinessView.swift */; };
+ 1061CDB40FBAC1EEE77206D5 /* DeveloperImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1B9D11BA0887FAFECDC9DF /* DeveloperImageView.swift */; };
1623B3509BB855616EA203F0 /* iOSDeveloperToolkitApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FC5D72518E6E2101A0B339B /* iOSDeveloperToolkitApp.swift */; };
16410705637BF3C36BD60ADB /* LocationViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC558873395BD2D10988B49F /* LocationViews.swift */; };
1BEAAB3DA748A2FF696A37BE /* LiveLogsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1BCA4BFD074C07318C65CC /* LiveLogsModel.swift */; };
@@ -65,6 +66,7 @@
C6641462093807C7726C77E6 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
CD1BCA4BFD074C07318C65CC /* LiveLogsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveLogsModel.swift; sourceTree = ""; };
D34F678B9113D08044E08644 /* FeatureModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureModels.swift; sourceTree = ""; };
+ DC1B9D11BA0887FAFECDC9DF /* DeveloperImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperImageView.swift; sourceTree = ""; };
E7E525F882B8476846CFA326 /* ReadinessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadinessView.swift; sourceTree = ""; };
EC558873395BD2D10988B49F /* LocationViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationViews.swift; sourceTree = ""; };
EF4B82A9EAB2FEBF4FBE2FA2 /* AppsViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsViews.swift; sourceTree = ""; };
@@ -132,6 +134,7 @@
EF4B82A9EAB2FEBF4FBE2FA2 /* AppsViews.swift */,
C6641462093807C7726C77E6 /* ContentView.swift */,
B37EBEC3A6576895CA18665A /* DataViews.swift */,
+ DC1B9D11BA0887FAFECDC9DF /* DeveloperImageView.swift */,
192082EDB2AA8A31B4DE9A43 /* DeviceViews.swift */,
EC558873395BD2D10988B49F /* LocationViews.swift */,
B81EECBCB7D517436001EFF8 /* LogViews.swift */,
@@ -291,6 +294,7 @@
743B753A384188F88D118565 /* ContentView.swift in Sources */,
ACAA69DD86A135192765E423 /* DataViews.swift in Sources */,
74E2D8771554772F6F5085DB /* DeveloperImageModel.swift in Sources */,
+ 1061CDB40FBAC1EEE77206D5 /* DeveloperImageView.swift in Sources */,
0046784D38773E756D16B381 /* DeviceViews.swift in Sources */,
9F456A81A2AF3E4B9BA9E1A0 /* FeatureModels.swift in Sources */,
1BEAAB3DA748A2FF696A37BE /* LiveLogsModel.swift in Sources */,
@@ -366,7 +370,7 @@
"ONLY_ACTIVE_ARCH[config=Debug]" = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.hideouts.iOSDeveloperToolkit;
PRODUCT_MODULE_NAME = iOSDeveloperToolkitApp;
- PRODUCT_NAME = "iOS Developer Toolkit";
+ PRODUCT_NAME = "iOS Developer Toolkit (Swift)";
SDKROOT = macosx;
SWIFT_TREAT_WARNINGS_AS_ERRORS = YES;
};
@@ -392,7 +396,7 @@
"ONLY_ACTIVE_ARCH[config=Debug]" = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.hideouts.iOSDeveloperToolkit;
PRODUCT_MODULE_NAME = iOSDeveloperToolkitApp;
- PRODUCT_NAME = "iOS Developer Toolkit";
+ PRODUCT_NAME = "iOS Developer Toolkit (Swift)";
SDKROOT = macosx;
SWIFT_TREAT_WARNINGS_AS_ERRORS = YES;
};
diff --git a/project.yml b/project.yml
index 961cb44..a5e963d 100644
--- a/project.yml
+++ b/project.yml
@@ -35,7 +35,7 @@ targets:
product: ToolkitFeatures
settings:
base:
- PRODUCT_NAME: iOS Developer Toolkit
+ PRODUCT_NAME: "iOS Developer Toolkit (Swift)"
PRODUCT_MODULE_NAME: iOSDeveloperToolkitApp
PRODUCT_BUNDLE_IDENTIFIER: io.hideouts.iOSDeveloperToolkit
INFOPLIST_FILE: App/iOSDeveloperToolkit/Info.plist
diff --git a/scripts/build-release.sh b/scripts/build-release.sh
index ebf7c36..921a124 100755
--- a/scripts/build-release.sh
+++ b/scripts/build-release.sh
@@ -8,12 +8,12 @@
# other OUTPUT_DIR must be empty or absent.
#
# Produces, in OUTPUT_DIR:
-# iOS-Developer-Toolkit-VERSION-macOS-universal.zip the app (arm64 + x86_64), ad-hoc signed
+# iOS-Developer-Toolkit-Swift-VERSION-macOS-universal.zip the app (arm64 + x86_64), ad-hoc signed
# with the hardened runtime; idt is at
# Contents/MacOS/idt and dependency licenses,
# notices, and the SBOM are in
# Contents/Resources/Licenses
-# iOS-Developer-Toolkit-VERSION.spdx.json SPDX 2.3 SBOM from Package.resolved
+# iOS-Developer-Toolkit-Swift-VERSION.spdx.json SPDX 2.3 SBOM from Package.resolved
# SHA256SUMS.txt checksums of both files
set -euo pipefail
@@ -50,7 +50,7 @@ CACHE="$ROOT/build-output/release-cache"
WORK="$(mktemp -d "${TMPDIR:-/tmp}/idt-release.XXXXXX")"
trap 'rm -rf "$WORK"' EXIT
-NAME="iOS-Developer-Toolkit-$VERSION"
+NAME="iOS-Developer-Toolkit-Swift-$VERSION"
ZIP="$OUT/$NAME-macOS-universal.zip"
SBOM="$OUT/$NAME.spdx.json"
ENTITLEMENTS="App/iOSDeveloperToolkit/iOSDeveloperToolkit.entitlements"
@@ -63,7 +63,7 @@ xcodebuild -project iOSDeveloperToolkit.xcodeproj -scheme iOSDeveloperToolkit \
-derivedDataPath "$CACHE/DerivedData" -archivePath "$WORK/app.xcarchive" \
ARCHS="arm64 x86_64" ONLY_ACTIVE_ARCH=NO \
-quiet archive
-APP="$WORK/app.xcarchive/Products/Applications/iOS Developer Toolkit.app"
+APP="$WORK/app.xcarchive/Products/Applications/iOS Developer Toolkit (Swift).app"
[[ -d "$APP" ]] || fail "the archive does not contain the app"
step "Building idt (arm64 + x86_64, release)"
@@ -105,7 +105,7 @@ verify_app() {
local app="$1"
codesign --verify --deep --strict "$app" || fail "codesign verification failed for $app"
local details
- for binary in "$app/Contents/MacOS/iOS Developer Toolkit" "$app/Contents/MacOS/idt"; do
+ for binary in "$app/Contents/MacOS/iOS Developer Toolkit (Swift)" "$app/Contents/MacOS/idt"; do
details="$(codesign --display --verbose=2 "$binary" 2>&1)"
grep -q 'Signature=adhoc' <<<"$details" || fail "$(basename "$binary") is not ad-hoc signed"
grep -Eq 'flags=0x[0-9a-f]+\(.*runtime' <<<"$details" || fail "$(basename "$binary") lacks the hardened runtime"
@@ -136,7 +136,7 @@ ditto -c -k --sequesterRsrc --keepParent "$APP" "$ZIP"
step "Verifying the ZIP"
mkdir "$WORK/unzipped"
ditto -x -k "$ZIP" "$WORK/unzipped"
-verify_app "$WORK/unzipped/iOS Developer Toolkit.app"
+verify_app "$WORK/unzipped/iOS Developer Toolkit (Swift).app"
(cd "$OUT" && shasum -a 256 -c SHA256SUMS.txt)
step "Done: $OUT"
diff --git a/scripts/check-layout.sh b/scripts/check-layout.sh
index 52fdf24..6456464 100755
--- a/scripts/check-layout.sh
+++ b/scripts/check-layout.sh
@@ -12,7 +12,7 @@ for size in 1180x700 900x560; do
cat "$out/window-geometry.txt"; echo
if [[ -e "$out/TIMEOUT" ]]; then echo "::error::screenshot harness timed out at $size"; exit 1; fi
pages="$(grep -c ': window (' "$out/window-geometry.txt" || true)"
- if (( pages < 14 )); then echo "::error::only $pages pages rendered at $size"; exit 1; fi
+ if (( pages < 15 )); then echo "::error::only $pages pages rendered at $size"; exit 1; fi
if grep -E 'SQUEEZED|OVERFLOW' "$out/window-geometry.txt"; then echo "::error::layout problem at $size"; exit 1; fi
done
echo "layout OK"
diff --git a/scripts/generate-sbom.swift b/scripts/generate-sbom.swift
index 873c27b..8b10d53 100644
--- a/scripts/generate-sbom.swift
+++ b/scripts/generate-sbom.swift
@@ -73,7 +73,7 @@ var packages: [[String: Any]] = [[
"SPDXID": appID,
"name": "iOS Developer Toolkit",
"versionInfo": version,
- "downloadLocation": "git+https://github.com/hideouts-io/iOS-Developer-Toolkit.git@\(commit)",
+ "downloadLocation": "git+https://github.com/hideouts-io/iOS-Developer-Toolkit-Swift.git@\(commit)",
"licenseConcluded": "MIT",
"licenseDeclared": "MIT",
"copyrightText": "Copyright (c) 2026 hideouts-io",
@@ -114,7 +114,7 @@ let document: [String: Any] = [
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "iOS-Developer-Toolkit-\(version)",
- "documentNamespace": "https://github.com/hideouts-io/iOS-Developer-Toolkit/spdx/\(version)/\(commit)",
+ "documentNamespace": "https://github.com/hideouts-io/iOS-Developer-Toolkit-Swift/spdx/\(version)/\(commit)",
"creationInfo": [
"created": formatter.string(from: Date()),
"creators": ["Tool: scripts/generate-sbom.swift", "Organization: hideouts-io"],