iOS app modularization: the conceptual understanding

Kevin Abram
8 min readApr 9, 2024
Photo by Alina Grubnyak on Unsplash

Modularization is the process of separating the functionality of a program into separate independent interchangeable modules. By default, the iOS app you make is monolithic, in which it does not need to import the other modules or functions that you have created.

If you need an explanation of how to create your modularization in practice through Swift Package Manager, you can refer to the article I made here: https://medium.com/@kevinabram1000/a-simple-modular-architecture-with-dependency-injection-in-ios-372d56a9bed9. I also provided the example code here: https://github.com/kevinabram111/Modular-Dependency.

This article is also a continuation and a revision of an article that I made before here: https://medium.com/@kevinabram1000/a-simple-modular-architecture-with-dependency-injection-in-ios-372d56a9bed9, in which there are cases where such an architecture will create a crash.

This article will cover the best practices and the conceptual understanding of how to implement modularization in an iOS app, which we should take a strong account into possible architectures that work and do not.

I will separate this article into several sub-sections, which include:

  1. The basics of implementing a modular architecture in iOS
  2. Circular dependencies in modular architecture
  3. Managing circular dependencies through abstractions
  4. Implementing the abstractions through dependency injection
  5. Further ideas for modular architecture implementation

As a disclaimer, before I start with the explanation, this is purely based on my experience as a software engineer in multiple companies. If you have other comments or inputs, feel free to provide your input or comments to the article. Thank you.

1. The basics of implementing a modular architecture in iOS

Photo by Kaleidico on Unsplash

Implementing a basic modular architecture in iOS can be of several ways. One way is by using a Swift Package Manager, which is discussed in the article here that I made with all the steps to do it and the final product: https://medium.com/@kevinabram1000/a-simple-modular-architecture-with-dependency-injection-in-ios-372d56a9bed9.

However, in this article section, I will tell you more about the basic implementation of modular architecture in iOS on the more conceptual side. Understanding the concept of modularization is essential before we start with a real project.

Image of Module B depends on Module A

To start, in the figure above, let’s say we have two modules, and let’s have a case in Module B will need to depend on Module A and import its contents. When visualized, it will be like in the figure above, where Module A will be imported by Module B. Module B’s codebase will have imports to Module A.

This is possible. This kind of implementation can be seen when you implement third-party libraries, in which the third-party libraries will be Module A, and your codebase (Module B) will need to import from the third-party library (Module A) to use the library’s functionality.

The image of the codebase that depends on RxSwift

Such an example can be from the figure above, wherein several third-party library implementations, like RxSwift, we will need to import RxSwift directly on the file on the codebase if we want to use it.

Image of Module C and B depends on Module A. Module D depends on Module B and C

To give a more complex example, you can also do this kind of modularization as well. In which it is linear, and the modules are imported from their parent. In this case, Module B and C will depend on Module A. Module D will depend on Module C and Module B.

In the figure above, Module C and Module B depend on Module A to work properly. While Module D depends on Module C and Module B to work properly.

The image of the codebase depends on the RxMoya and RxAlamofire libraries. While RxMoya and RxAlamofire depend on RxSwift

This is achievable, a simple example is that maybe a third-party library that you downloaded needs another third-party library like in the figure above. For example, if you want to implement a third-party library named RxAlamofire (that needs RxSwift) in your codebase, you will surely need RxSwift pre-installed as a parent.

Image of the modules that depend on each other’s libraries from Module A to Module B

However, what happens if you directly import the modules from each other like in the figure above, will it work? The answer is no. This is because it will create a circular dependency that results in a crash. What is circular dependency? I will explain it in the next section.

2. Circular dependencies in modular architecture

Photo by Etienne Girardet on Unsplash

By the official definition, circular dependency happens when two or more components depend on each other, therefore creating an infinite loop of dependency that can be difficult to break. This will result in the project or the app you’re making to crash.

Image of the circular dependency that is created between two modules

In the figure above, we can see that Module A and Module B will depend on each other, therefore creating an infinite loop. In theory, Module A will import Module B, then Module B will import Module A, and it will create an error.

