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.

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 considered as an orthogonal thing to your code, you can inject code to log things, to wrap execution in a transaction or simply provide some context to your fellow developer.

One of my favorite context providing annotation at this time was brought by Google-Annotations package : @VisibleForTesting

Its goal is rather simple: provide the context that the visibility of the variable / method is not as restricted as it should be, but this is for the sake of testing.

When going back to my loved XCode (just kidding), I miss these kind of useful information.

Of course you can add a comment, that maybe someone will read if he begins to wonder why the visibility is too important.

// this method should be private but we want to access it from unit test code  
func doStuff() { }

You can also play with the deprecation annotation to trigger a warning (one more to add and parse with your eyes…)

@available(*, deprecated, message: "This is visible for testing")  
var myState: State

But one thing I was really missing is the ability to really set the proper visibility on my fields while keeping the testability.

Recently, Swift 5.9 had added Macro support. Macros can be seen as ways to generate code based on specific instructions (this brings back old Java-apt memories).

Macro types

There are multiples macro types, whether they can attach to fields, and depending on what they can do:

  • Providing accessors
  • Generating code alongside the annotated field
  • Generating code “in-place”

There are two ways of calling macros :

  • Attached ones with @MacroName
  • Freeform ones with #MacroName

I will not enter the details of each type and implementation, you will see more on this later here and can scout on GitHub repositories for inspiration.

Attached macros are written with a leading @ and can generate code alongside some of our declaration. This allowed me to introduce my own @VisibleForTesting for swift implementation.

The idea behind this is really simple, generate specific code with public visibility that wraps call to the “non-exposed” real method.

This way we get the best of both worlds, we keep our fields private, we tell our colleagues that this field is available for testing and we are able to test it properly.

What does it look like ?

To use this library, you need to add an SPM dependency on this repository: https://github.com/CedricGatay/SwiftMacroUtils

.package(url: "https://github.com/CedricGatay/SwiftMacroUtils", branch: "main")

Then, let’s say you want to give access for testing to the name var of the Dog struct to your testing code, you simply need to do the following

struct Dog {
    @VisibleForTesting
    private var name: String?
}

Under the hood, the macro will generate a public accessor that you will be able to use in your tests

public var __test_name: String? {
    get {
        self.name
    }
    set {
        self.name = newValue
    }
}

The same goes for let, func, and init . The only specific thing to keep in mind that if you annotate a class initializer, you need to mark it as required, otherwise the build will fail (but a nice comment will tell you why).

Dark mode support

As you might know, iOS supports Dark mode since iOS 12. It is pretty straightforward to implement it by using dynamic colors. Either system provided ones or adjust them using trait collections.

Dynamically switch

At times, we can not rely on default colors but we have to listen for traitCollection changes in order to do what is appropriate for adapting the UI to the mode being active.

It is easy to do by checking current traitCollection :

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)

    let userInterfaceStyle = traitCollection.userInterfaceStyle 
    // Either .unspecified, .light, or .dark
}
 

App switcher tricks

One thing that I was not aware is that iOS switches back and forth between active mode and the complementary when the app goes to the background :

To have proper representation in application switcher, if the trait collection is changed, the OS takes a screenshot of your app using light and dark schemes.

Consequences

This can lead to subtle problems, in my case, I was loading a specific stylesheet for MapBox, that, when loaded, was registering / refreshing elements on map. Thus, when the app was put to the background, a lot of things were happening, without a clear explanation (and the app was sometimes crashing due to the very fast switching between map stylesheets).

The workaround is rather simple : if the app is in the background we prevent loading the stylesheet, the trade-off is acceptable : the app can display an invalid preview in application switcher, but, it is nicer on CPU / Network.

Git daily usage

If you’re like most developers nowadays you make an extensive usage of git across a large number of repositories.

The now-classic branching model that consists in using a branch for every fix / feature / experiment before creating a merge request is perfect for collaborating efficiently. However it comes with one downside, local branches can quickly become messy and removing stale branches is not that easy

Removing remote branches

git is a really well designed tool and removing remote references from your local repository is easily done by

git remote prune origin

This will remove local references to non existing branches, however it is not useful to remember this command, we can configure git so that it prunes automatically on fetching :

git config --global fetch.prune true

Removing local branches

This is where things can be quite messy, as local branches might have been merged, rebased, or squashed, and “classic” commands allowing to detected merged branches are not always working as they ought.

If you search how to clean up local branches, you might find a lot of commands involving git branch --merged with grep and xargs all the way.

I found out a little neat tool to do so, called git-gone (github/lunaryorn/git-gone), written in Rust 🧡

Install it

As all rust project, it is packaged and easy to install using cargo

cargo install git-gone

List branches to prune

Before doing anything silly, we can list the branches that are candidate to removal (the -f flag forces fetching remote)

$ git gone -f list
feature/asciidoc
fix/imageCompress

Prune’em all

And finally, you can remove them (the -f flag is still here to sync with remote)

$ git gone -f prune
Deleted feature/asciidoc (restore with `git checkout -b feature/asciidoc 8f007a`)
Deleted fix/imageCompress (restore with `git checkout -b fix/imageCompress af523b`)

git-gone is even clever enough to give you the command to restore a branch if change your mind (as long as you did not git gc the revision is still available locally, so you can restore it)

Welcome to the mac world

Wether you’re new to macOS or an user coming from different machines, the tools we tend to use is very important to be productive.

Here you’ll find my list and what I install on a fresh machine to get started.

System tools

First and foremost, I start with brew to handle all my packages, but instead of doing so manually, I use my dotfiles and my “magic” install script that does all the heavy lifting for me.

Keyboard tools

I use a TypeMatrix with a Colemak layout and also the internal keyboard of my laptop with its default layout. Colemak layout is available on a fresh macOS install but it is far from perfect as there are a lot of missing dead keys (to type accented letters mainly), so I start by installing the layout provided on this page: Colemak mac

To easily switch between the two and get almost the same feeling, I use Karabiner Elements.

Everyday tools

  • sdkman: to manage installation of various sdk (mainly Java based)
  • iTerm: nice terminal app with profiles
  • tmux: multi terminal in one window, switching terminal with a keystroke
  • zsh: Z-Shell
  • starship: fast shell prompt
  • ripgrep: faster grep
  • bat: nice cat alternative (with paging / highlighting)
  • exa: replacement for ls
  • Alfred: Spotlight with more features, this is my main app launcher / switcher
  • SetApp: App subscription service, use many tools from this (BetterTouchTool, iStat, BarTender…)