Lessons Learned Modularising a Large Ios App
I’ve always been interested in how codebases change over time: as teams grow, more code gets added, but it also makes the system more complex and more difficult for a new starter to understand. As engineers we strive to build our systems in a way that makes it easy to extend and add new features (without breaking existing functionality). In recent years, one of the ways to help teams move fast is to move people into “vertical”/feature teams and have them focus on specific domains of the business. Each team would have engineers from across the disciplines: front-end, back-end, mobile etc. The team would also have members from other parts of the business: product, design, QA. Essentially everything that is needed for them to be autonomous, do their job, and minimise dependencies on other teams.
Conway’s Law says organisations that design systems (that’s us) will design those systems in a way which copies the communication structure of the organisation. Essentially, if we have feature teams, we will probably architect our systems in a similar fashion… micro-services, anyone? After all, that is what’s happening. Of course, if those feature teams are not truly autonomous, what’s stopping them from creating systems which are also not autonomous?
Back-end systems are, of course, quite different to iOS applications. They’re distributed, and can end up being sprawling monsters across multiple data-centres around the world. Our iOS applications sit on a device that can fit in a user’s pocket. This absolutely does not mean similar practices shouldn’t be followed. As mentioned, we want to build our systems in ways that allow us to extend and add new features - while also minimising the number of bugs.
What we’re essentially doing here is creating multiple frameworks - be it static or dynamic - that will be used by our app.
At WorldRemit we went through a couple of stages until we got to a point where we were happy with the solution. For background, the application is currently sitting on around 200,000 lines of Swift code, including tests. Not only is it cumbersome for someone to come along and get up-to-speed, it makes contributing to it more difficult than it needs to be. There are a mix of Cocoapod and Carthage dependencies: the preference for us has always been Carthage; however, some “must-have” dependencies simply only exist as Cocoapods. Bit of a pain, but we can work with that.
For the first spike, we thought we’d try out continuing with the Carthage theme, since that was the preference. We would create separate repositories for our frameworks, work on them, and then include them into the main app via the Cartfile. After the initial transfer of code, it was very quick that this method wasn’t going to work. Here’s how the workflow would go:
- We’d start work on a feature
- Feature would require work done to the main app as well as the framework
- Create a pull request for the framework, get the code approved and checked in - all the while having the version in the Cartfile set to a random commit SHA.
- Create a release for the framework and change the Cartfile version
- Create a pull request for the main app changes
This clearly made it very difficult for anyone to work on a feature that required changes in multiple frameworks (including the main app). At the time, we thought we’d continue for a while. I found a post by Eric Horacek explaining a simple approach to modularising an iOS application. The reasons were more around compile times, but the solution would be the same. In the post, Eric mentions a script that would take the code from the Carthage checkouts folder and integrate it within your Xcode workspace. Since the script wasn’t included, I wrote one myself. The benefit would being able to work on both the framework and the app at the same time when working on a feature. Again, it worked okay but it still had one fundamental flaw: pull requests in the framework still needed to be made, and made without context of any other codebase. We felt this approach was a bit awkward; code reviews are important way of maintaining code quality and sharing knowledge between engineers. If another engineer can’t review with all the context available, they’re at a huge disadvantage.
The second spike was to go for a mono-repo approach. This completely rules out Carthage and Swift Package Manager since they only support one project per repository.
Using Xcodegen we could create a sub-projects (or ‘modules’) that we could then integrate into our main app’s workspace.
The approach is straightforward to create a new module:
- Add a sub-folder in the main repo for the new module
- Add a new
project.jsonfile for Xcodegen
- Add Xcodegen command to the aggregate generate script (to simplify the step for other engineers); this step can be automated further by searching for
- Add xcodeproj to the workspace
- Start working on the new module!
With this approach engineers can focus on their domains. We set up core modules for things like logging, analytics, shared UI components etc. This allows us to easily share common code between modules and the main app target, but also allows us to create internal apps; for example, with the shared UI components module we created an app to display theme(s) and controls. This includes buttons in all their various states, fonts, colours, custom card views and more. You may think it’s a pain to set up yet another app, and you’re right, there is an initial hurdle to jump; however, the benefits of being able to load a small app to view changes you’ve made to shared views compared to running the main app, signing in, and getting the app in the various states is massive. Playgrounds are used similarly for other scenarios.
Incremental build times go down; Xcode indexing times go down when working on individual projects; tests can be moved to their individual modules.
One of the downsides is Continuous Integration build times do not go down, since our agents build from scratch.
When it comes down to it, software engineers are building features for customers. Modularising a monolith codebase obviously doesn’t involve an immediate improvement to the customer experience; however, over time, the time will be gained back when engineers are able to build features faster. For further reading, I recommend this post by Martin Fowler.