The image of Modules C and B depends on Module A. Module D depends on Modules B and C. This does not create a circular dependency

On the other hand, in the figure above where Module A is imported to Module D, there is an end to the import chain, therefore not creating a circular dependency, and no looping to the chain. This kind of modular implementation is safe, but it is one way like in the figure above.

Image of the circular dependency that is created between three modules

In the figure above as another example, we can see another circular dependency as well, in which the chain of import will not end in the end, and will infinitely loop from one module to another, resulting in an error.

So, is it possible to implement a Modular architecture such that Modules A and B depend on each other? The answer is yes, but not directly. The answer to this is through abstractions, by creating an abstraction layer. What are abstractions? I will explain it in the next section.

3. Managing circular dependencies through abstractions

Photo by Sigmund on Unsplash

By definition, abstraction is the process of generalizing concrete details, such as attributes, away from the study of objects and systems to focus attention on details of greater importance.

Image of the abstraction layer implemented as a bridge between Module A and Module B

In this case, the abstraction that we will use here will serve as a bridge between several modules, like in the image above. This way, Module A will not import Module B, and Module B will not import Module A. Instead, both Module A and Module B will import the abstraction layer.

This will not create a circular dependency, and will not create a crash, since the chain of import ends at a certain point. In this case, the chain of import will end at Module A and Module B.

Examples of the abstraction layer implementation

One way to visualize the inside of the abstraction layer is that it consists of generic classes to help bridge several modules altogether. It can consist of Swift protocols that can be used as factories to create generic view controllers named UIViewController.

Examples of a wrong abstraction layer implementation, resulting in a circular dependency

Why do we need to make the classes and structs in the abstraction layer generic? The answer is that if we make it non-generic, the abstraction layer will need to import from Module A and Module B, which will cause yet another circular dependency resulting in a crash. We will need to make sure that the abstraction layer has only generic classes and structures.

So what is next? well, we will need to implement these abstractions through dependency injection. What is dependency injection, and what does it look like? I will explain it in the next section.

4. Implementing the abstractions through dependency injection

Photo by Mufid Majnun on Unsplash

By definition, dependency injection is a programming technique in which an object or function receives other objects or functions that it requires, as opposed to creating them internally.

Final visualization of the abstraction layer with dependency injection

As in the figure example above, we will provide the implementations on specific modules in Module A and Module B respectively, with the abstraction layer serving as the bridge between each module. The abstraction layer will then provide the functionality as it is provided.

This means if you add another module, let’s say Module C. You will add the bridging to create the view controller on the abstraction layer, and provide the implementation of the protocol inside Module C. This way, Module B and Module A will be able to route into Module C, and vice versa.

Dependency Injection can be done via third-party libraries. The full practical implementation of this can be found at https://github.com/kevinabram111/Modular-Dependency using Swinject as the third-party library used for dependency injection in an iOS app.

You can also get to know how the codebase on the GitHub link that is attached and works by referring to the article I made before here: https://medium.com/@kevinabram1000/a-simple-modular-architecture-with-dependency-injection-in-ios-372d56a9bed9

5. Further ideas for modular architecture implementation

Photo by Paymo on Unsplash

Modularization has several pros and cons, one of the pros is that it will be easier for parallel development, and testing. On the other hand, the cons will be managing an extra layer of abstraction, and ensuring that the abstraction layer will only consist of generic classes, which will potentially refactor the entire codebase.

Example implementation of utility module that is used in Module A and Module B

For an idea, I have several ideas that you can incorporate into your project, for example, you can create a module on your own that will hold the utility. This will consist of extensions and helpers that will help other modules.

Advanced implementation of modular architecture where we use git submodules

In another example, you can also incorporate git submodules into your project, in which you will place the utility module in a separate module altogether like in the image above.

To conclude, Modularization is a powerful tool, but it may also not be the best fit for your current project as well. Take your time to consider the pros and the cons, and then you can take your time exploring more about this. Best of luck with your exploration :)

--

--