URL Routing on macOS

Most iOS developers have probably dealt with URL schemes and URL routing at some point during their career. This topic became even more important after the introduction of deep linking in iOS 9. Today we’re going to take a look at how this works on the Mac and create a small URL routing library.

The iOS-way

If you’re familiar with URL schemes on iOS, skip to the next section. If you’re not, this is a brief overview of how it works:

You register a unique URL scheme for your app using the Info.plist file. Let’s call it myapp. If another application or website opens an URL with your scheme, e.g. myapp://event, iOS either launches your application or brings it into foreground and notifies your application’s delegate by calling the following method:

func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool

From there on it’s up to you to handle the received URL. You could parse the URL yourself or use one of the countless URL routing libraries that emerged during the years, my favorite being JLRoutes.

Back to the Mac

The Mac is a much older platform with tons of legacy APIs. Registering schemes works just like it does on iOS using the Info.plist, but receiving URLs is tricky. When I first took a look at this, I just assumed NSApplicationDelegate would handle opened URLs, similar to iOS. That couldn’t have been farther from the truth.

After a quick StackOverflow search, I knew NSAppleEventManager is what I need. Now, what is NSAppleEventManger you ask?

NSAppleEventManager Provides a mechanism for registering handler routines for specific types of Apple events and dispatching events to those handlers.

In case you want to know more about this, you can read about Apple Events in Cocoa here. This article just covers what we need to know in order to route URLs on macOS.

NSAppleEventManager allows you to register one object (called handler later on), that implements a certain method, to receive events that are of a certain class and have a specific identifier. In our case the class is kInternetEventClass, which is a constant of the type AEEventClass, and the identifier is kAEGetURL, a constant of the type AEEventID. The method signature we need in order to receive events must be equivalent to this:

func handleEvent(_ event: NSAppleEventDescriptor, with replyEvent: NSAppleEventDescriptor)

Notice that we receive events as NSAppleEventDescriptor objects. NSAppleEventDescriptor is a nested type which stores other instances of NSAppleEventDescriptor as parameters, each one associated with a keyword. The URLs our application receives will be wrapped in a descriptor, which itself is stored inside the main event descriptor, and can be accessed using the keyDirectObject keyword, a constant of the type AEKeyword.

let keyword = AEKeyword(keyDirectObject)
let urlDescriptor = event.paramDescriptor(forKeyword: keyword)
let urlString = urlDescriptor?.stringValue

Now that we know how to receive URLs, it’s time to create a class which is able to receive URLs and actually handle them. A Router.

Swift, but…

While all the code is going to be written in Swift, the majority of my macOS projects are still Objective-C. Thus the Swift code I’m presenting here is intended to be compatible with Objective-C. We will not only use classes, as Swift structs are not compatible with Objective-C, but they will also all inherit from NSObject. The code, however, doesn’t depend on NSObject or reference types. You could easily convert the code to use value types (to a certain extent) and Swift classes.

Router

The Router will receive events, extract URLs from them and take appropriate actions depending on the content of the URLs.

public class Router: NSObject {
    
    public static let global = Router()
    
    public override init() {
        super.init()
        NSAppleEventManager.shared().setEventHandler(self, andSelector: #selector(handleEvent(_:with:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL))
    }
    
    deinit {
        NSAppleEventManager.shared().removeEventHandler(forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL))
    }
  
    @objc
    private func handleEvent(_ event: NSAppleEventDescriptor, with replyEvent: NSAppleEventDescriptor) {
        guard let urlString = event.paramDescriptor(forKeyword: AEKeyword(keyDirectObject))?.stringValue else { return }
        guard let url = URL(string: urlString) else { return }
        print(url)
    }
  
}

This is the first implementation of our Router class. Instances of this class will register themselves as handler for URL events (that’s what I’m going to refer to from now on when talking about the combination of kInternetEventClass and kAEGetURL) during instantiation and unregister during deallocation. Incoming events will be handled, the URL will be extracted and printed to the console. So far so good!

Remember how I mentioned earlier that there can only be one handler per class/identifier combination? If we created two Router instances, the second instance would replace the first one as handler for URL events. Going further, if the first instance got deallocated before the second instance, the first instance would actually unregister the second instance as handler and no one would receive URL events anymore. We’re going to solve this later on, for now we’ll only access the shared instance global to avoid this.

