add project generation to the current application

Do you know what unites all iOS developers working on large projects? We have all once encountered this old friend – the .xcodeproj file, which stores dozens, or even hundreds of conflicts after each merge. We also lived with this problem for many years until we found a solution.

Meet Tuist, a tool that has revolutionized the way we approach project management. It not only helps avoid conflicts, but also automates the generation of key components, making life much easier for the team.

If you still have this “relic of the past”, then perhaps our experience will help you finally get rid of it. Let's figure out how to do this.


Table of contents

  1. We are planning

  2. We analyze the current settings of the project and individual targets

  3. Putting things in order in the settings

  4. Putting things in order in the diagrams

  5. Generating a new project

  6. It's just beginning

We are planning

Before you rush headlong into the pool and switch to Tuist, it’s worth thinking about: do you need it at all? If your main problem is project file conflicts when merging changes from multiple developers, Tuist may be overkill. In such cases, it is easier and faster to use XcodeGen.

We decided to move towards Tuist for several reasons:

  1. Automatic generation of project file.
    We, as true “old believers,” have maintained and edited it manually for years. But the internal initiative to cut the monolith gave rise to so many conflicts that it became a real test for our iOS developers.

  1. Analysis and verification of the dependency graph.
    The Cocoapods we use does not provide this functionality out of the box. We tried to write our solution on our knees, but it did not cover all our needs. Utility graph included with Tuist, it makes it much easier to write your own tool for Impact Analysis.

  1. Caching dependencies and modules.
    Again, Cocoapods doesn't do this by default. We used Rugby for local caching, but this is only a partial solution to the problem.

If your goals are similar to ours, then Tuist may be just the tool you need.

We have created a Roadmap with specific steps and timelines that will help us smoothly transition to Tuist and achieve the tasks described above.

The first step was to study documentation on drafting a project manifesto project.swift for Tuist. This allowed us to understand in advance what needed to be changed in the current project so that the transition would be as painless as possible. This stage became our “zero” step in the Roadmap.

We analyze the current settings of the project and individual targets

Tuist provides a set of utilities that can make project migration much easier. Among them is a utility for uploading current project settings or individual targets to a file xcconfig.

To download project settings, use the following command:

tuist migration settings-to-xcconfig -p MyProject.xcodeproj -x MyProject.xcconfig

As a result of settings from the project MyProject.xcodeproj will be saved to a file MyProject.xcconfig.

To download the settings of an individual target, use a similar command:

tuist migration settings-to-xcconfig -p MyProject.xcodeproj -t MyTarget -x MyTarget.xcconfig

Target settings MyTarget will be saved to a file MyTarget.xcconfig.

To check how the settings uploaded in this way correspond to the actual Xcode settings, create a new project and perform the same upload in it. Next, analyze the difference between the two sets of parameters and their values.

Most likely at this stage you will discover something that requires editing in the current project. For example, in our case these were the settings CLANG_WARN_STRICT_PROTOTYPES и CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELFwhich “jammed” warnings in Objective-C, which still accounts for about 17% of the entire codebase.

Having understood the project and target settings, you can move on to the diagrams.

Putting things in order in the settings

We decided to move all settings to xcconfig files and build such a hierarchy.

Project-wide settings are stored in a file Project.xcconfig. Next comes the basic config Base.xcconfig with general settings for all targets. Here is an example of files from our project:

Project.xcconfig

CN_VERSION = 1.275

CN_VERSION_RS = 1.275

CN_BUILD_NUMBER = 1420

MARKETING_VERSION = $(CN_VERSION)

CURRENT_PROJECT_VERSION = $(CN_BUILD_NUMBER)

VERSIONING_SYSTEM = apple-generic

Base.xcconfig

DEVELOPMENT_TEAM = $(CN_DEV_TEAM)

EXCLUDED_ARCHS = armv7

IPHONEOS_DEPLOYMENT_TARGET = 14.0

SWIFT_VERSION = 5.0

At first glance, these two files are very similar and could be combined. However, we preferred to separate the settings for the entire project and global settings for all targets. In our case, this division is dictated by the fact that during the build process our CI works with Project.xcconfigsubstituting the required parameter values.

What follows is a set of files such as Target1.common.xcconfig, Target2.common.xcconfig etc., which include the base file #include Base.xcconfig and contain general settings for specific targets. An example of general settings for the main target of our project:

