Modularity lifehacks on iOS

Yura Privezentsev
hh.ru
Published in
8 min readJun 21, 2022

Hello, everyone! My name is Yura and I’m an iOS-developer in core-team at hh.ru. In this article I’m going to talk about how we work with multiple modules in our iOS-apps. We’ll discuss the environment and structure of our project, touch the topic of build time and dive into code generation.

Our team and projects

Our core-team is responsible for making it convenient to work on the app for other iOS teams. We create automations, improve codebase and refactoring and so on.

Currently we’re working on two applications: for applicants and employers. 10 iOS developers are working on both projects, and we’re distributed between 5 teams in the mobile department. In order to make simultaneous work comfortable, we split our app into multiple modules or features.

In our terminology feature is a separate Xcode project with its own isolated code and everything needed like resources or texts and which is connected to other features and the main app. Thanks to such a feature division we provide possibility for independent work in each team.

In 2018 our application was a huge monolith piece of code, poorly divided into four barely independent, but still interrelated parts. Lately we’ve been cracking and dragging these parts, so now the monolith looks much smaller. As for new features, we make them all isolated. Nowadays there exists 75 independent features in our app, it has become some kind of a container ship which carries a lot of container features.

But with the division came the problems. In the article I’m going to thoroughly describe the following:

Firstly, such a huge number of features is difficult to support in the app: you need to follow the coherent structure and networks between them.

Secondly, our project is really big. As it consists of a great amount of lines of code, the compilation takes a lot of time.

Thirdly, a big project is hard to maintain due to the tremendous number of agreements and relations between features. In a nutshell, the size of the project complicates the development.

Problem 1: Project structure support

As it naturally comes, a huge number of xcode projects are difficult to manage. You must have faced the conflicts of xcodeproj files while merging. Most of the time they are solved pretty easily. Nevertheless, if they appear quite often, it becomes annoying to waste time on them.

As the app consists of many independent features, their support can be difficult. While managing projects in Xcode interface you can change some projects, but easily miss or forget to add something to others. That’s why we started looking for the source of the problem right away.

At first we used CocoaPods — popular dependency manager. We realized that we could build our multi-module architecture via applying development pods to our features. Inner projects for CocoaPods are written as a special podspec file. It’s smaller than the Xcode project description and contains only the main information.

However, with growing project we started to encounter more and more problems and restrictions from CocoaPods.

For example, CocoaPods forbids its simultaneous work with Swift Package Manager and Carthage for external dependencies. At some point it became a serious problem for us. Plus, CocoaPods is quite tricky about cashing dependencies.

We started contemplating leaving Cocoapods, but we need to decide where to go and what other tools we could use for external dependencies. As a result of our thorough search, we came across Tuist. It’s a utility which generates and supports Xcode-project.

Project description with Tuist looks something similar to package description for SPM but supports nearly all settings of xcodeproj. Thanks to Tuist, our project description has become even easier for comprehension.

We hid all the meta information which could be found in other sources, and left only the most important things. It comes down to mentioning the name of the feature, which resources should be generated for it with SwiftGen and what targets it has. The lists of dependencies are formed automatically for each target. It is done with the help of the script that looks for all import in the target’s files. The writing of the project got easier, and the process became more comfortable.

While we were moving from CocoaPods to Tuist, we noticed that the location of our project’s files was quite peculiar due to some historical reasons and God-knows-what else. To avoid repeating the action twice, we cleared the structure of the project and arranged everything, so now it lies in the right folders. That was what the developers thanked us for.

Problem 2: Slow compilation

At some point the developers noticed that project build took around five minutes. That became a huge problem, as they got distracted during that time and fell out of the loop, switching to other tasks. That’s why a lot of time was wasted on diving into the work again.

We started thinking how we could fix that and decided to begin with gathering honest statistics on how much time is spent on our projects’ compilation. That’s where XCLogParser came to our rescue. Xcode presents reports after build in xcactivitylog format. These reports contain all the information about the build, including each file. At the same time, XCLogParser can parse all these reports into a format which is easier to understand for an ordinary human. For example, reflect it visually or in json format.

