Insights / Developer Tools
Xcode Doctor Case Study: Building a Diagnostic Tool for iOS Developers
iOS developers lose an average of 4–8 hours per App Store rejection cycle caused by Xcode configuration issues. These rejections are almost entirely preventable. This is the case study of why and how Xcode Doctor was built — and what we learned shipping a developer tool for a workflow that Xcode itself doesn't fully diagnose.
The problem
App Store rejection from a configuration issue follows a specific pattern. The submission goes through. 24–48 hours later, Apple returns a rejection with a message that identifies the symptom — binary validation failed, entitlement missing, invalid provisioning profile — but not the underlying cause. The developer spends 2–4 hours investigating, submits a fix, waits another 24–48 hours, and sometimes hits the same issue from a different angle.
The failure modes involved are well-known in the iOS development community. Signing certificate mismatches. Provisioning profiles that expire between development and submission. Entitlements declared in the app but not registered in the provisioning profile. Watch and Widget targets with incorrect extension point identifiers. Missing privacy manifest entries for third-party SDKs.
None of these failures have obvious visual indicators in Xcode before submission. The build succeeds. The archive succeeds. The validation step in the Xcode Organizer sometimes catches them — but not reliably on all Xcode versions. Apple's App Store Review Guidelines specify what disqualifies a submission but not how to verify compliance before submission in a structured way.
The cost in hours is significant. But the cost in timing is worse. Deadline-sensitive releases — coordinated with launch campaigns, investor demos, or conference keynotes — can miss their window entirely because of a signing certificate that expired the week before submission.
The solution: 9 checks in under 2 seconds
The design decision for Xcode Doctor was to focus exclusively on the configuration failure modes that cause App Store rejection — not general code quality, not architecture review, not build performance. A focused tool that does one job well ships faster and gets used more than a comprehensive tool that takes 30 minutes to run.
Xcode Doctor runs 9 checks. Each check is independent, runs in parallel, and completes in under 2 seconds total on a modern Mac. The analysis is purely read-only — nothing is modified, deleted, or submitted. The tool reports findings immediately in a structured results view.
| Check | What it catches |
|---|---|
| Signing certificates | Expired, revoked, or development certificates in a distribution context |
| Provisioning profiles | Expired profiles, entitlement mismatches, device limit coverage |
| Entitlements | Entitlements declared in the app but missing from the provisioning profile |
| File references | Missing or broken file references in the .xcodeproj |
| Watch/Widget targets | Incorrect extension point identifiers and target linking failures |
| Privacy manifest | Missing NSPrivacyAccessedAPITypes entries for required SDK categories |
| Deployment targets | Inconsistent minimum deployment targets across app and extension targets |
| Bundle identifiers | Mismatched bundle IDs between app, extensions, and provisioning profiles |
| Notarization readiness | macOS-specific hardened runtime and entitlement requirements for notarization |
Architecture decisions
Native macOS, not command-line. The decision to build a native macOS app rather than a CLI tool was driven by a technical requirement: Xcode Doctor needs to verify signing certificate validity against the Keychain and check provisioning profile status against the Apple Developer Portal state that is cached locally. These operations require the Security framework and Keychain access APIs — difficult to invoke reliably from a command-line tool without elevated permissions or manual file export steps.
Swift concurrency for parallel checks. Each of the 9 checks is implemented as an async throws function and run in a withTaskGroup for parallel execution. File I/O for reading project files, entitlements, and provisioning profiles is the bottleneck — not CPU. Running all checks concurrently reduces total analysis time from ~8 seconds sequential to under 2 seconds.
func runAllChecks(projectURL: URL) async -> [CheckResult] {
await withTaskGroup(of: CheckResult.self) { group in
group.addTask { await self.checkSigningCertificates(project: projectURL) }
group.addTask { await self.checkProvisioningProfiles(project: projectURL) }
group.addTask { await self.checkEntitlements(project: projectURL) }
group.addTask { await self.checkFileReferences(project: projectURL) }
group.addTask { await self.checkWatchWidgetTargets(project: projectURL) }
group.addTask { await self.checkPrivacyManifest(project: projectURL) }
group.addTask { await self.checkDeploymentTargets(project: projectURL) }
group.addTask { await self.checkBundleIdentifiers(project: projectURL) }
group.addTask { await self.checkNotarizationReadiness(project: projectURL) }
var results: [CheckResult] = []
for await result in group { results.append(result) }
return results
}
}Read-only by design. Every check is read-only. The tool parses project files, reads entitlement plists, queries the Keychain, and inspects provisioning profiles — but never writes to any file. This is an explicit design constraint, not an afterthought. Developer tools that auto-fix configuration issues often create worse situations by applying fixes in unexpected order or based on partial information.
The privacy manifest check: a late addition
Apple's privacy manifest requirements became mandatory for App Store submissions in May 2024. Apps using third-party SDKs in the “required reason APIs” category — including popular analytics, crash reporting, and networking libraries — must include a PrivacyInfo.xcprivacy file with the correct NSPrivacyAccessedAPITypes entries.
Apple's email notifications about missing privacy manifests were sent automatically — but many teams missed them. The rejection message for missing privacy manifest entries was one of the most common new rejection reasons in 2024. Adding a privacy manifest check to Xcode Doctor was prioritized after seeing multiple teams hit this in rapid succession in the first month of enforcement.
The check reads the project's Swift package dependencies and embedded static libraries, compares their declared API access against the known required-reason API categories from Apple's documentation, and flags missing manifest entries with a direct reference to the Privacy Manifest Files specification.
Outcomes from production use
9
Checks per analysis
<2s
Total analysis time
0 bytes
Data sent to any server
The most common finding from production use: provisioning profile entitlement mismatches, followed by privacy manifest gaps from newly added third-party SDKs. Both are common precisely because they are hard to notice without a dedicated check — they don't produce build errors or warnings until the binary is submitted to Apple.
Developers who run Xcode Doctor before each App Store submission report catching at least one issue per submission that would have resulted in a rejection. Eliminating one 48-hour rejection cycle per release is equivalent to recovering roughly a full working day per release cycle.
What we would do differently
Xcode project file parsing is fragile. Apple has never published a formal specification for the .xcodeproj format. The file is a structured plist in a specific encoding — readable, but undocumented. Xcode updates occasionally change the internal format in ways that require parser updates. If we were starting over, we would invest more in defensive parsing and format version detection earlier.
CI integration should have been day-one scope. The most requested feature after launch was a headless mode for running checks in CI pipelines. This was added as a separate command-line companion release, which required duplicating some parsing logic. A better initial design would have separated the analysis engine from the UI layer from the start — a clear violation of our own architecture advice.
The scope constraint was the right call. The temptation to add build performance analysis, test coverage reporting, and SwiftLint integration was real. Keeping Xcode Doctor focused on App Store rejection prevention — and nothing else — is why it loads in under a second and runs in under two. Scope creep in developer tools produces slow tools that no one runs in their workflow.
FAQ
What is Xcode Doctor?↓
Why was Xcode Doctor built as a native macOS app instead of a CLI tool?↓
What Xcode configuration issues does Xcode Doctor catch?↓
Does Xcode Doctor modify any project files?↓
Related reading
App Store submission coming up?
The iOS Architecture Audit includes an App Store compliance check covering the same categories as Xcode Doctor — plus a full system architecture review, AI readiness assessment, and a prioritized roadmap. Delivered in 5 business days.
Apply for an Audit