Getting Started with iOS App Modularization — An Introduction
This is the first part of the modularization tutorial. Refer to the list below for the complete parts :
Part 1: Introduction
Part 2: Extracting Catalog
Part 3: Final Extraction and Micro App
App modularization is an issue everyone runs into as a software engineer, moreover, if your app is already grown to the scale that it takes too long to build the entire app, just like the Tokopedia app. In our case, we have to find a way to fasten our build time to meet our weekly release and this is where the modularization helps. By modularizing our app, we can choose to build what we need at the moment thus improving our build time and everyone is happy!
Tokopedia uses a custom modularization approach to enable fast development, but before going to custom modularize your app, you have to understand the basics of modularization and all the other related things you need to know, this article will cover all of them and guide you to have your first taste of modularization, hang tight!
There is a slew of solutions you can use to tackle modularization. From splitting codebase chunks to increase maintainability to creating good encapsulation. But what is the motivation behind modularization?
When should you split your codebase into modularized parts?
Why should you even do such a thing?
What is Modularization
Modularization is the practice of dividing features of an application into separated modules so that they are interchangeable, reusable, and easily maintained. If you prefer a less-technical explanation, think of a car stripped off its steer, dashboard, and wheels. All these smaller parts of the vehicle can be fully customized on their own and easily replaced by other parts of the same kind if needed.
Most teams fall back to the modularization philosophy to split monolith codebases that are too large to handle. It is because as time goes on, the build time grows linearly with the lines of code. Slow build time hurts developers’ productivity in the long run and will result in long waiting times before developers can fully test their apps.
By splitting the features, developers will only build the needed features without having to compile the entire app. Theoretically, this will decrease the compile time, improving developers' productivity.
When Should You Modularize Your App?
One question might arise when considering modularization. How do you know if your app needs to be modularized or not?
The answer to such a question is pretty subjective since it depends on your aims from the start and your current situation. For example, suppose you are building a startup that needs to have as many features delivered as possible within a tight schedule. In that case, modularizing your app from the very beginning will result in either :
1. Premature optimization.
2. A good foundation while the app is still in its infancy.
All of these concerns are valid and you must make decisions with this in mind.
How to Modularize An iOS App
Let’s say your team finally decide it is the best time to perform modularization, and the next question is how to start modularizing your app.
Rest assured, you will learn how you can modularize an iOS project step-by-step.
Consider an illustration below :
Let’s say you have an iOS app with three features: catalog, product detail, and shared. One way to get the app modularized is to split the app into four different modules, each with a library and a bundle. In iOS, a module is called a framework, and this article will use those two words interchangeably.
A library is a collection of resources and the code itself, compiled for one or more architectures, in shorter words, it is all the code you have written. Whereas a bundle in iOS is a directory with subdirectories usually used to store assets, files, and many other specific types of content. The framework itself is a group that contains all the libraries and the resources it needed.
Furthermore, there are two important frameworks that need to be explained :
1. Main
Like its name, the main is your main framework; think of it as your app’s entry point.
2. Shared
Think of this framework as the place of all components used within the entire app, containing everything like the code of UI components, (e.g accordions, bottom sheet, popup, etc. ). The shared framework is the best place to start splitting the app since it does not depend on anything externally.
Moreover, iOS has two types of frameworks, dynamic and static, respectively.
A dynamic framework is a bundle of code loaded into an executable at runtime instead of compile time. It behaves exactly like a dynamic library, except for the difference that a dynamic framework is a dynamic library embedded in a bundle along with optional assets, such as images. UIKit and the Foundation frameworks belong to this category.
On the other hand, a static framework is a bundle containing a static library file. They are statically linked, or copied into the executable binary, and not loaded at runtime.
For the sake of simplicity, this article will only cover extracting a monolith into dynamic frameworks.
You can read more about dynamic and static libraries here.
Let’s Get Started
Clone the starter project here
Open the MyAwesomeApp.xcodeproj
. Try to run the project to get a clue about the app you are trying to modularize.
As you can see, the app is pretty straightforward, you have a catalog page showing all the product cards and several rows of promotional and inspirational products. Another feature to add is navigating to the respective product detail page upon tapping one of the product cards.
1st Step: Convert App Into Xcworkspace
The first thing you have to do to modularize our app is to convert our app to an xcworkspace
Click file > new > workspace, and set the workspace name as MyAwesome. Then, save the .xcworkspace
inside the modular folder.
Another window will appear and you can close the previous window. Click File, Add Files to MyAwesomeApp, and add your previous myawesome.xcodeproj
to your workspace.
Your Xcode will then become something like this.
Until now, you have successfully added the xcodeproj
inside an xcworkspace
. Now, you can start to split the features into their own corresponding frameworks.
Remember that you must start extracting from the least dependable frameworks. By referring to the dependency diagram before, you can see that the least dependable framework is the Shared framework. Start by extracting this framework first.
2nd Step: Creating the Shared Framework
Create a new framework named Shared
and add it to our workspace. Refer to the GIF below for the step-by-step.
One thing to note here, remember to choose MyAwesomeApp.xcworkspace
the place to add the new framework. On top of that, also make sure the group refers to the MyAwesomeApp.xcworkspace
.
Now your workspace will look something like this, with two xcodeproj
inside an xcworkspace
.
The next step is you have to move the all contents of the folder Shared
in the MyAwesomeApp
framework ( we will call this the main framework in future usages for simpler and better understanding ) to its own Shared
framework’s folder. You can use the help of the finder app in order to do so.
Right-click on the Shared
folder in the main framework, choose Show in Finder
, we will call this Shared folder the source folder.
Do the same thing for the Shared
folder in the Shared
framework, and we will call this folder the destination folder.
Move all the folders inside the source folder to the destination folder, and remember to tick copy items if needed
when prompted. You can refer to the GIF below for a better understanding.
Next, you have to adjust the module property of the moved asset files. Refer to the UI
folder in the Shared
framework. Click the custom class section on the right pane of the code editor, click the module
text field and type Shared
, the framework’s name. Doing so will cause the Inherit module from target
checkbox will be automatically unchecked.
This step is to set the resource file’s module explicitly to the Shared framework since you have moved the file there, if you do not, the file will still think it belongs to the main framework, since it inherits from the target, causing unexpected behaviours.
Do the same for the InspirationItemCollectionViewCell.xib
and InspirationCollectionViewCell.xib
.
The next step is the most essential part of integrating your Shared
framework into the main framework.
Navigate to the main framework, locate the Frameworks, Libraries, and Embedded Content
section, and choose the Shared
framework to be embedded. This step is to tell the main framework that it has the Shared
framework as the dependency when running the app.
Try to run the app, but expect the build to fail because of these errors.
3rd Step: Resolving the errors
Let’s solve the errors sequentially, navigate to the CatalogPageNetworkProvider.swift
As you can see, most of the errors are caused by the file not having the definition of NetworkResult
, this is because the NetworkResult
is originally located in the Shared folder, and because you have moved all of the Shared’s contents to its own frameworks, the code from the main framework does not have access to them again.
The solution for this is you have just to import the Shared framework in the file.
But the error persists even if you have imported the framework, this is still happening because apparently the NetworkResult
has an internal access control level, any definition that has internal access control level can only be accessed from its own module, and since you are trying to access the enum from outside the framework, you still cannot locate the definition, which explains the error.
Note that access control level plays a huge role in modularization, you have to determine which code that are exposed and which are not
Change the access control level to looser access controls, such as public or open. Navigate to the shared framework, find the NetworkResult.swift
file and add an access control before the enum definition.
The errors in CatalogPageNetworkProvider.swift
should be solved now.
Navigate to the ProductDetailPageViewController.swift
. The errors are because the file cannot find the definition of the Product
struct and the load
function since those definitions are already moved into the shared framework, hence the solution is the same as the previous errors, which is
1. Adjust access control levels
2. Import Shared
Import the shared framework at the top of ProductDetailPageViewController
and add a public
control level in front of the load
function definition in UIImageView+Extension.swift
. You should have resolved the errors.
Next up, navigate to the CatalogPageViewModel.swift
, the same errors are happening in this file, it cannot identify the HashDiffable
protocol and the ProductResult
.
You can use a similar solution, import the shared framework on the file and add a public
access control to the HashDiffable
definition and its isEqual
function.
Lastly, the CatalogPageViewController
, where it cannot identify the Product
struct. Simply importing the shared framework in this file will solve the error.
Try building the app and it should have been able to run now.
4th Step: Handling Bundle Issues
One thing that you will notice upon the app launch is your app does not look like it used to be before modularization, you have only presented a white screen and your list of product cards is gone. Why is this happening?
Luckily, looking at the app console, you should see the error’s root. A message : URL NOT FOUND
.
If you try to find the error string in the entire app, you will notice that it is only coded in the CatalogPageNetworkProvider.swift
. Furthermore, If you look further into the code, the errors are because you can't locate the ProductData.json
This happens because the ProductData.json
is already moved to the Shared framework, but how can you access the file using a bundle? 🤔
First, to access a bundle from a framework, you have to know its identifier.
Navigate to the Shared framework, click on the target and inspect the general tab. Turns out you already have a bundle identifier of the shared framework, and you can use this in order to access the files within the framework.
Navigate back to the CatalogPageNetworkProvider.swift
, change the guard
validation in both of the fetchProduct
and fetchInspiration
Instead of using Bundle.main
, use your shared framework’s bundle by initializing it using the bundle ID. Remember to update the bundle ID to your framework’s bundle ID as your organization name most likely will be different.
Try to run again the app. This time, instead of getting the error message URL NOT FOUND
, your app will crash.
Suppose you try to debug further from the stack trace, you will see that the crash is caused by CatalogPageViewController
calling the cellForIndexPath
function, which triggers the collectionView
to load the ProductCollectionViewCell
, but it was unable to do so, causing the crash.
On the brighter side, this crash proves you have successfully loaded the JSON files since it is happening on the
CatalogPageViewController
‘s function that is triggered upon receiving data.
Your view controller causes the error since it cannot identify the ProductCollectionViewCell
. The solution is to specify the bundle where this view controller loads the NIB.
Locate the viewDidLoad
function inside the CatalogPageViewController
, and change the code to become :
Try running the app, and you will get another crash caused by the same issue, the InspirationCollectionViewCell
could not load its NIB. Locate its awakeFromNIB
function and change it into this
Remember to adjust the identifier to your own bundle ID
After changing the code, try to rebuild the app and it should run successfully!
You have successfully extracted your shared module, leaving only two more features before you finish modularizing your app. We will continue extracting the catalog feature into its framework next.
You can access the code materials until this point here.
Now let’s continue to the second part here