I made Xcode's tests 60 times faster
Time is our most precious resource, as both humans and programmers.
An 8-hour workday contains 480 minutes. Out of the box, running a new iOS app's test suite from the terminal using xcodebuild test
takes over 25 seconds on my M4 MacBook Pro. After extracting my application code into a Swift package—such that the application project itself contains virtually no code at all—running swift test
against the same test suite now takes as little as 0.4 seconds. That's over 60 times faster.
Given 480 minutes, that's the difference between having a theoretical upper bound of 1152 potential actions per day and having 72,000.
If that number doesn't immediately mean anything to you, you're not alone. I've been harping on the importance of tightening this particular feedback loop my entire career. If you want to see the same point made with more charts and zeal, here's me saying the same shit a decade ago:
And yes, it's true that if you run tests through the Xcode GUI it's faster, but (1) that's no way to live, (2) it's still pretty damn slow, and (3) in a world where Claude Code exists and I want to constrain its shenanigans by running my tests in a hook, a 25-second turnaround time from the CLI is unacceptably slow.
Anyway, here's how I did it, so you can too.
The Starting Point
Here's what you get out-of-the-box after creating a new app with the latest OS 26 betas:
- A freshly-generated iOS 26 app (SwiftUI + SwiftData)
- A handful of boilerplate code listings and tests that the wizard creates
- Running tests via
xcodebuild
takes ~25 seconds minimum on my machine (add the UI tests and it runs well over a minute) - Most of that time was Xcode performing code signing, spinning up simulators and doing... whatever Xcode does. Hell, it takes 1-3 seconds for the process to exit after it prints **TEST SUCCEEDED** for some fucking reason
- Want to pick up some speed by running a single Swift Testing function from the CLI? Well, if you want to do that you'll have to read this post to understand why that's impossible and then monkey-patch Apple's test runner
- Think you'll be clever and run
swift test
against your source code instead ofxcodebuild
? Well, you can't. It'll just bark at you that thetest
subcommand is only for Swift packages and not apps
This adds up to a situation in which test-driven development infeasible and using Claude Code unworkable. And this is a Hello World app! It's only going to get slower from here.
So—after consulting with some real iOS developers like Blake McAnally to confirm this isn't a really stupid idea—I went to work extracting the application code into a Swift package, for no other reason than to enable faster tests and better command-line ergonomics with swift test
.
The Future State
It's frustrating as hell that something this stupid actually works:
- Create a Swift package as a framework for all your app's business logic, views, dependencies, everything
- Keep your app target as a thin shim that depends on the package as a framework and calls out to the root view from its entry point
- Test the package with
swift test
- Test UI interactions with
xcodebuild
, presumably less often
This way, instead of firing up a simulator to run headless tests of Swift code, we're only doing so when we actually want to run a full build that includes UI tests.
Here's what the architecture now looks like:
MyApp.app (thin shell)
└── imports MyAppCore framework
├── Models (SwiftData)
├── Views (SwiftUI)
├── Business Logic
└── Tests (Swift Testing)
I'm brand new to modern iOS development, so please tell me if I'm off my rocker here, but as far as I can tell, all this solution really does is introduce a minor Swift package configuration shim between the application project and the code itself, allowing it to be compiled and tested outside the context of my slow and girthy Xcode build.
The Implementation
Okay, now for some more detailed instructions you can follow along at home.
Step 1: Create the framework as a Swift Package
First, create a Swift Package alongside the Xcode project:
// MyAppCore/Package.swift
// swift-tools-version: 6.2
// The swift-tools-version declares the minimum version of Swift required to build this package.
let package = Package(
name: "MyAppCore",
platforms: [.iOS(.v26), .macOS(.v26)],
products: [
.library(name: "MyAppCore", targets: ["MyAppCore"])
],
targets: [
.target(name: "MyAppCore"),
.testTarget(name: "MyAppCoreTests", dependencies: ["MyAppCore"])
]
)
Step 2: Connect the framework to the app
If you don't already have a workspace, you'll need to create one to use both your app and framework together:
- Create a workspace file (if needed):
<!-- MyApp.xcworkspace/contents.xcworkspacedata -->
<?xml version="1.0" encoding="UTF-8"?>
<Workspace version = "1.0">
<FileRef location = "group:MyApp.xcodeproj">
</FileRef>
<FileRef location = "group:MyAppCore">
</FileRef>
</Workspace>
- In Xcode, open the workspace (not the project!)
- Add the framework as a dependency:
- Select the
MyApp
target → "General" → "Frameworks, Libraries, and Embedded Content" - Click "+" and add
MyAppCore
- Select the
Now your app knows it depends on your "core" framework, so any builds will build it as well.
Step 3: Move everything to the framework
Move all your code and (non-UI) test listings from the app target to the newly-created Swift package
Before:
MyApp/
├── MyAppApp.swift
├── ContentView.swift
├── ModelContainerFactory.swift
├── Item.swift
└── Assets.xcassets/
After:
MyApp/
├── MyAppApp.swift # Now just a thin shell
└── Assets.xcassets/ # Resources stay in app
MyAppCore/
├── Package.swift
└── Sources/
└── MyAppCore/
├── ContentView.swift
├── ModelContainerFactory.swift
├── Item.swift
└── MyAppRootView.swift
Only the APIs your app actually references need to be made public
. Internal types stay internal
, and your tests can access them with @testable import
.
Step 4: Expose the root view from the framework
The framework exposes a simple root view that the app can inject the SwiftData model container into and then attach to the window group:
public struct MyAppRootView: View {
private let modelContainer: ModelContainer
public init(modelContainer: ModelContainer) {
self.modelContainer = modelContainer
}
public var body: some View {
ContentView()
.modelContainer(modelContainer)
}
}
Step 5: Slim Down the App
The app target is now an empty shell. All that's left is to add the package's root view to the application window group:
import SwiftUI
import MyAppCore
@main
struct MyAppApp: App {
let modelContainer: ModelContainer
init() {
do {
modelContainer = try ModelContainerFactory.createContainer()
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}
var body: some Scene {
WindowGroup {
MyAppRootView(modelContainer: modelContainer)
}
}
}
Step 6: Run your tests
If you fancy a single script/test
file, you could do something like this to give yourself a convenient way to run just your unit tests:
#!/bin/sh
case "$1" in
unit)
# script/test unit
shift
cd MyAppCore && swift test "$@"
;;
ui)
# script/test ui
shift
xcodebuild -scheme "MyApp-UITests" test "$@"
;;
*)
# script/test
# Run everything - unit tests first (fast), then UI tests
"$0" unit && "$0" ui
;;
esac
Want fast feedback? Just run script/test unit
. Want to run a single test? That's just script/test unit --filter testFunctionName
Step 7: Enjoy better output
Because Swift Testing is a modern tool, its output is actually reasonable. Meanwhile, xcodebuild
barfs out an endless stream of gcc-lookin', PTSD-triggering nonsense.
Running swift test
for a fresh Xcode project outputs this:
$ swift test
Running framework unit tests with swift test...
Building for debugging...
[1/1] Write swift-version-39B54973F684ADAB.txt
Build complete! (0.07s)
Test Suite 'All tests' started at 2025-07-25 20:15:48.726.
Test Suite 'All tests' passed at 2025-07-25 20:15:48.727.
Executed 0 tests, with 0 failures (0 unexpected) in 0.000 (0.001) seconds
◇ Test run started.
↳ Testing Library Version: 1082
↳ Target Platform: arm64e-apple-macos14.0
◇ Test itemPersistence() started.
◇ Test configurationCreatesModelContainer() started.
◇ Test contentViewInitializes() started.
◇ Test rootViewInitializesWithContainer() started.
◇ Test frameworkInitializes() started.
◇ Test rootViewInitializesWithConfiguration() started.
◇ Test itemCreation() started.
✔ Test frameworkInitializes() passed after 0.001 seconds.
✔ Test itemCreation() passed after 0.001 seconds.
✔ Test configurationCreatesModelContainer() passed after 0.013 seconds.
✔ Test rootViewInitializesWithContainer() passed after 0.013 seconds.
✔ Test rootViewInitializesWithConfiguration() passed after 0.014 seconds.
✔ Test contentViewInitializes() passed after 0.014 seconds.
✔ Test itemPersistence() passed after 0.014 seconds.
✔ Test run with 7 tests in 0 suites passed after 0.014 seconds.
Not perfect, but at least comprehensible. Which is more than can be said of xcodebuild
.
What's the catch?
When will this bite me in the ass? We'll find out. I'm told this will complicate static resource handling (e.g. images) a bit, and some Xcode features could get grumpy, but overall it sounds like there are a lot of teams doing essentially the same thing—they're just not necessarily blogging through it like I am.
Testing tools have never been Apple's forte, so all we can do is hope Apple leans into this transition and elevates swift test
to the default way to run non-UI application tests before too long. Hopefully they also have plans to introduce a new project build tool for modern Swift apps that discards the legacy xcodebuild
cruft.
What about you? Got your own hot Apple development tips? Let's hear'em.