Second Thoughts on iOS Project Setup

👍
· Jan 23, 2024 · 10 mins read
Second Thoughts on iOS Project Setup

In the last few years, I have been working on a few medium to big iOS projects. All of these projects have great modular architecture — functionalities and features are broken down and implemented in their own libraries or frameworks. One project had more than forty libraries and another had more than sixty. But here came the question: what’s the best practice to set up a project with so many dependencies, especially when they are all under heavy development? I had the fortune to work with my team to improve these project setups. The following is what I learned from the process.

When it comes to dependency management for iOS projects, you might immediately think of Cocoapods, Carthage and SPM. They are all great tools and have a rich set of features to choose from like versioning, branch/commit referencing, etc. We can also use these tools either to fetch source code or build libraries. So, which method should we use in projects? Where exactly should we put the source code of these libraries: Multi-Repo or Mono-repo solution?

There might be no absolute right or wrong answers to all these questions. Different methods are suitable for different cases, which is why knowing the pros and cons of the different approaches is essential to making pragmatic decisions in practice.

The goal of the project setup

It might be worth discussing the goal of the project setup first before diving into the process of setting up the projects because different goals will lead to different outcomes. This is especially true for big companies where the setup might be done by different teams. For example, if DevOps teams are in charge of this task, they might be focusing on configuring CI/CD pipelines efficiently instead of finding an easier setup for daily development with not enough input from developers.

From my point of view, any project setup should prioritise developers’ happiness as it’s they who are going to deal with the project setup on a daily basis. It’s always worth the effort to make it as easy as possible for developers even if it means more effort on CI/CD configurations as this setup is usually a one-time effort.

How many is too many

This article is not about how to break projects into components and libraries. It’s about how to hook components and libraries together to form the final projects. The components and dependencies are referenced to others and the dependency map can be a very complicated, leveled structure. Keeping this map as simple as possible sometimes is very important for the project’s evolution and maintenance. In most cases, if we find at one level, we have more than ten dependencies, it could be an indicator that we should merge some of them to simplify the dependency map.

Hybrid setup vs. pure setup

In essence, project setup is all about how to hook source code and artifactories from different places to build the final products. Dependency management tools like Cocoapods, Carthage and SPM are designed for this purpose. Git submodule or custom scripts can help too. How should we choose from all these options?

For example, if we have a third party library dependency that is available from Cocoapods, Carthage and SPM , is there a best option to choose? The answers is it depends. For practical reasons, it depends on the team members’ preference and the current project setup. If one of these tools are already in use, it might be better to keep using the existing tool and keep the setup pure. For new projects, however, it might make sense to use SPM as it’s supported natively, which removes the need of installing extra softwares.

What if it is a dependency that is only available in build framework format? In this case, intuitively, we can just add it into current repo and refer to it directly. It should work well if the repo size is not a concern and it’s not upgraded often. Otherwise, we can put it into some artifactory management systems and fetch it from there.

In some cases, git submodule can be a great help too if we want to incorporate source code from other repositories and they are happened not supporting any dependency tools yet.

In summary, different tools are designed for different purposes and we should use them accordingly. With this being said, from my point of view, we should always consider reducing the tools that we use because it would make developers’ daily life more complicated if more tools are involved even if we think we have setup them to work automatically.

Multi-repo vs. Mono-repo

Multi-repo comes naturally with the idea of modular architecture, where each library is stored and managed in a separated repository. This is at least what we used to do with third party library dependencies that we have proper versioning management and the library is implemented and verified separately as well.

For an internal library dependency, we can also adopt Multi-repo solution for sure. However, it does mean we need to pay some extra cost — submit code into different repository and create and review pull requests in different places etc.

In Mono-repo solution instead, we store the code of libraries in the same repository of the main project. With this setup, we can refer libraries directly in projects or workspaces, which removes the needs of running dependency tools. This solution could be the best possible simple solution that suit for any small to medium size projects, especially the libraries in the context are only used in the current project.

Version references vs. branch commit references

The choice between versioning and referring to a branch commit when managing dependencies in iOS projects depends on various factors, including project size, team workflow, and the stability of dependencies.

Versioning ensures of using a stable and tested release of a dependency. Also it simplifies dependency management by providing clear, named references to specific releases. This makes it easier to understand and manage dependencies over time. Lastly, versioning, especially semantic versioning, allows specifying compatible ranges of versions to ensure that is works with a range of compatible releases. This should be the first choice whenever it is feasible.

Referencing to branch Commit, however, gives us some flexibilities. For example, in order to use the latest changes that is not released yet. This usually happens for projects that are in early development stages. It’s essential to strike a balance between stability and flexibility based on the project’s needs and risk tolerance.

Test strategies for libraries

Ideally, a full functional test should do done at library level and an integration test should be performed at the project level that consumes the library.

Some teams relax the test of the internal libraries or do not even have tests at all for internal libraries. From test pyramid point of view, this would not be a best practice. Because, the lower level the tests sit, the lower cost would it be. It’s better to ensure the library’s functionality is reliable and stable on its own and make it easier to detect bugs and issues within the library.

Just like building LEGO blocks, the tests on the library level make sure the quality of the library blocks, while the tests on the higher level make sure the quality of the design and integration.

Git submodule/subtree vs. dependency management tools

Submodule and subtree from git can bring source code from other repositories into projects, which are a light ways to integrate resources into main projects.

However, if the dependency map is very complicated where the dependent libraries are also dependent on each other with different versions, to resolve the dependency map to use the right versions of the libraries could not be trivial. This is where dependency management tools shine. Tools like Cocoapods, Carthage and SPM can help resolve library versions automatically.

Dependency management tools are usually a better choice when it comes to iOS project setup. However, for libraries that are in binary format, if we don’t want to include them in main project repository, using git submodule/subtree to fetch those into main project could be an easy choice.

Source code vs. built libraries referencing

For some libraries that we don’t have the source code, we have to refer to these build libraries. When it comes to internal libraries and some open source libraries, should we use source code referencing or refer to the build libraries?

Building time is the first obvious one because we have to compile all source code altogether if we refer to the source code in project. However, don’t overestimate this as well. I had an experience to update one project to refer source code instead of built libraries. To everyone’s surprise, we were able to build the project from source code in about five minutes. Considering Xcode offers cache for the unchanged code, the build time in daily development process will be less than this, which is surely acceptable. With all source code in the main project, it is much easier to explore and debug the project.

On the other hand, referring to the build libraries is simpler because of the encapsulation of the built libraries. With the proper setup(with proper LLDB source mapping with dSYM etc. for example), we can still debug the project with ease. However, the configuration of CI/CD to build these libraries could be complicated because we have to build them in sequence based on the dependency map and this process can take long time.

Project format versions

In essence, Xcode project file is in plist format and it’s almost stable from versions to versions. But not always the case though. If upgrade the project format from Xcode 3.x to Xcode 15.x, we might find some changes like some duplicated definitions were removed automatically. As the best practice, it’s good go keep project format version updated if don’t need to open it in older versions of the Xcode.

Library type considerations

One more thing worth considering is App loading time. If we setup to statically link the libraries, it will take less time to load the app comparing using dynamic libraries, which you can check the details from this WWDC video. Under this, Apple provides technology like static library, dynamic library and mergeable library.

More on app loading time topic — it is better also check out this WWDC session — Link fast: Improve build and launch times, which explains some linker settings that we can employ to our purposes.

Conclusion

In this article, we have covered a few factors that need to consider when we setup iOS projects. Please let me know your thoughts. Happy coding!

Comments

Sharing is caring!