Cian.common.xcconfig

#include "Base.xcconfig"

BUILD_LIBRARY_FOR_DISTRIBUTION = NO

CODE_SIGN_ENTITLEMENTS = Cian/CIAN.entitlements

GCC_PRECOMPILE_PREFIX_HEADER = YES

GCC_PREFIX_HEADER = Cian/Cian-Prefix.pch

SWIFT_OBJC_BRIDGING_HEADER = Cian/Cian-Bridging-Header.h

ONLY_ACTIVE_ARCH = YES

Then there are specific settings for individual target configurations, such as Target1.debug.xcconfig, Target2.debug.xcconfig etc., which include the corresponding shared file #include Target.common.xcconfig and contain specific settings. An example of setting up the debug configuration of the main target from our project:

Cian.debug.xcconfig

#include "../../Pods/Target Support Files/Pods-Cian/Pods-Cian.debug.xcconfig"

#include "Cian.common.xcconfig"

#include "Signing.debug.xcconfig"

PROVISIONING_PROFILE_SPECIFIER=

GCC_PREPROCESSOR_DEFINITIONS = $(inherited) $(PODS_GCC_PREPROCESSOR_DEFINITIONS) DEBUG=1

GCC_SYMBOLS_PRIVATE_EXTERN = NO

OTHER_SWIFT_FLAGS = $(inherited) $(PODS_OTHER_SWIFT_FLAGS) -DDEBUG

SWIFT_OPTIMIZATION_LEVEL = -Onone

OTHER_LDFLAGS[arch=arm*] = $(inherited) -Xlinker -interposable

In addition to these configuration files, additional, specific sets of settings may be useful in the project. For example, we have files Signing.debug.xcconfig And Signing.release.xcconfigwhich stores all parameters related to the application signature for release and debug configurations.

Putting things in order in the diagrams

IN Project.swift you will need to describe all the circuits of the main project.

When we opened the list of our schemes and analyzed each of them, it became clear that some of them were no longer relevant. This is a great time to remove unnecessary schematics. For example, we had several schemes that were used only to run a small set of tests that tested specific scenarios in individual features. These tests were also included in the test plans of other schemes, so there was no need for separate schemes.

Among the remaining schemes, we found that some of them involved running a subset of UI tests, the list of which was compiled manually.

If we moved to Tuist, we would have to describe each of these tests manually in the manifest. This is clearly not what we wanted to do.

The solution to this problem is to create a file xctestplanin which you can collect all the necessary tests. This way we can easily specify testAction V Project.swift for a specific circuit, for example as follows:

.scheme(

    name: "AuthUITests",

    shared: true,

    buildAction: .buildAction(targets: ["Cian", "CianUITests"]),

    testAction: .testPlans(["CIANUITests/Plans/AuthUITests.xctestplan"], configuration: "Debug")

)

This approach allows you to simplify the management of circuits and tests in a project.

Generating a new project

After preparing the current project at step zero of our roadmap, we move on to the next stage – the direct generation of the project.

In order to generate a new project described in the manifest Project.swiftjust one command is enough:

However, for this simple command to produce the desired result, it will take a lot of effort to write the manifest itself Project.swift.

We approached this process iteratively. First, our goal was to get a project generated from the manifest with the necessary set of configurations (Debug, Release, etc.) and global settings (supported iOS version, Swift version, etc.). Then we planned to add one target at a time to the manifest with the necessary settings and schemes.

To decide which target to start with, you can use another utility from Tuist, which shows a list of project targets sorted by the number of dependencies.

Launching the utility looks like this:

tuist migration list-targets -p MyProject.xcodeproj

As a result, you will get output something like this:

[

  {

    "targetName" : "MyExtension",

    "linkedFrameworksCount" : 1,

    "targetDependenciesNames" : []

  },

  {

    "targetName" : "MainTarget",

    "linkedFrameworksCount" : 2,

    "targetDependenciesNames" : ["MyExtension"]

  },

  {

    "targetName" : "MainTargetUITests",

    "linkedFrameworksCount" : 3,

    "targetDependenciesNames" : ["MainTarget"]

  },

  {

    "targetName" : "MainTargetUnitTests",

    "linkedFrameworksCount" : 4,

    "targetDependenciesNames" : ["MainTarget"]

  }

  ...

]

