Splitting a Xcode project with SPM

I was talking on Mastodon about splitting an Xcode project into smaller pieces. Here's an elaboration.

Background: With SwiftUI previews you want small focused Xcode schemes. The larger your scheme, the less likely the preview is to succeed. You also get faster compilation if you don't always build the whole app. You can get smaller schemes with framework build targets or with Swift Package Manager. Frameworks are a pain in the ass, SPM mostly isn't.

A note about the confusing terminology, in case you're not familiar with both Xcode and SPM: Xcode has projects, targets and schemes. SPM has products and targets. SPM products show up in the scheme selector of Xcode.

Here's how you set up my ideal Xcode app project:

Create a root directory for your project. This is where you have your .git folder or equivalent. In the root directory, create a new Xcode project, so you have root/MyApp/MyApp.xcodeproj and next to it root/MyApp/MyApp/ for the app files.

Then create a SPM package directory next to MyApp, something like root/MyAppCore/. Run swift package init --type library in it. Now open root in Finder and drag MyAppCore into MyApp in the Xcode Project Navigator sidebar (you don't need a workspace if you drop it onto the project.) After this you should have two entries under the MyApp project in Xcode: MyAppCore and MyApp. You should also see MyAppCore in the Xcode scheme selector.

After this you only have to add MyAppCore to the app build target in the Frameworks, Libraries and Embedded Content section of the project editor's General tab, and you're good to go.

With a setup like this, you can keep very little code on the app side, and put almost everything in the SPM package. You'll probably need your SwiftUI App, app delegate or scene delegate, depending on what and how you're building, on the app side, but other than that, just shove everything into the package. Every file you add to the package instead of the app is one change to the project file you avoided.

Ideally you want a wide but shallow SPM project structure with as many targets as is necessary, but structured so that they don't build up into deep dependency trees. You don't need to create a SPM product for each target; it's enough to create a projects as you need them from previews or for when you want to have Xcode compile just one part of the package.

To make imports easier in the app, you can use the MyAppCore product as an umbrella module. That means the MyAppCore target depends on every other target in the package, and then in MyAppCore/MyAppCore.swift calls @_exported import on all of them. @_exported is an underscored attribute and thus may break at some point, but it doesn't seem like a huge risk. So in Package.swift you can have this:

.target(
    name: "MyAppCore",
    dependencies: [
        "DB",
        "Networking",
        "PreferencesUI",
        /* all the other targets */
    ]
),

and then in MyAppCore.swift:

@_exported import DB
@_exported import Networking
@_exported import PreferencesUI
/* all the other targets */

Now when you do import MyAppCore on the app side, you get everything.

With a setup like you rarely need to touch the Xcode project file. It reduces the need for a tool like XcodeGen, which is priceless on a more monolithic project. If you want to avoid the project editor completely, I recommend looking into it.

© Juri Pakaste 2024