Modular iOS Guide
Are you looking to reuse code in multiple apps? Or maybe you want to offer some code to your customers to include it in their apps so they can consume your services quicker. Having chunks of reusable code that can be used with multiple apps is called module.
How do you modularise code? How does modularisation work? Can modules includes images?
This guide will help you answer the above questions and more. On iOS there is more than one way to modularise code. I will walk you through different options. I’ll cover what the differences are between each of them. Then I’ll look into the options on how to distribute it.
I will not go in depth with how-to steps. There will be no coding involved in this guide. My aim with this guide is to help you see the larger picture and point you into the right direction. I have written tutorials on with detailed steps on how to build, package and distribute modules on iOS for the different options. I will provide links throughout this guide and at the end of it.
In this post I will cover:
- What a module is
- Why modularise your code
- Why not to modularise your code
- Modularisation options
- Packaging options to prepare your module for distribution
- Distribution options for your module
For this post I assume you are comfortable with iOS development and programming in general. You should have a basic understanding of how apps works in the iOS ecosystem.
What is a module?
In simple terms its a package that contains code. Some options allow resources such as images to be included too.
For example you may want to separate your login screen into a separate module. The module will include the code and resources to launch and display the login screen. The module will also include images used in the login screen. The app can then include the login module. Subsequently the app can then load and display the login screen from the module when needed.
You may be asking why should I break my code into modules? There are many reasons. Here is a non-exhaustive list of reasons why you should modularise code:
Reuse of code
If you’d like to use some code in more than one app or target then modularising your code will allow you to share easily your code with these separate apps.
Reusing code means that the code needs to updated only in one place. Alternatively you can copy and paste code from project to project. However if your code contains bugs then you’ll have to fix the bug across all of your projects. This can lead a higher maintenance cost.
Modularisation allows you to share parts of your apps code publicly for others to consume and change. This isn’t an option for everyone.
However if the code is not core to your business and others are replicating the same code then modularising and going public can allow you to share the maintenance burden with other developers.
Furthermore other developers might even add features to the module which you could consume for free.
Another reason is that your business offers services to other app builders to consume. Instead of having each of your customer producing the code to consume your services, you can have them consume some already packaged code. This will allow them to start consuming your services faster and you’ll start making money earlier!
Separation of concerns
Different parts of your app will have different reasons to change. For example when you change your authentication methods then only the authentication code needs to change.
Some parts of your code are meant to be consumed by only one class or a set of classes that are related to the code.
When your code lives within the main app other parts of the code have access to the code that they are not meant to be using. Sometimes developers can make mistakes and use code from places they are not intended to. This could leads to bugs or code entanglements.
Modularisation can keep code private to other parts of the app that are not intended to be using this code.
Speeding up build time
Building your project from scratch means rebuilding code that is unchanged or not meant to change as part of your app. Re-compiling code that does not change can increase your compilation time as your app grows larger and more complex.
However modularising your code and consuming compiled modules can speed up your build times significantly. Compiled modules means that when your app code builds from scratch then only the app code will be compiled and not the module. Thus reducing the amount of code compilation on any given project.
Why you shouldn’t modularise your code?
Modularization has some drawbacks. These should be taken into account before deciding whether to modularize your code. Here are some reasons why you shouldn’t modularise your code:
First the process of modularising your code can be complex. First you need to extract the code where it is used. Then you need to package and distribute this code. Finally you’ll need to make sure where the module code is used the proper import statement is declared to use that package.
Furthermore if your module has shared code with your app then you might be required to modularised the shared code too.
Some feature developments might require changes in multiple modules as well as the app consuming it. This may increase the cost of development for some features.
Increasing launch time
This point requires some understanding about how modules works on iOS.
Having too many dynamic frameworks — one of the modularisation options — can slow down the launch time of your app.
Modularisation options for iOS
There are 2 ways to distribute modular code for iOS:
The option you choose can have different implications for your how your module code runs. Let’s see how each of these work and then compare them.
A framework is a structured directory that can contain shared code and shared resources such images, nibs (compiled form of xibs and storyboards) and other assets.
The framework lives separately to the app and is loaded separately too. The system loads the app which in turns tells the system that it requires to consume the framework code to run. The system then loads the framework that the app relies on. All of this happens at runtime.
A static library is a collection of compiled source code files. Let’s say we have
FileC.swift. With a static library we can compile these files and wrap them inside a single file containing all of these. The file has has an extension of
.a; short for archive. Sort of like getting some pages and making a book out of it.
When our app consumes code statically the code that it consumes gets copied into the app executable binary.
When the system loads the app, the static library functionality is loaded with it as single executable.
What is the difference between static libraries and dynamic frameworks?
Static Libraries are loaded with the app which could lead to faster app launch times.
Static Libraries can’t have resources included within. These can still be imported separately using bundles. However it requires delivering an additional artefact.
Dynamic libraries are loaded separately and can have slower app launch times.
Dynamic libraries can include resources.
How to package your iOS module for distribution
There are multiple ways of packaging your iOS module for distribution. Packaging options include:
This when you deliver your code to the consumer. The code is accessible and readable. The code gets compiled on the consumers machine along with their code.
This form doesn’t benefit from faster build time compilation as the code gets rebuilt on every clean build of the consuming app.
Additionally the consumer can make changes to your code.
The module code gets compiled and then distributed. The consumer uses the compiled form. The consumer will not be able to read and modify the code. The consumer will enjoy faster build times than open code packaging. Furthermore you are able to hide your code.
Compiling a framework to a
.framework or a static library to an archive (
.a) will only work with either simulators or devices but not both. There is a way for consumers to have both build variants in a single artefact called XCFrameworks (more on the next section).
Compiled and packaged as an XCFramework
XCFrameworks allow you to package multiple built variants of your module into a single artefact. That means that within a single package an integrator will be able to use your module with simulators and devices.
You can learn more on how to build XCFrameworks for frameworks on the post below:
How to build universal iOS frameworks using XCFrameworks
Compiling iOS frameworks for distribution is not a straightforward task. Out of the box Apple offers no option for…
You can learn more on how to build XCFrameworks for static libraries on the post below:
How to distribute your module
There are multiple options on how to distribute modular code. The distribution depends on the modularisation option and how its packaged. Each distribution method has its own benefits and drawbacks.
Here I will also list out your distribution options for your module with and without additional tools. These additional tools listed here are dependency managers that help to make deploying and integrating with your module easier.
Let’s take a look at some of the options.
Distribution without dependency managers
Here you have three options:
- Distribute your module Xcode project
- Distribute your module compiled
For the first option the consumer of your framework can state directly the module target as a dependency of their app. This is achieved through an Xcode workspace which can contain one or more Xcode projects in a single Xcode window.
The second option is to compile the module and then distribute it. The consumer downloads the compiled module into their project and links to it directly.
Distribution using Cocoapods
Cocoapods is a popular iOS dependency manager. In simple terms it allows you to specify which dependencies (or modules) your app depends on. Cocoapods takes care of the installation and configuration of these.
Again here there are two ways you can distribute your module through Cocoapods:
- Distribute your module code
- Distribute your module compiled
The first option tells Cocoapods where your source code lives and the files to include. Cocoapods then fetches the source code and wraps it in a module. The difference with linking directly to the Xcode project is that the integrator has much greater control on the configuration of the project. They can even choose how they would like to link with your code; statically or dynamically. Cocoapods takes care of the rest.
The second option is much similar to installing compiled modules without Cocoapods. However Cocoapods still adds value by allowing greater version control to the integrator. Cocoapods supports distribution of compiled dynamic frameworks, compiled dynamic frameworks packaged as XCFrameworks, compiled static frameworks and static libraries as XCFrameworks.
Distribution using Swift Package Manager
Swift Package Manger (SPM) is the latest dependency manager to join the party. Swift Package Manager is that it is included with Swift and is maintained by the Swift community.
Similarly to Cocoapods, SPM relies on package specification file that tells the consumer end how to install and link to the package.
There are two way to distribute your module however with some limtations:
- Distribute your code as a static library
- Distribute your module compiled
Note for the first option at the time of writing you can only distribute code without resources. Furthermore dynamic linking for iOS is not supported.
The second option is to distribute your module compiled. This is similar to Cocoapods. The difference is that Swift Package Manager does not support the distribution of archives (
.a) and frameworks (
.framework). However it does support the distribution of XCFrameworks for both static libraries and dynamic frameworks.
Distribution using Carthage
The final popular option for iOS dependency manager although the least popular option from the list is Carthage.
Carthage is very different to the first two options. It does not require a module specification file. Alternatively it relies on the Xcode project files to know how to build the module. If the Xcode project specifies dynamic frameworks then the module will be built as universal dynamic framework.
Carthage only fetches and builds the dependencies for you. It does not link the compiled module to the consuming app project. That is up to the consumer of the module to do.
If the module is already compiled then it only fetches the compiled artefact for you.
Carthage is a great option for those wanting a very simple solution to their dependency management needs. Carthage can also work with projects that are not intended to support Carthage.
However it being the least popular means that it is the slowest to get updates and bug fixes.
I have included links to earlier tutorials where I explain step by step of different ways to package and distribute iOS code. Below you can find the full list:
- Reusing code with Swift frameworks
- Distributing Swift Frameworks via Cocoapods
- How to build universal iOS frameworks for distribution
- Distributing Compiled Swift Frameworks via Cocoapods
- How to distribute iOS frameworks using Carthage
- How to build universal iOS frameworks using XCFrameworks
- How to distribute compiled iOS frameworks using Swift Package Manager
- Distributing universal iOS frameworks as XCFrameworks using Cocoapods
- Reusing code and resources with Swift static libraries and resource bundles
- Distributing compiled iOS Swift static libraries and Swift static frameworks
- How to distribute compiled static frameworks via Cocoapods
- How to Distribute iOS Libraries With Swift Package Manager
- How to build universal iOS static libraries using XCFramework