Over time, any code base grows as the project grows and matures. It creates two major constraints for developers: how to keep the code in order while keeping build times as short as possible. Let’s look at how a modular architecture solves this problem.

Module,

Starting with modules, we can represent them as code resources that are isolated from other main applications. Then, add it as a dependency to our iOS app.

Creating modules can also greatly improve the testability and reusability of your code.

This dependency can be technical aspects of the application (networking, storage, and so on) or functional (search, accounts, and so on) to encapsulate complexity.

Once defined, we can start adding the code and resources to be quarantined.

There are only two ways to package code: dynamic frameworks and static libraries.

The main difference between the two is how they are imported in the final executable. Static libraries are included in compile types and can be copied in executables, while dynamic libraries are included in executables at run time and never copied, so startup times are faster.

Create a module

Now that we know what can be a module, let’s create one. Suppose we create a new application for e-business, we need to create a specific dependency to represent the core concepts of our application. I’m going to call it Core.

First, I create a dynamic framework project.

Because it is an e-commerce application, the core of our application is represented by the products we sell. Let’s create a simple object for this.

public struct Product {
    let name: String
    let price: Double
}

Copy the code

Since our users wanted to browse the product, we needed a way to get it. Let’s create a protocol to expose this.

public protocol ProductServiceProtocol {
    func getAllProducts() -> [Product]
}

public final class ProductService: ProductServiceProtocol {
    public init() { }

    public func getAllProducts() -> [Product] {

        // imagine we fetch products from server
        let products = [Product(name: "shoe", price: 100), Product(name: "t-shirt", price: 30)]

        return products
    }
}

Copy the code

Note that we need definitionsinitforpublic, otherwise,internalThe default is, which makes it unusable from other imports.

Our module is ready, let’s import it into the application.

The import module

Once a dependency is created, we can include it in our application. For this section, I first created a workspace, which makes it easier to work on two projects at once.

I added an application to my workspace and to my core module. They are not yet linked.

To import the Core framework into the application and be able to use it, I just drag and drop the framework file into the main application section. Linked Framework and Libraries

If you build the main application, you can see that Core is part of it. Great, I can use it now.

With a very simple example, let’s see if you can get the product in the main application.

import Core

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let products = ProductService().getAllProducts()
        print(products)
    }
}

Copy the code

No warning, the console records the result as expected.

[Core.Product(name: "shoe", price: 100.0), the Core Product (name:"t-shirt", price: 30.0)]

Copy the code

Wait, but I have a lot of dependencies, some of them connected, how do I deal with them?

With more modules and dependencies, the next obvious question is how to manage them. Let’s look at some dependency managers.

Rely on the manager

To deal with the increasing number of dependencies, we need some way to group and manage them.

Let’s start with a naive approach without a dependency manager, where all the code is in a repository under the same project.

If it’s great for small applications, it can quickly become a headache if you have more than one or two modules. Folders will not help with separation.

Taking this approach further, the next step is to separate projects in a workspace. This is the solution demonstrated above. This is a good way to isolate code and understand its visibility and accountability.

However, it is still under the same Git repo. Buybacks can get crowded as projects expand. Also consider build time: each dependency is rebuilt using the main application.

Let’s try to separate git repo and use Git submodules. It’s better, and the code can be reused in other projects, but we’re still limited by the build time.

Another Angle for dealing with dependencies is to create an * umbrella framework * to embed each dependency in a package to limit builds and keep the workspace clean. The truth is, if you use CocoaPods, you probably already do.

If you look at the workspace and explore the Pods project, this is how dependencies are handled. However, build time remains a bottleneck.

Finally, another popular dependency manager is Carthage. The main difference is that dependencies are built before they are imported. This is the best solution to keep the build optimized.

I didn’t mention Swift Package Manager (or SPM) because it’s only available for macOS so far.

They are also other emerging solutions for incremental builds like Buck or Bazel, but first for the continuous integration pipeline.


In summary, we learned how to isolate code into modules, making it easy to reuse and test while keeping the project clean. Sample projects with modules can be found here.