Xcode 26.4 quietly removed the ability to run app-hosted unit tests against physical iOS 16 devices, and Xcode 27 finally added a Delete Derived Data menu item — which the community correctly read as Apple admitting the IDE corrupts its own caches on a regular basis. Between those two announcements is the entire reality of shipping on Apple platforms in 2026: every minor Xcode release breaks something, and the fixes ship as deprecations instead of bug fixes.
This post is the playbook I use to keep a working build through that churn.
Pin your Xcode versions like they are dependencies
Treat Xcode the way you treat Swift packages or Ruby gems — declare which version each project uses, and never upgrade silently.
Use xcodes to manage multiple Xcode versions
The xcodes CLI (installable via Homebrew) downloads, installs and switches between any released Xcode without going through the App Store.
brew install xcodesorg/made/xcodes xcodes install 26.3 xcodes installed xcodes select 26.3
Record the chosen version in the repo
Add a .xcode-version file at the repo root:
26.3
Then have your bootstrap script call xcodes select based on that file. A new contributor or a fresh CI runner picks up the right Xcode automatically.
Stop fighting DerivedData by hand
Apple finally surfaced Delete Derived Data as a first-class menu item in Xcode 27, which is welcome. But if you’re on anything older — or you want the operation scripted — set up the alias once:
brew install trash
# In ~/.zshrc:
alias ddd='trash ~/Library/Developer/Xcode/DerivedData/*'
trdd() {
trash ~/Library/Developer/Xcode/DerivedData/"$1"-*
}
ddd clears everything; trdd MyApp clears only that project.
When DerivedData isn’t enough
# Swift Package Manager cache trash ~/Library/Caches/org.swift.swiftpm # Module cache trash ~/Library/Developer/Xcode/DerivedData/ModuleCache.noindex
Run those when you get "module compiled with Swift X.Y cannot be imported by Swift X.Z" after switching Xcode versions.
Make xcodebuild reproducible
xcodebuild \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-destination 'generic/platform=iOS' \
-derivedDataPath build/DerivedData \
-clonedSourcePackagesDirPath build/SourcePackages \
-resultBundlePath build/result.xcresult \
CODE_SIGN_STYLE=Manual \
clean build
-derivedDataPath build/DerivedData puts cache artifacts under your repo root, not in ~/Library. Every build starts from a known state.
The iOS 16 test regression
Xcode 26.4 broke app-hosted unit tests on physical iOS 16 devices. Apple’s apparent plan is to drop iOS 16 in Xcode 27 rather than fix it.
Options:
1. Pin to Xcode 26.3 for the test job only. Both Xcodes coexist via xcodes.
2. Run unit tests on the simulator for iOS 16.
When to skip a beta entirely
Signals to wait it out:
– Release notes mention known issues hitting your stack.
– Forum threads on the first beta show packaging or device-side install failures.
– A new “Delete Derived Data” affordance ships — that usually means an underlying cache bug was found late.
– Your dependencies don’t yet have a green CI run against the new version.
Wait for .1 if you can.
A short pre-commit hygiene checklist
xcodes selectto the pinned version from.xcode-version.dddto clear DerivedData.xcodebuild clean buildwith the pinned-derivedDataPath.- Inspect the resulting
xcresultbundle before uploading.
The cost is about two minutes. The cost of finding out at notarization time is hours, sometimes days.
0 thoughts on “How to survive Xcode toolchain regressions without losing a week”