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.
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:
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:
Notice that we receive events as
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
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
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 will receive events, extract URLs from them and take appropriate actions depending on the content of the URLs.
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
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
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
route to be a pattern of a path. We need patterns for two things: placeholders and wildcards.
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
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:
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.
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
Router class will act as a storage and manager for
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:
A bit cumbersome, right? We’re not even using default parameters! Let’s solve this by adding two convenience methods to our
With this in place, registration will be much cleaner:
The first example, just like the one above, will match any route at its root:
def://… With the second example we can even match two routes for a specific scheme:
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:
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 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.
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.
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.
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:
- The URL
scheme://view/someuser?referrer=otheruseris being routed
- The route
/view/:usershould match the URL
- The request should have two parameters after being fulfilled:
referrer, with the values
This means we need to split the given route into its components, separated by a
/. Each component can be one of three things:
- Placeh0lder: String starting with
- Wildcard: String equal to
- Path Component: Any hard-coded string
Doesn’t this practically scream Enumeration?
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.
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
Back to the
RoutingRequest. Fulfilling a request looks really straightforward when using our
RouteComponent type instead of a plain string:
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).
remainingRouteComponents array we keep track of the route components we didn’t match yet. During each iteration we remove the first remaining route component.
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.
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
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 indicates that the remainder doesn’t matter. In this specific case, the remainder is empty, which is just fine.
The first implementation of
URL objects because the
RoutingRequest type didn’t exist yet. Time to refactor!
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.
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.
handleEvent(_:with:) methods also looks much cleaner after adopting the convenience method:
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
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
Objects which the
URLEventHandler class notifies about URL events will be called listeners. Those listeners will need to adapt the
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:
Instead of storing our listeners directly, we wrap them inside a
WeakObject instance and store an array of those in our
Handling URL events pretty much looks like it did originally in the
Router class, except we now need to notify each listener.
Speaking of the
Router class, it still needs to adopt our new URL event handling.
Now we can use multiple
Router instances at once without them interfering with each other.
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
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
I encountered this charming method while looking at the documentation:
AppleEvent type is part of a typealias jungle that ultimately leads to
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
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.
Now we can test that
URLEventHandler work together 🎉
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.
I created this library while working on a new feature for my app Short Menu. Check it out if you still have a moment or two after reading all of this, it’d mean a lot to me 💜.