Here’s an example of json, which is presented by XCLogParser after report analysis. The line that interests us is the time spent on the project compilation.

Nevertheless, this json is actually enormous and it contains the most detailed information possible about each step of project build. This is what really excites us! We take all this information and send it to our database. There we use it to create beautiful graphics about the time spent on project build in Grafana. That’s what graphics for apps look like:

They’re going up and down, but it’s ok, because currently we have two different types of machines on CI: processors Intel and M1. The machines on M1 build a project faster, of course. We don’t just look at these graphics; they help us find problems in the configuration of our projects. You can find more details on changing the speed of build in this article.

Environment setup

An obvious point. If a buggy commit got into the repository, this bug should be reproduced similarly: both in the developers’ mac and on CI. Therefore, if the bug is fixed, the fix should work on all devices too, the developers’ and CI.

This leads us to the idea that the project’s environment should be reproducible and unified, so that there are no true stories, when somebody complains in the chat about having a bug and the app build is screwed, whereas the guy on the other end replies: “WORKSFORME”

In order to avoid such situations, we have a little lifehack. For example, apart from the swift code and the app itself, our project consists of various tools like Ruby, Fastlane, Danger, app dependencies which we install via Carthage, and swift utilities installed via Mint.

If you install, set up and launch these things by yourself, you can die a little. It means that the developer has to keep in mind a bunch of tools and remember the sequences of their installation, so that nothing crashes. And what if you have to update something? A whole different story.

We’d come to the conclusion that we needed a unified script for setting up the whole environment. And we wrote it:

It installs all the necessary dependencies step by step, configures the project’s environment and does everything possible so that the developers don’t waste their time on thinking about it. Now it’s possible to download the project’s repository, launch our Bootstrap-script, make some tea, and come back later to find that everything is open and ready for productive work. The true wonder of this tool is that all dependencies and tools of the project are written in the repository. Simply put, the repository becomes the one and only source of truth. And this is very convenient — you can install and set up all this environment by using just one command.

Code generation

The next story is about code generation. One of the tools that we use — Xcode templates. I assume you’re already familiar with them. There are lots of useful articles on Xcode templates on the Internet, and they do their thing pretty well. The only drawback is a sophisticated configuration process. If you add different conditions when generating the template, it becomes twice as more complicated. And it has a disgusting syntax. Well, you know.

Nevertheless, Xcode templates are pretty good for some small groups and separate files. They’re convenient and available right from the standard Xcode menu. This allows it to stay in one app, not to fall out from the context and create small files quickly. Our project has some templates for such purposes, like for MVVM modules. Also there is a set of small templates for generating models, analytics and view files, design systems and so on.

The next tool is again Tuist. It offers a command for file generation — scaffold. It’s rather convenient for creating big file groups and folder hierarchy. We use it for generating new features. A new blank feature actually contains a lot of code. It arranges the necessary folder hierarchy right away, so that the developer can generate everything necessary and move on to completing their task.

Here you can both create separate files via stencil templates, which should be in the content of this file. All in all, we have everything needed for creating a new feature.

Apart from that, we use SwiftGen in our project for generating code and access to different resources, for example to images and localization strings. For all the rest we use Sourcery which enables us to generate mocks for Unit-tests and other details on the project. So it saves us for real.

Conclusions

Tuist makes working with the project a lot easier: both in generation, and in new features’ creation. We constantly monitor the speed of the build, which allows us to spot the problems as soon as they appear. We try not to build the whole project at once: we assemble only the most necessary parts, thanks to the idea of splitting the project into features. Last but not least, we automate the development process and feature configuration, so that developers don’t waste time on such routines.

That’s all for now! Share with us your experience in working with big projects in the comments and feel free to ask any questions!

Sounds fun? Subscribe to our news channel on telegram and “HHella cool stories” channel so you won’t miss new videos, articles, and other news. And you can ask our engineers any questions on any topic in the comments or in hh developers’ telegram-chat.

--

--