Now, what should our URL routing actually look like? We want to run a certain action — a simple closure — when we receive an URL that matches a specified pattern — consisting of of a scheme and a path. We’re going to encapsulate this association of an action with a pattern in a separate class and call it RouteHandler.

RouteHandler

In order to match URLs, we’re going to need a scheme and a route. The scheme used to open the app might be unimportant, so we’ll just make it optional and match any scheme if there’s none specified.

But hold on, what’s a route? You only mentioned a path earlier!

Imagine a route to be a pattern of a path. We need patterns for two things: placeholders and wildcards.

Placeholders

Let’s say you’re creating a social network app and want to add a route to view user profiles. Instead of registering paths for every user, which would be virtually impossible, you just register a route with a placeholder: /view/:user. When receiving an URL for this route, whatever is in place of the :user path component will be used as the user parameter.

Wildcards

It’s possible you want to allow an unknown number of path components at the end of an URL because you’re not aware of all possible paths beforehand. In this case, you register a route with a wildcard at the end: /super/secret/path/*. In this case, only the first three path components need to be matched while the remaining components can be ignored.

I previously mentioned I’m using JLRoutes on iOS. For our Mac library, I’m going to shamelessly copy Joel’s idea of having priorities. The idea behind priorities is pretty self-explanatory, so I’m just going to leave you with an implementation of the concepts described above:

public typealias RouteHandlerAction = (URL) -> Bool
public typealias RouteHandlerID = Int

open class RouteHandler: NSObject {
    
    public let route: String
    public let scheme: String?
    public let priority: Int
    public let action: RouteHandlerAction
    
    var id: RouteHandlerID
    
    public required init(route: String, scheme: String?, priority: Int, action: @escaping RouteHandlerAction) {
        self.route = route
        self.scheme = scheme
        self.priority = priority
        self.action = action
        self.id = -1
        super.init()
    }
    
    @available(*, unavailable)
    public override init() {
        fatalError()
    }
  
    public func handle(url: URL) -> Bool {
        if let scheme = self.scheme {
            guard scheme == url.scheme else { return false }
        }
        // … route matching
        
        return self.action(url)
    }
  
}

After making the default initializer unavailable, we provide a new designated initializer that allows us to set all required properties. handle(url:) will first check if an URL can be handled by the handler and then do so in case of a match. The returned boolean lets the caller, which is going to be our Router, know, whether the URL was handled successfully or not.

RouteHandlerID

Yeah, I kind of sneaked this into my code. We’re going to use handler IDs later so users can identify handlers without storing them.

Back to the Router

Our Router class will act as a storage and manager for RouteHandler objects.

public class Router: NSObject {
    // …
    private(set) var handlers: [RouteHandlerID: RouteHandler] = [:]
    private var currentHandlerIndex = -1 
  
    @discardableResult
    public func register(_ handler: RouteHandler) -> RouteHandlerID {
        self.currentHandlerIndex += 1
        let id = self.currentHandlerIndex
        self.handlers[id] = handler
        handler.id = id
        
        return id
    }
    // …
}

We’ll store handlers in a simple dictionary and use their IDs as keys. Later on, this will allow us to unregister handlers super fast if we know their IDs. If you imagined some super fancy calculation happening to create IDs you were wrong — it’s as boring as it can get, an increasing integer.

Registering a handler using our current implementation looks like this:

let handler = RouteHandler(route: "/", scheme: nil, priority: 0) { url in
    return true
}
Router.global.register(handler)

A bit cumbersome, right? We’re not even using default parameters! Let’s solve this by adding two convenience methods to our Router:

public class Router: NSObject {
    // …
    @discardableResult
    public func register(_ route: String, for scheme: String? = nil, priority: Int = 0, action: @escaping RouteHandlerAction) -> RouteHandlerID {
        return self.register(RouteHandler(route: route, scheme: scheme, priority: priority, action: action))
    }
  
    @discardableResult
    public func register(_ routes: [String], for scheme: String? = nil, priority: Int = 0, action: @escaping RouteHandlerAction) -> [RouteHandlerID] {
        return routes.map { self.register($0, for: scheme, priority: priority, action: action) }
    }
    // …
}

With this in place, registration will be much cleaner:

Router.global.register("/") { url in
    return true
}
Router.global.register(["/entrance", "/backdoor"], for: "secret") { url in
    return true
}

The first example, just like the one above, will match any route at its root: abc://, def://… With the second example we can even match two routes for a specific scheme: secret://entrance and secret://backdoor.

Now we got our fancy registration and everything set up, but none of our handlers will ever be called. To do so, we need to first sort all registered handlers by their priority in a descending order and then give each handler a chance to handle the URL:

public class Router: NSObject {
    // …
    @objc
    private func handleEvent(_ event: NSAppleEventDescriptor, with replyEvent: NSAppleEventDescriptor) {
        guard let urlString = event.paramDescriptor(forKeyword: AEKeyword(keyDirectObject))?.stringValue else { return }
        guard let url = URL(string: urlString) else { return }
        
        let handlers = self.handlers.sorted {
            return ($0.1.priority > $1.1.priority)
        }
        for (_, handler) in handlers {
            guard handler.handle(url: url) else { continue }
            break
        }
    }
    // …
}

When sorting a dictionary, the result will be an array of tuples, each consisting of the key and value of an entry. In our concrete case, the return type is [(key: RouteHandlerID, value: RouteHandler)]. This forces us to use the syntax you see in the code snippet above. While we could use map to get an array of handlers as opposed to an array of tuples, this would add an unnecessary loop.

After sorting our handlers, we iterate through them and let each handler try to handle the URL. As soon as we find a matching handler, we break the loop. The only issue now is that we don’t actually perform any URL parsing or route matching yet. If you look closely at the current code of our RouteHandler class, you see we’re only matching the scheme but don’t look at the contents of the URL before passing it to our action. With the current implementation, the first handler matching a certain scheme will be called and the route will be completely ignored.

To match routes with URLs, we need to parse incoming URLs and compare their components with the components of all routes. If we performed the parsing of the received URL inside our RouteHandler class, we’d do the same work each time we call handle(url:) on a RouteHandler object. To avoid this unnecessary repetition, we’re going to move all the work we can do beforehand into a separate class.

RoutingRequest

RoutingRequest objects can be instantiated using a URL. The initializer is failable due to the fact that the URL object could be formatted in a way unsuitable for our purposes. The URLComponents class is going to handle the very basic parsing and validation for us, we then only save the properties which we later need to match routes.

public final class RoutingRequest: NSObject, NSCopying {
    
    public let url: URL
    public let scheme: String
    public private(set) var parameters: [String: String?]?
    public private(set) var wildcardComponents: URLComponents?
    
    let pathComponents: [String]
    let queryItems: [URLQueryItem]?
  
    public init?(url: URL) {
        guard var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return nil }
        self.url = url
        
        guard let scheme = components.scheme else { return nil }
        self.scheme = scheme
        
        components.moveHostToPath()
        self.pathComponents = components.pathComponents
        self.queryItems = components.queryItems
        
        super.init()
    }
    // …
}

If you’re familiar with the URLComponents class, you probably noticed our initializer is using methods or properties not supplied by the standard library. The code is using a small extension on URLComponents which you’ll find along with the full implementation of the library on GitHub. The final implementation will also provide some options to control the URL parsing.

The parameters and wildcardComponents properties are not set during instantiation as they depend on the route of the RouteHandler object. Matching a RoutingRequest with the route of a RouteHandler will be called fulfillment, as the request is incomplete before being matched with a route.

public final class RoutingRequest: NSObject, NSCopying {
    // …
    func fulfill(with route: String) -> Bool {
        var parameters = [String: String?]()
        
        for (index, component) in self.pathComponents.enumerated() {
            // route parsing …
        }
      
        if let queryItems = self.queryItems {
            for item in queryItems {
                parameters[item.name] = (item.value?.characters.count != 0) ? item.value : nil
            }
        }
        
        if parameters.count != 0 {
            self.parameters = parameters
        }
        
        return true
    }
    // …
}

The fulfill(with:) method takes a route as parameter and returns a boolean which indicates whether the request could be fulfilled using the given route or not. We also append the queryItems we extracted during the instantiation to the parameters so users only need to deal with a single dictionary. If you read carefully, you might’ve already discovered that queryItems is an internal property anyway, so this implementation detail will not be exposed to the user of the library. What’s still missing here is the actual route parsing. Let’s recap what it will look like using the following example:

let url = URL(string: "scheme://view/someuser?referrer=otheruser")!
let request = RoutingRequest(url: url)!
_ = request.fulfill(with: "/view/:user")
  • The URL scheme://view/someuser?referrer=otheruser is being routed
  • The route /view/:user should match the URL
  • The request should have two parameters after being fulfilled: user and referrer, with the values someuser and otheruser, respectively.

This means we need to split the given route into its components, separated by a /. Each component can be one of three things:

  • Placeholder: String starting with :
  • Wildcard: String equal to *
  • Path Component: Any hard-coded string

Doesn’t this practically scream Enumeration?

RouteComponent

enum RouteComponent {
    case path(String)
    case placeholder(String)
    case wildcard
}

The RouteComponent type can not be exposed to Objective-C, but that’s okay because we’re only going to use it internally. The names of placeholder and path components will be stored using associated values. In terms of parsing, we only allow wildcards at the end of a route and when we encounter a placeholder, we need to strip off the leading : before saving the name.

enum RouteComponent {
    // …
    static func components(of route: String) -> [RouteComponent] {
        let pathComponents = route.pathComponents
        var components = [RouteComponent]()
        
        for (index, component) in pathComponents.enumerated() {
            if component == "*", index == pathComponents.count-1 {
                components.append(.wildcard)
                break
            }
            
            if component.characters.first == ":" {
                let parameterName = String(component.characters.dropFirst())
                components.append(.placeholder(parameterName))
                continue
            }
            
            components.append(.path(component))
        }
        
        return components
    }

}

Now that we have our route parsing in place, we need to actually make use of it! To avoid unnecessary work, we should do this as soon as possible. Let’s refactor and parse our routes when instantiating our RouteHandler objects:

open class RouteHandler: NSObject {
    // …
    let routeComponents: [RouteComponent]
  
    public required init(route: String, scheme: String?, priority: Int, action: @escaping RouteHandlerAction) {
        // …
        self.routeComponents = RouteComponent.components(of: route)
        super.init()
    }
    // …
}

Back to the RoutingRequest. Fulfilling a request looks really straightforward when using our RouteComponent type instead of a plain string:

public final class RoutingRequest: NSObject, NSCopying {
    // …
    func fulfill(with routeComponents: [RouteComponent]) -> Bool {
        var parameters = [String: String?]()
        var remainingRouteComponents = routeComponents
        
        pathLoop: for (index, component) in self.pathComponents.enumerated() {
            guard routeComponents.count >= index+1 else { return false }
            let routeComponent = routeComponents[index]
            remainingRouteComponents.remove(at: 0)
            
            switch routeComponent {
            case .path(let name):
                guard name == component else { return false }
            case .placeholder(let name):
                parameters[name] = component
            case .wildcard:
                let remainingPath = self.pathComponents[index...self.pathComponents.endIndex-1].joined(separator: "/")
                self.wildcardComponents = URLComponents(string: remainingPath)
                break pathLoop
            }
        }
        
        if remainingRouteComponents.count > 0 {
            guard remainingRouteComponents.count == 1, case .wildcard = remainingRouteComponents[0] else { return false }
        }
        // …
    }
    // …
}

We iterate through all path components of our URL. First, we need to make sure a route component exists at the index of our current path component. If there are more path components than route components, we fail (wildcards are an exception, we’ll get there in a second).

Using the remainingRouteComponents array we keep track of the route components we didn’t match yet. During each iteration we remove the first remaining route component.

A path component is the easiest to handle. We simply check if the name of the route component is equal to the path component. If it’s not, we fail immediately.

placeholder components don’t require any matching, we only need to save the path component as a parameter with the name of the placeholder as the key.

Matching a wildcard is probably the trickiest case. First of all, we get the remaining path components using a range starting with our current index and ending with the endIndex of our pathComponents array. To create a string from this subset of components — an array of strings — we join them with / as separator. We assign a new URLComponents object to the wildcardComponents property to make it easier for the user to access e.g. the path of the matched wildcard. Lastly we make use of a somewhat lesser known feature in Swift: Labeled Statements. After matching a wildcard, we don’t need to — and can’t, as line 8 would make the fulfillment fail immediately— continue the loop. But if we just called break, this would only affect the switch statement and not our for loop. Thus we label the for loop as pathLoop and tell Swift to break it using break pathLoop.

Hold on, the matching isn’t done yet! We’re not handling the case of having more route components than path components. That’s where the remainingRouteComponents array comes into play. If there are any unmatched components, that’s generally a bad sign, with the exception of a single leftover wildcard. A wildcard indicates that the remainder doesn’t matter. In this specific case, the remainder is empty, which is just fine.

Making Requests

The first implementation of RouteHandler used URL objects because the RoutingRequest type didn’t exist yet. Time to refactor!

public typealias RouteHandlerAction = (RoutingRequest) -> Bool
// …
open class RouteHandler: NSObject {
    // …
    public func handle(request: RoutingRequest) -> Bool {
        if let scheme = self.scheme {
            guard scheme == request.scheme else { return false }
        }
        let request = request.copy() as! RoutingRequest
        guard request.fulfill(with: self.routeComponents) else { return false }
        
        return self.action(request)
    }
  
}

After matching the scheme, we make a copy of the request object. We need to do this because calling fulfill(with:) on a request object can mutate its state, but the request passed to handle(request:) by the Router could be reused with another handler. This way, we don’t mutate the request object used by the Router and it can be reused safely. In order for this to work, RoutingRequest needs to implement NSCopying. We could avoid this by using value types, but they would remove the Objective-C compatibility.

If the request is fulfilled we pass it to our action, otherwise we fail.

The Router class will be the next target of our refactoring. First, we extract the actual routing into its own method. This doesn’t only make the code cleaner, but it will also allow users of the library to route URLs manually without making a round trip to the system. We also add two convenience methods which can be called with just a raw URL.

public class Router: NSObject {
    // …
    @discardableResult
    public func route(request: RoutingRequest) -> Bool {
        let handlers = self.handlers.sorted {
            return ($0.1.priority > $1.1.priority)
        }
        for (_, handler) in handlers {
            guard handler.handle(request: request) else { continue }
            return true
        }
        
        return false
    }
  
    @discardableResult
    public func route(urlString: String) -> Bool {
        guard let request = RoutingRequest(string: urlString) else { return false }
        return self.route(request: request)
    }
  
    @discardableResult
    public func route(url: URL) -> Bool {
        guard let request = RoutingRequest(url: url) else { return false }
        return self.route(request: request)
    }
    // …
}

The handleEvent(_:with:) methods also looks much cleaner after adopting the convenience method:

public class Router: NSObject {
    // …
    @objc
    private func handleEvent(_ event: NSAppleEventDescriptor, with replyEvent: NSAppleEventDescriptor) {
        guard let urlString = event.paramDescriptor(forKeyword: AEKeyword(keyDirectObject))?.stringValue else { return }
        self.route(urlString: urlString)
    }
    // …
}

Looks good! We’re almost done, but…

Remember how I mentioned earlier that there can only be one handler per class/identifier combination?

We still can’t use multiple router instances at the same time! The easiest solution would be to make the initializer of Router private, so no one, except the class itself, can create instances, which would force everyone to use the shared instance global. It’s not my intention to start the whole singleton-debate now, but I believe there’s a better solution than this.

Due to this restriction of having only one handler for URL events, we will eventually need to introduce a singleton. However, we can refactor the event handling into a separate class and have a singleton there instead of in the Router class.

URLEventHandler

The URLEventHandler class will, as the name suggests, handle URL events. We’re going to apply the solution mentioned above here and make the initializer private, so the only object that can ever be instantiated is the global singleton.

public final class URLEventHandler: NSObject {
    
    public static let global = URLEventHandler()
    
    private override init() {
        super.init()
        NSAppleEventManager.shared().setEventHandler(self, andSelector: #selector(handleEvent(_:with:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL))
    }
    
    deinit {
        NSAppleEventManager.shared().removeEventHandler(forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL))
    }
  
}

Objects which the URLEventHandler class notifies about URL events will be called listeners. Those listeners will need to adapt the URLEventListener protocol.

public protocol URLEventListener: class {
    func handleURL(_ url: String)
}

Types that implement the URLEventListener protocol will need to be classes because we want to keep weak references to our listeners. Storing weak references inside an array isn’t supported out of the box, so we need a small helper:

private struct WeakObject<T: AnyObject> {
    private(set) weak var object: T?
    
    init(object: T) {
        self.object = object
    }
}

Instead of storing our listeners directly, we wrap them inside a WeakObject instance and store an array of those in our URLEventHandler class:

public final class URLEventHandler: NSObject {
    // …
    private var listeners = [WeakObject<URLEventListener>]()
  
    public func addListener(_ listener: URLEventListener) {
        self.listeners.append(WeakObject(object: listener))
    }
    
    @discardableResult
    public func removeListener(_ listener: URLEventListener) -> Bool {
        guard let index = self.listeners.index(where: { $0.object === listener }) else { return false }
        self.listeners.remove(at: index)
        
        return true
    }
  
}

Handling URL events pretty much looks like it did originally in the Router class, except we now need to notify each listener.

public final class URLEventHandler: NSObject {
    // …
    @objc
    private func handleEvent(_ event: NSAppleEventDescriptor, with replyEvent: NSAppleEventDescriptor) {
        guard let urlString = event.paramDescriptor(forKeyword: AEKeyword(keyDirectObject))?.stringValue else { return }
        for listener in self.listeners {
            listener.object?.handleURL(urlString)
        }
    }
    // …
}

Speaking of the Router class, it still needs to adopt our new URL event handling.

public class Router: NSObject, URLEventListener {
    // …
    public override init() {
        super.init()
        URLEventHandler.global.addListener(self)
    }
    
    deinit {
        URLEventHandler.global.removeListener(self)
    }
  
    public func handleURL(_ url: String) {
        self.route(urlString: url)
    }
    // …
}

Now we can use multiple Router instances at once without them interfering with each other.

About Testing

Screenshot of test coverage in Xcode

This is a small library and 33 tests were enough to achieve this test coverage. There are a couple of edges cases which aren’t being covered by the tests as of the time of writing.

Testing is boring and there’s nothing spectacular here either…except for this one thing: how do you test NSAppleEventManager?

I wouldn’t know how to create a mock application that receives URL events by the system (possibly using LSSetDefaultHandlerForURLScheme?) and I’m afraid the overhead wouldn’t be feasible. As it turns out, we don’t need to worry about that because we can send events to NSAppleEventManager manually!

I encountered this charming method while looking at the documentation:

func dispatchRawAppleEvent(UnsafePointer<AppleEvent>, withRawReply: UnsafeMutablePointer<AppleEvent>, handlerRefCon: SRefCon)

The mysterious AppleEvent type is part of a typealias jungle that ultimately leads to AEDesc (AppleEventAERecordAEDescListAEDesc). dispatchRawAppleEvent(_:withRawReply:) needs pointers to AEDesc objects, which are C structs. To create these structs we need to use even more C pointers and deal with other fun methods like AEPutParamDesc.

Luckily NSAppleEventDescriptor provides a property, aeDesc, that returns a pointer to an AEDesc object. This means we only need to deal with NSAppleEventDescriptor and it will do the heavy lifting to create its C counterparts.

var url = …
let urlDescriptor = NSAppleEventDescriptor(applicationURL: url)
let event = NSAppleEventDescriptor(eventClass: AEEventClass(kInternetEventClass), eventID: AEEventID(kAEGetURL), targetDescriptor: nil, returnID: AEReturnID(kAutoGenerateReturnID), transactionID: AETransactionID(kAnyTransactionID))
event.setParam(urlDescriptor, forKeyword: keyDirectObject)
NSAppleEventManager.shared().dispatchRawAppleEvent(event.aeDesc!, withRawReply: UnsafeMutablePointer(mutating: NSAppleEventDescriptor.null().aeDesc!), handlerRefCon: &url)

Now we can test that NSAppleEventManager and URLEventHandler work together 🎉

Final Words

I left out some small details which I didn’t find noteworthy (e.g. how the Router deals with handler unregistration). The full source code is available on GitHub.

This was my first time writing about a technical topic, I appreciate all feedback whether it’s about my style of writing or my code.

About the Author

Florian Schliep is a software engineer & entrepreneur based in Berlin, Germany. He is available for consulting in mobile engineering strategy, hiring and due diligence.

© 2024 Florian Schliep. All rights reserved. — Imprint