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
(
AppleEvent
AERecord
AEDescList
AEDesc
).
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