How to make your life easier by modularizing your iOS project

The Dark Side of Modularization

Working on our iOS application has been an incredibly eye-opening experience. I was anticipating being overwhelmed by jumping into a colossal monolithic application with a complicated set of interdependencies. Luckily for me, our mobile team was on the same track as our web teams and had taken steps to avoid a monolithic architecture for both their iOS and Android applications. The decision was made to fragment the application into highly cohesive modules that abstract away implementation details from the main application. On iOS, externalizing code is best done with frameworks; which are libraries that can also contain resources like xib files, storyboards and images. At the beginning of my work term, our app was consuming over a dozen internal frameworks and several external frameworks. As a result of managing so many dependencies, the time it took to build our app had skyrocketed and became a serious block in our daily workflow. Just imagine having to wait 25 minutes every time you wanted to get the latest changes from the repository!

A New Hope

Over the last few months we have made several key revisions to our workflow that has allowed our framework approach to be far more effective, and ultimately, made our lives easier.

Our transformation into frameworks began with extracting the small, reusable, components and has now extended to features as large as the entire searching tab of the application. What makes this framework approach so valuable stems from the roots of modularization as a software technique: maintainability, reusability, and abstraction. With each major feature of our application in it’s own framework, different iOS teams at Hootsuite are able to maintain different frameworks independently, each with their own separate commit history. Many of our internal frameworks depend upon each other, they were designed to be as reusable as possible. Each framework has a well defined public interface, the implementation details of a framework are inherently abstract. As a co-op starting out in a new position, this level of abstraction and modularity has made my life easier and far more productive!

We have categorized our frameworks as either a Component, a Feature, or a Core utility. This graph doesn’t include all of our frameworks, but it illustrates the hierarchy:

iOS Features Components Core

When is it suitable to make a framework?

For us, it is whenever we have either a very reusable component, or a feature containing highly cohesive code. One of the largest features that I have been working on for past few months is the new Twitter and Instagram search. We encapsulated the entire searching functionality into its own framework. The project can run on its own and has it’s own demo application. This level of encapsulation has vastly improved my productivity over the last couple of months; working in a confined environment reduces the surrounding complexity and has allowed me to take on more responsibility. Having a large base of frameworks has many benefits, but as mentioned before, as we increased the number of dependencies for our app, our build time became a serious issue!

How do we manage our frameworks?

On iOS, executable code in a framework bundle is automatically a dynamically linked shared library. Because dynamic libraries are being referenced at runtime, the developer of the framework must maintain compatibility with client apps as the framework is updated, otherwise, any breaking changes the developer makes to a framework would automatically be included by any client apps. Most frameworks combat this issue by specifying a version with each stable release. We host each of our frameworks on their own Github repository and use Git’s tagging system for versioning.

There are two main dependency managers to choose from for iOS development: Cocoapods and Carthage. Cocoapods logoCocoapods has a centralized collection of ‘Pods’ that can be found on their website. With Cocoapods you create a ‘Podfile’ and specify your dependencies followed with a version. After running ‘pod install’, Cocoapods will create a ‘Workspace’ and manage all your project settings for you under the hood. This approach abstracts away much of the complexity but reduces the amount of flexibility and control you have over your project. Because Cocoapods is a very comprehensive tool, when things go wrong, it’s often hard to understand the problem.

Carthage takes a very different approach: it provides a much simpler tool that is easier to maintain, but requires more initial setup. CarthageThere is no centralized collection of frameworks with Carthage, rather, you specify the URL of your dependencies along with the version inside a plain text file called a ‘Cartfile’. When running Carthage, source code for each dependency is placed into a ‘Checkouts’ folder and the built framework is placed into a ‘Build’ folder. Upon building, Carthage will create a ‘Cartfile.resolved’ text file which defines exactly which versions of your dependencies Carthage has built. For these reasons, Carthage is less of a ‘Black Box’ tool then Cocoapods, and doesn’t require you to maintain a ‘Podspec’ file. For us, control and flexibility had precedence over simplicity; after using both, we opted for Carthage as our dependency manager.

How did we solve our build issue?

While managing so many frameworks between multiple developers our daily workflow was constantly being blocked by our build time with Carthage. When pulling in changes for the main project, if the changes referenced any new code in one of the frameworks, the project would fail to build. This could be solved by using Carthage to build the necessary frameworks to their latest version, however, it was often difficult to determine which frameworks needed to be built. With Carthage it’s possible to build every dependency with one command, but this process would take up to 25 minutes! With changes constantly being made to each of our frameworks on a daily basis, Carthage was seriously hindering our productivity. Over several months we made several key revisions to our dependency management system to alleviate our issues with Carthage.

To decipher the interconnectivity between our main application and our internal frameworks, we built a tool that empowered us to visualize the dependency graph and diagnose issues. This was particularly useful for alleviating version conflicts which could cause Carthage to fail. Red circles indicate that a framework is being referenced at more than one version:

iOS Visualization Dependency Graph

For our first attempt to reduce our build time we added a pre-build run script that would determine which individual frameworks needed to be built when running the project. When Carthage builds your project it will create a cartfile.resolved plain text file that indicates the exact versions that were built. Our pre-build run script was added to our project in Xcode that would compare the local and remote versions of the cartfile.resolved file and build only the frameworks which had been updated. This script did save us quite a bit of time on a daily basis but Carthage was still too slow!

Our second revision we aimed to decrease the build time for our external dependencies. Carthage can automatically use prebuilt frameworks if they are attached to a GitHub Release in your project’s repository. Unfortunately, we didn’t get any noticeable improvement after including the pre-built binaries.

For our final revision that truly made our framework approach painless, we implemented a combination of Git Submodules alongside Carthage. Git Submodules enabled us to combine all of our frameworks inside our main application as a subdirectory. Each framework retained its individual project structure with a separate commit history! With submodules, each framework is referenced from the main project with a commit hash associated to a branch. Once changes are made to a framework, instead of having Carthage build at the latest version, only the submodule hash needs to be updated. Our steps for pulling in changes to a framework are now instantaneous!

Conclusion

There are plenty of articles out there that discuss why setting up and using Git submodules can sometimes be a headache. For instance, every time a submodule commit is updated, you demand a manual git submodule update by every collaborator. Forgetting this explicit update can result in silent regressions. At Hootsuite we use Fastlane to automate the building and releasing of our app. To alleviate many of these headaches, we introduced custom Fastlane ‘Actions’ that allow us to to automate the updating of submodule references. After using Git Submodules for quite some time I can’t imagine going back. One of the best side-effects with submodules is the ability to access any file from any framework all from the same project. The benefits, small and large have made our development lives far easier and more productive!

With frameworks, we wanted to modularize our codebase without disrupting our daily workflow, which relies heavily on git feature branches and GitHub pull requests for code reviews. Having a modular codebase has made our work infinitely more reusable for new projects and features. For a recent company wide Hackathon I worked with a team to implement a SiriKit extension into our main application. By having our networking and UI frameworks available, we were able to import those dependencies and get a working sample in minimal time. By having our work in highly cohesive frameworks, each with an API that is considerably more obvious, our codebase is easier to maintain. Overall, with frameworks, the connectivity between our models, views and controllers seems less convoluted and easier to comprehend.

I highly recommend the transformation of your iOS projects into separate frameworks, but it doesn’t come easy, in fact we’ve started taking steps to eliminate unnecessary third party dependencies from our app. We’ve even gone as far as eliminating the need for Alamofire altogether by making our own networking layer using NSURLSession at it’s core. It’s clear for us that Git submodules are the best solution at this time; with constantly changing dependencies and separate commit histories. That being said, we are certainly keeping up to date with the Swift Package Manager and hope it makes it’s way to iOS.

About the Author

Tim DavisTim is a Co-op Software Developer on the mobile team (iOS). He is a big fan of the Apple development environment and software ecosystem. On his free time, he makes the most of the four seasons here in British Columbia by playing golf, hockey and snowboarding up in Whistler.