Swift module registration

Let’s modularize our Swift code!

I am using tuist for a while now in my projects. This tool was initially bring into the project’s scope to ease day to day developer operations : no more fiddling with conflicts in xcodeproject files !

Tuist ?

What is tuist by the way ?

Have you ever booted an iOS project, from a few years things have evolved a bit.

Before 2020 I’d say we were doomed to using xcworkspace for defining complex projects.

Cocoapod and Carthage were the cool kids on the block, allowing to download libraries and use them in our beautiful apps.

After 2020, and still now, Swift development have changed to massively using SPM: the built in dependency management tool baked in the swift toolchain. The process is straightforward and easy to set up, a few clicks in XCode and voila! The other way of using SPM is by using a Package.swift file to describe what you want to achieve.

The swift file works very well but lacks from a lot of advanced features available in XCode projects. Or if it is possible, it is not convenient to do.

This is where tuist fills the gap, like Gradle in the Java ecosystem did a few years ago. We can finally use Swift to describe our Swift projects. This means no more copasting things over and over and the dreaded xcodeproj is no more : you’ll only ever have to merge swift files !

Code modularity appears !

It also came in handy for bringing modularity into our codebase. We managed to split dependencies and make modules per feature or functional scope.

This comes at a cost, having to register our different modules to our main app. When I’m talking about registering, I’m considering handling dependencies, routing and navigation. This can represent a lot of modules, a lot of useless lines in our codebase.

To be clearer, our main app contains authentication logic as well as basic http interactions, our modules are dumb as they depend on this external provider to be able to issue any http call.

First implementation

Every of our module was being added to our ResourceLoader class that would call the necessary initializers to set things in motion like the following

func register(){
  LoggingModule.register()
  
}

With our ResourceLoader implementing a protocol exposing everything that was useful for our submodules, protocol which was made available through a simple used-everywhere dependency.

Auto-registration for the win !

We can do module autoregistration by scanning embedded frameworks. As we can do with IoC, the idea is to let every module declare itself to the main consumer. No more long register method in our application launch, and new modules are automatically discovered at launch time !

Let’s illustrate this with some code, we declare a protocol that will allow consumer to call registration for our module.

public protocol ModuleRegistration {
    func register()
}

This is our base building block for registration, every module implements ModuleRegistration to contribute to the main app.

In every module project, we then set a class that will be responsible for registration. It will implement this specific protocol and be populated by its full name into Info.plist.

@objc
public class MyFrameworkRegistration: NSObject, ModuleRegistration {
    public func register() {
      // inject into the global context our dependencies
      // for instance we can do
      // Container.register(MyLogging.self, MyLoggingImpl.self)
    }
}

Please notice important things in this class. It needs to be annotated @objc and inherit from NSObject to be discoverable later on.

The magic being this is that we will declare this class to be our NSPrincipalClass in the Info.plist of our framework.

To do so, we add the final touch by enriching the Info.plist of our module using Tuist project’s description :

.target(
        name: "MyFramework",
        destinations: .iOS,
        product: .framework,
        bundleId: "org.9h41.ios.modules",
        deploymentTargets: .iOS("16.0"),
        infoPlist: .extendingDefault(with: [
             "NSPrincipalClass": "MyFramework.MyFrameworkRegistration",
        ]),
        sources: .paths(["MyFramework/Sources/**"])
)

Main app implementation

Then in our main app, we scan for NSPrincipalClass in all of our included bundles , one just need to call the Modules.autoregister() method in application startup.

public enum Modules {
  public static func autoregister() {
          Bundle.allFrameworks.compactMap(\.principalClass)
              .forEach { clazz in
                  autoregister(clazz: clazz)
              }
  }
  static func autoregister(clazz: AnyClass) {
        let instance = clazz.alloc()
        if let module = instance as? ModuleRegistration {
            module.logger.debug("Doing registration for \(clazz)")
            module.register()
        }
    }
}

Our main app will then scan all included bundles, look for NSPrincipalClass, test if it matches the protocol requirement and then calls the registration function.

The declared dependencies in our frameworks are now properly registered inside our app context, dependency injection system or anything you can imagine.

Swift macro : @VisibleForTesting

As an ancient Java developer, I’ve learned to use a set of annotations to bring meta programming in my projects.Meta programming can be c...… Continue reading

Dark Mode iOS Snapshot

Published on January 10, 2021

Git-gone

Published on December 31, 2020