Modular Architecture in iOS

Leandro Pérez
5 min readAug 20, 2019

--

Great animation comes from Owen Chikazawa

Intro

In large scale projects, and arguably in any project, the capability for creating, running and testing parts in isolation is a necessity. A change in one part should not require to recompile the entire app until it is really necessary.

In this article, I’m going to explain how we can create a modular architecture using Cocoapods and Xcode. Something that looks like this:

Motivation

Imagine we need to make a small change in a screen and try it out to see if that change works. If the app we are working on is built monolithically as a single product, we will need to recompile the entire codebase to see the results. This process is time-consuming and tedious. We should be able to compile and run parts independently, in isolation.

Sometimes a part of an application is complex and large enough to be treated as a product on its own. We can call that a module. Modularity is key towards achieving software that can be scaled and maintained over time.

Even though it might add complexity at the beginning of a project, in the long run, we may find many benefits in a modular solution: less build-time when recompiling after a change is made, clearer development areas/responsibilities, isolation of changes, being able to use playgrounds to build and test UI, etc.

External Dependencies

Nowadays, most iOS applications use either Cocoapods or Carthage as an external dependency manager. This article focuses on Cocoapods, but the approach could work with Carthage too.

As far as I know, the most important difference is that Carthage might yield lower compile times in clean builds. Carthage compiles the frameworks outside the build phases of the app, only when we run an update or an install.

I haven’t tried Xcode 11, iOS13 and the Swift Package Manager yet. It seems that structuring a modular architecture with such tools is quite easy. That might be the case, but it’s going to be a while before all apps can target iOS 13 to have those capabilities. So, Cocoapods and Carthage will still be here for a while.

So, how can we achieve modularity using Cocoapods as dependency manager?

Architecture

The approach consists in having a single workspace that contains multiple projects. One project for the final product (the app) and one project for each module. The app depends on the modules and the modules can depend on each other. Finally, Cocoapods provides external dependencies.

Architecture

Each module compiles as a framework that is used by the application target. Modules can depend on each other (no circular dependencies though) and on 3rd party SDKs, handled by Cocoapods.

We define the external dependencies in a single Podfile. When we install the pods, the workspace gets populated with a Pods project. The Pods will output frameworks that each module, and the app, will depend upon.

In the following section, I will explain how to create such a structure.

Setting up the workspace.

  1. Create a workspace.
  2. Create a project for the app.
  3. Initialize Cocoapods and name the app dependencies.
  4. Place the Podfile at the Root level of the workspace, where the workspace file is.
  5. (Optional) I like having the Podfile and the Podfile.lock inside the workspace.
  6. (Optional) I like having a Playground for each module to try things out.
  7. Make sure the app compiles and uses the external dependencies.

The workspace should look something like this:

Initial workspace and folder structure.

Creating a module.

  1. Create a project and place it inside the workspace.
  2. Add the module’s dependencies to the Podfile.
  3. Add the dependencies of step 2 into the pods of the app too.
  4. Run pod install and compile the module to make sure it can use the external dependencies.
  5. Add the module as a dependency of the project (follow the steps in Adding an Module as a Dependency of the App).

At this time, your Workspace, Podfile, and Folder structure should look like this:

Workspace after adding the first module, called ‘Core’.
How to create a new module.

Adding a Module as a Dependency of the App.

  1. Create a group without folder and name it “Dependencies” or whatever you like.
  2. Drag the project you want to depend upon into the group created in the previous step.
  3. (Only the first time) Create a Copy Files Build Phase to Frameworks.
  4. Add the framework product to the copy frameworks build phase created in step 3.
Project, Podfile and Folder structure after adding the second module.

Adding inter-module dependencies.

Do this when a module depends on another internal module. For example, Feature X depends on Core.

  1. In the target module (Feature X), create a group without folder and name it “Dependencies” or whatever you like.
  2. Drag the project you want to depend upon (Core) to the group created in the previous step.
  3. Drag the framework product to the embedded binaries
  4. Add the framework product to the copy frameworks build phase created in step 3.

Conclusion

I would argue that not only large scale projects can benefit from this kind of architecture. Having the capability of compiling independent parts of the app and running them in separate playgrounds is very nice indeed. It is not very hard to achieve and the benefits can be enjoyed over time.

It is not very complicated to configure the Xcode workspace to work with modules once you get the hang of it. This approach can be used in new and in existing projects too.

The impact of this kind of modularity exceeds mere practicality, altering the way we design and code internally. It becomes evident when an object depends on things that do not belong to a particular layer or module. Design mistakes become more evident and the overall quality improves.

Check out the sample Github project:

Part 2!

I wrote a second article about some details that were left behind this one. Check it out!

--

--

Leandro Pérez

Software Engineer, coding since 2001. I have done plenty of work in iOS with Objective-C and Swift.