We take the target with the least number of dependencies, move it, then the next one, and so on until complete victory. However, the approach may greatly depend on the specifics of your project, so in some cases a different path may be more effective.

How can we make sure that we haven’t lost anything in the process of transferring from the old project to the new one?

Firstly, when generating, it is better not to overwrite an existing project, but to create a new one next to it. For example, so that we have MyProject.xcodeproj And NewMyProject.xcodeproj. This will allow you to easily compare the current and new versions of projects.

To make comparison easier, you can use a third-party tool xcdiffwhich allows you to compare two projects and report on differences in settings, targets, build steps, etc.

xcdiff supports several report formats: console output, HTML, JSON and Markdown.

Example of a report in html format:

To get a similar report, run the following command:

xcdiff -p1 ../MyProject.xcodeproj -p2 MyNewProject.xcodeproj/ -t Cian -f htmlSideBySide -d > xcdiff.html

Options -p1 And -p2 indicate the path to the compared projects. Parameter -t specifies the target for which the report will be generated. The -f parameter is responsible for the report format, and the flag -d indicates that we are only interested in the differences between the two projects. As a result, a file will be created xcdiff.html with a report that will help you find the differences between the old and new versions of the project.

Next, you have to repeat the cycle many times: making changes to Project.swift,project generation and diff checking. This process requires patience, but in the end your efforts will be rewarded with a new, fully functioning project. At least in our case we achieved the desired result.

And here it’s worth telling about a little trick that will help make Tuist and Cocoapods friends. The attentive reader may have already noticed the following line in the file Cian.debug.xcconfigwhich we talked about earlier when setting up the project:

#include "../../Pods/Target Support Files/Pods-Cian/Pods-Cian.debug.xcconfig"

If you do nothing special and specify such a configuration file in the manifest Project.swift for the target, then with a clean installation and calling the command tuist generate You'll get this error:

Configuration file not found at path /Users/john.appleseed/cian/Pods/Target Support Files/Pods-Cian/Pods-Cian.debug.xcconfig

Fatal linting issues found

Consider creating an issue using the following link: https://github.com/tuist/tuist/issues/new/choose

The problem is that Tuist validates the files specified in the manifest when generating the project xcconfigincluding those that connect inside them.

Accordingly, Tuist checks for availability xcconfig files from Cocoapods, which at this stage have not yet been created, since to generate them you need to run the command pod install. However, in order to execute pod installwe need a project file, which is exactly what we are trying to generate. It turns out to be a vicious circle.

The solution is to trick Tuist by generating stubs on the required paths before executing the command tuist generate. These placeholders will then be overwritten by the real files from Cocoapods.

To pull off this trick, you can use a script that checks for the presence of files in the appropriate paths for each target and configuration:

/Users/john.appleseed/cian/Pods/Target Support Files/Pods-<TARGET>/Pods-<TARGET>.<CONFIGURATION>.xcconfig

If the file is missing, the script creates an empty file, which will then be overwritten by the result of the command pod install.

Your project may already have a script that developers run when setting up a project or switching to a new branch. This script can be supplemented with checking for the presence of the necessary xcconfig files and their generation if necessary. After this, you can safely start generating the project via Tuist and installing dependencies via Cocoapods.

Let's move on. As soon as you get the result you are happy with on your local machine, be sure to check the generation and build on your CI/CD. Take the time to create a separate branch for CI and configure at least one build machine to run and build your project with an auto-generated file. After all, in the end, our goal is not just to generate a project, but to make sure that the application with this file reaches users safely.

It's just beginning

So, we have come to the point where we have completed the first two steps of our roadmap: we prepared the current project for moving and learned how to generate a new project using Tuist.

Now you can move on to the next stage – transferring project modules to Tuist. This will be a much larger and more complex adventure than the steps we have already taken. In our case, we are only at the very beginning. We will definitely talk about how we will transfer modules to Tuist in Cyan and what will come of it in the following episodes. And that's all for now.

I hope you learned something useful. Well, or at least they smiled when they learned that in 2024 someone else is resolving conflicts in xcodeproj manually, and were glad that the number of such projects has become one less.

If you already use Tuist in your projects, share your experience in the comments!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *