adam tecle

Configuring bundle ID and app icon with xcconfigs and tuist

Sep 12, 2023

What are build settings?

Apart from all the Swift and/or Objective-C code that makes an iOS app function, there’s a separate area of concern that deals with the details of how that particular app is compiled, packaged, and prepared to be run on a device - that area of concern is encapsulated by Xcode build settings. What version of the Swift language to use? What compiler warnings to enable? Where should the compiler search for C/Obj-C header files? These are a few of the types of details you’d configure in build settings.

To change a build setting without any separate tooling on an iOS project, you’d dive into Xcode and change something directly within the IDE. Each time you change a build setting within the IDE, you’ll end up producing a diff in the the .pbxproj file or other related files. These files are prone to git conflicts in a team environment.

As a project grows, you may want to move from having a monolithic app target to having code split up into loosely coupled and separately compiled modules, which increases the complexity of your build process. Something that would be nice is to have some way of templating the build settings for each new module in order to make that process as repeatable and as automated as possible. We can accomplish this with some tooling.

What’s tuist?

I was introduced to tuist at a previous role and for the first time, dealing with build configurations on iOS projects felt a lot more manageable.

The core idea is that you describe your project in code, and you dynamically generate .xcodeproj and .xcworkspace files - which can now be added to a .gitignore. Every detail of how your built product is configured is specified either through a nice Swift DSL, xcconfigs, or some other humanly grokkable format.

I wanted to write a quick post about one simple and practical tuist use case: you’d like to use a different bundle ID and app icon for your debug build and your release build. One reason why these two changes together are useful is because they’ll allow you to have a debug build and a release build installed simultaneously on your device, and be able to distinguish them from one another visually.

Extracting xcconfigs

Assuming that you’ve already configured a basic project in tuist, you’d start by extracting existing build settings for all your app targets and projects xcconfig files.

# Extract target build settings
tuist migration settings-to-xcconfig -p Project.xcodeproj -t MyApp -x MyApp.xcconfig

# Extract project build settings
tuist migration settings-to-xcconfig -p Project.xcodeproj -x MyAppProject.xcconfig

Put these files somewhere that makes sense for your project - for me, it was RootProjectFolder/Modules/MainAppModule/Configs.

Then, I split up shared target build settings into one file named “Base.xcconfig”. “Debug.xcconfig” and “Release.xcconfig” will import the base file using the #include directive.

In tuist, you’d specify the xcconfig for the project within the Project initializer:

return Project(
    name: name,
    organizationName: "orgname",
    options: options,
    packages: packages,
    settings: Settings.settings(
        configurations: [
            .debug(name: "Debug", xcconfig: .relativeToRoot("Modules/MyProject/Configs/Project.xcconfig")),
            .release(name: "Release", xcconfig: .relativeToRoot("Modules/MyProject/Configs/Project.xcconfig")),
        ],
        defaultSettings: .none
    ),
    targets: targets,
    schemes: [
        makeDebugScheme(),
        makeReleaseScheme(),
    ]
)

Similarly for your target (unrelated configuration omitted):

Target(
    name: name,
    platform: platform,
    product: .app,
    bundleId: "$(PRODUCT_BUNDLE_IDENTIFIER)",
    settings: .settings(
        configurations: [
            .debug(name: "Debug", xcconfig: "Configs/Debug.xcconfig"),
            .release(name: "Release", xcconfig: "Configs/Release.xcconfig")
        ],
        defaultSettings: .none
    )
)

Notice that $(PRODUCT_BUNDLE_IDENTIFIER)? This is a variable defined in your target’s xcconfig file - set this to the appropriate bundleID for that particular target. That should take care of configuring bundle IDs.

For separate app icons, first define a variable in Base.xcconfig:

BASE_ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon

Then in your Debug.xcconfig:

ASSETCATALOG_COMPILER_APPICON_NAME=$(BASE_ASSETCATALOG_COMPILER_APPICON_NAME)-Debug

And in release:

ASSETCATALOG_COMPILER_APPICON_NAME=$(BASE_ASSETCATALOG_COMPILER_APPICON_NAME)

Notice that we don’t need to append a suffix here, since the release AppIcon is titled AppIcon.

And after another call to tuist generate, we’re done.


Final thoughts

Even on a small app, tuist has been a big QoL increase for me - I don’t enjoy menu diving in an IDE and I’d much rather have my configuration as code. It’s one of those tools that I’ll now always seriously consider reaching for as an early technical decision when starting a new project.