Xcodegen — Getting Deeper

David Martinez
6 min readJan 4, 2024

In our first article about xcodegen (that you could read here) we covered:

  • How we use xcodegen.
  • A brief example illustrating its usage.
  • The pros and cons of adopting xcodegen.

Now, in this fresh piece, we want to delve deeper into this remarkable tool and present a more complex exercise. Fancy joining us :)?

Here are the elements we aim to create or explore:

  • Construct a project supporting various variants (Staging and Production), with different certificates, configurations, provision profiles, and bundle configurations.
  • Use xcconfig files to configure our project.
  • Integrate an Appex Notification extension with OneSignal tool.

Ok!, if you ready … let’s rock!

A few things before start

Regardless of whether you use Xcodegen, it’s beneficial to pause and ponder the various aspects of the project:

  • How should I structure my code?
  • Will I use xcconfig files to specifying my configurations?
  • What will my distribution model look like?
  • … and so forth.

This tutorial serves as a didactic study case, you may not necessarily require to segregate your project into multiple modules or create variants. Each project is unique — its size, the expertise level of the development team, its growth trajectory, and the time the team has at its disposal all help define your workspace.

Be smart about it, glean ideas from your knowledge pool, this article (and others alike), chatGPT, and so forth. Start simple, and expand as necessary ✌️.

Note: This article maybe requieres a little understanding about yaml language specification and how xcodegen works.

What we need?

Let’s imagine that we’ve analyzed our project and decided to structure it as follows:

  • Framework UICore: With our amazing UI features.
  • Framework CommuncationsCore: To perform our http requests (using Alamofire, …)
  • MyApplication (SwiftUI)
  • MyApplicationNotificationExtension (also SwiftUI)

The project will distribute into two separate bundles (one for Staging and one for Production):

  • com.atenea.myapplication
  • com.myclient.myapplication

And the corresponding notification extension bundles would be:

  • com.atenea.myapplication.notification
  • com.myclient.myapplication.notification

Finally we want to configure all of these contents using:

  • Info.plist files (with its common usage)
  • entitlements files (with its common usage)
  • xcconfig files (to store the real bundle, development teams, and so other variables)

Our modules will have the following folder structure:

MyModuleName
— | Configs
— — | entitlements
— — — | <environment>.entitlements
— — | xcconfig
— — — | <file>.xcconfig
— — | …
— — | Info.plist
— | Resources
— — | xccassets, …
— | Sources
— — | yourcode
— | config.yml →
the xccodegen spec for the module

Ok! we have all the ingredients, so … we will start working!

Let’s goooo!

Due to this example is so much bigger that previews one, we try to explain, step by step the content that you will find in the result exercise located at github here.

Create the .gitignore file

First of all, our beloved .gitignore file that excludes the xcodeproj file and include this 🙂, to all of ourselves.

Creating our xcconfig files

As defined earlier, we’ll use xcconfig files for some project constants, for example:

  • the application bundle (MYAPP_BUNDLE_ID)
  • the application version or marketing version (MYAPP_VERSION)
  • the development team code (DEVELOPMENT_TEAM)
  • the provision profile Id (MYAPP_NOTIFICATION_PROVISIONING_PROFILE_SPECIFIER)
  • ….

By leveraging inheritance, the xcconfig files will be divided into the following:

  • common.xcconfig → houses version, build and base code sign
  • staging.xcconfig → contains app name, app icon, bundle identifier for staging, entitlements location, google plist information, notification extension bundle an entitlements
  • production.xcconfig → same us stagging.xcconfig but for our production app
  • staging-debug, staging-release, production-debug, production-release → provision profiles ids and code sign for each distribution mode

In this gist you will see find how we’ve configured staging (debug/release) environments using this architecture:

Each file includes specifications that can be inherited from other xcconfig files. For instance:

  • The app version remains uniform across all environments, hence we include the app version (MY_APP_VERSION) in the root xcconfig
  • … whereas the name of the provision profile differs for every environment (Staging / Production) and destination (debug / release) and hence we have a unique MYAPP_PROVISIONING_PROFILE_SPECIFIER for each of them.

Fragmentation results after Xcodegen configuration are visible in these screenshots:

Bundle identifiers/AppIcon for each environement but one version only
Signing specified for each environment

Create the folder base for each module

As the section header indicates, devise your folder structure and add base files for each module. You should end up with something like:

folder base

Time for xcodegen and yml files!

If you have extensive projects with multiple modules, dependencies, configurations … and you try to use a unique yaml file, maybe you find something this:

Splitting our yamls (or configuration) files is a great practise for lager projects as this simplifies reading. In our example we achieve this by creating the subsequent yaml files:

  • Main project.yml (in the root folder) that links all the other yamls.
  • packages.yml containing all the packages our project will use (e.g., Firebase, OneSignal,…)
  • A config.yml file for each module we need (UICore, CommunicationsCore, app, app notification extension)

Let’s take a look over our project.yml:

project.yml

(1) → At first we found the our xcodeproj project name
(2) → Some options for configuring xcodegen
(3) → Set the base iPhoneOS target to iOS 15.0
(4) → Finally we include each of the yml in which we splitt every content

Let’s take a look over our package.yml:

packages.yml

This file has nothing special, only the package we need :).

Let’s take a look over our CommunicationsCore.yml:

This file has a loooot parts we need to cover:

(1) → At first, we want to create a new target so we encapsulate all the date inside a “targets:” key
(2) → We then give the CommunicationsCore name and tag, and define it as a framework. This creates an Xcode framework. Lastly, we specify that our framework is compatible with iOS only.
(3) → Under settings, we state the bundle identifier and Info.plist. While it’s possible to share this information via an xcconfig as well, we chose to keep it here for simplicity and to provide another way to specify project properties.
(4) → Now we specify scheme information indicating the test framework that we will create later.
(5) → Add a target framework dependency for Kingfisher library
(6) → We state where the sources are and the folder group where they will be added when Xcodegen creates the framework. Since all groups are the same in our project, we will find a Modules/CommunicationCore folder housing the Sources folder, the Resources folder, and the Info.plist
(7) → Lastly, we specify the test target in the same way that the main target

Let’s take a look over our App/config.yml:

(1) → At first we create our configuration. Each configuration will generate a xcode configuration that could be configured using a xcconfig file as we illustrate here:

(2) → In the same way as the framework, we create the application target but, this time, we choose the application type.
(3) → Due to we want to specify the configuration using xcconfig files, we will link each configuration with the location of desired xcconfig file.
(4) → Now, override the project properties that we want using the xcconfig file keys. Note: remember that if the xcconfig keys has the same name of the build setting (e.g: MARKETING_VERSION) you don’t need to perform this step. Follow this article if you want to learn more about xcconfig files.
(5) → Connect the dependences (including the Notification Extension that must be embed)
(6) → Create two different schemes for each variant configuration
(7) → An example of how to add some command line arguments

The UICore and the NotificationExtension are implemented using the same principles as their counterparts. If you wish to understand them in depth, please peruse them, but they do not present any new information. :).

And that’s all! If you have any questions about this article, please write them in the comments :).

Thanks for reading and happy coding! 🎉

Bibliography

--

--