Our Journey to Generated Projects
The Xcode project file has quite a complicated structure and it brings unnecessary complexity where something simpler could be used instead. Thus we were very intrigued to move to project generation. Project generation has a lot of benefits – not committing Xcode files means fewer merge conflicts (truth be told, merge conflicts have become quite rare now!) and also clearer project settings. Unless you are a wizard ????♂️, you probably cannot read a project file fluently (and even if you can, it is still very hard to get familiarized with it).
For project generation we needed a third-party tool to actually generate the project. tuist turned out to be a great choice. Being able to configure our projects in Swift has brought us a lot of flexibility. Tuist is easy to use and extremely powerful at the same time!
In this blog post I’d like to go through the more complicated parts of integrating our own project template with tuist. In case you want to take a deeper look, the whole setup is available in this repository.
Goal
Before starting the process of setting up tuist we needed to define what we *want* from the setup. For now we use one `xcodeproj` per project, so we can keep our configuration quite specific, though extending it should be possible (e.g. modular architecture). In `Project.swift`, there should only be the configuration that differs from the template – for example specific dependencies or project name. This is enabled by project description helpers. Most of the configuration will be in the `Tuist` directory where the helpers reside. Defining a new project should now be a matter of a few lines ✨.
Project Template
As I have noted, we want most of the configuration to be done with description helpers. To achieve that we can do:
extension Project {
public static func project(name: String) -> Project {
Project(name: name)
}
}
This `static func` can then be used in our `Project.swift`. We can add additional parameters for higher configurability when we need it. My advice is to start with a single `Project` and one `Target` for which you first add `sources` and `resources`. Leave the configurations, schemes and build phases for later – try to make the project compilable first and tie up loose ends later. That being said, it should be quite a trivial task, so I’ll just skip to the configurations ????
Configurations
In our apps we use five different app configurations (Debug, Beta-Development, Beta-Stage, Beta-Production and Release) which enables us to switch between different sets of environments during our development. For these configurations we have created an enum AppCustomConfiguration which defines different settings for each configuration. You definitely want to have different settings for Debug and Release.
To our previous extension we can then add:
settings: CustomSettings = CustomSettings(configurations: [.debug,
.betaDevelopment,
.betaStage,
.betaProduction,
.release]
where CustomSettings is just a helper struct for syntax sugar. This setup enables us to change the configurations we use if we do not need `.betaStage` for example. It can also be easily extended to what you need in your project.
Scheme configuration works in a very similar fashion.
Abstraction
The great thing about description helpers is that it is really easy to ensure separation of concerns and even a complicated setup can be easily read since you can use multiple files. Even if you do not need description helpers per se, I still recommend you to use them, so your `Project.swift` is not blown out of proportions.
Code Signing
Tuist defaults to automatic code signing, so if you use manual signing, you are going to need to do some additional configuration. In `Project.Settings` we can add these values:
"CODE_SIGN_STYLE": "Manual"
"PROVISIONING_PROFILE_SPECIFIER": SettingValue(stringLiteral: "match InHouse cz.ackee.enterprise.\(name).\(identifierName)")
As you can see, we use a standardised provisioning profile with the help of fastlane match to make the setup easier. `identifierName` points to the current scheme description (e.g. Debug’s description is `debug`), `name` is the name of our project. The final provisioning profile looks like this in the end: “match InHouse cz.ackee.enterprise.Project.debug”.
In `Target.Settings` we will also need to change `CODE_SIGN_IDENTITY` where for configurations, apart from Debug, we need to set this: `"CODE_SIGN_IDENTITY": "iPhone Distribution"` This will depend on how you use code signing in *your* project, though. This was the part I was most afraid of when I had first started the migration – but it turns out my fears were not justified ????⚖️
Did I Get It Right?
So, as you can see, even more complicated configuration is possible, but sometimes it does take some effort to get it right – to help you in this regard, I can wholeheartedly recommend xcdiff. It is a great tool! With xcdiff you can easily see what the differences between the generated and the old project are. You will probably not achieve having the projects exactly the same, but it is extremely helpful to go through it and try to spot the things that need to be changed. In my experience I usually look at the different settings as the code structure is inherently different and will not tell you much.
Tuist and CI
The greatest headache for us has been running a tuist-generated project on our CI. The reason was quite simple in the end – `tuist generate` only adds the files that already exist but if you generate some files in your build phase, those will not be added – and that means our build would fail ????. To fix this problem we use `Setup.swift`. There we can pre-generate all the files that we generate, so they are always added to the generated project. You can do it in `Setup.swift` as in the following snippet:
let file = “path_to_your_generated_file”
let upCommands: [Up] = [.custom(name: "\(file)'s intermediate directories", meet: ["mkdir", "-p", parentDirectory(for: file)], isMet: ["test", "-e", parentDirectory(for: file)]),
.custom(name: "\(file)", meet: ["touch", file], isMet: ["test", "-e", file])]
It may look complicated but basically all we are doing is creating all intermediate directories, if needed, and then `touch` the file. Then you just need to run `tuist up` before `tuist generate` in your CI setup and you should be good to go.
Our Project.swift
The final configuration is not in the description helpers but in the root folder. This is the easiest part and the final version can look like this:
import ProjectDescription
import ProjectDescriptionHelpers
let project = Project.project(name: "ProjectTemplate",
projectVersion: Version(0, 1, 0),
platform: .iOS,
dependencies: [
.cocoapods(path: "."),
.carthage(name: "ACKategories"),
.carthage(name: "Alamofire"),
.carthage(name: "ReactiveCocoa"),])
So simple, right? ????
Final Thoughts
This has really been just a quick walkthrough of things that we have encountered. Otherwise I shall direct you to tuist documentation which is really exhaustive and covers most of what you need. You can have a look at our template project here. I have to say it has been a pleasure working with tuist so far and it should make our projects more maintainable, but also more *fun* in the future. In other words, I hope you will try it and let us know if you have any more questions! ????