How to Access the AppKit API from Mac Catalyst Apps
By: Mhd Hejazi
Tags: Programming, Swift, Xcode, iOS, Macos

B. Using a Plugin

Since Mac Catalyst apps are also macOS apps, they can embed macOS plugins that use AppKit API and load them at runtime. Let’s build a very simple plugin that shows a message using AppKit’s NSAlert .

1. Add a macOS Bundle target

Let’s name it MacPlugin.

2. Embed the plugin in the app target, and change “Platforms” to macOS

Since the bundle is manually loaded at runtime, we can’t reference its classes directly. Instead, we will make those classes conform to shared protocols that define the contract between our app and the plugin. Later on, when we load a class from the plugin, we will cast it to the shared protocol.

3. Create a shared protocol “Plugin”

Make sure it’s included in both targets.

Bundles can only load Objective-C classes or Swift classes that inherit from NSObject . Shared protocols are usually written in Objective-C, but we can also use Swift protocols as long as they inherit from NSObjectProtocol and only use Swift features that are compatible with Objective-C runtime (For instance, features like structs, generics, and enum associated values aren’t compatible with Objective-C runtime and hence are not supported).

When you add Swift code to the plugin target, Xcode will suggest configuring an Objective-C bridging header. That’s wise advice indeed — take it!

Let’s write a simple protocol with one method that shows an alert from AppKit land.

Notice that we annotated the protocol with @objc(Plugin) . This is very important because it ensures that the name of the protocol will stay the same in the eyes of Objective-C runtime in both targets.

Without the annotation, the runtime will see two different protocols with different names since the module name is part of the default name:

• App.Plugin — the protocol included in our app

• MacPlugin.Plugin — the protocol included in the plugin

And that means we can’t cast the plugin class that conforms to MacPlugin.Plugin to App.Plugin .

Also, notice that we added an initializer to the shared protocol. This is necessary to make it possible to create instances of the plugin classes after loading them into memory.

4. Write the class that conforms to the shared protocol

You only need to include it in the plugin target.

The implementation is straightforward. We’re simply showing an alert using the AppKit’s NSAlert class.

5. Load the plugin bundle and create a new instance of MacPlugin

We’re creating a new instance of Bundle with the URL of the plugin. The plugin bundle is expected to be at the built-in plugins directory inside the main bundle because that’s where Xcode is copying it.

We’re then calling bundle.classNamed() , which loads the specified class into memory and returns the class type. If the specified class can’t be found, the method returns nil . Notice that the class name must include the module name.

Note 1: If you only have one public class in your plugin, you can make it the principal class of the plugin by setting the “Principal class” property ( NSPrincipalClass ) in the Info.plist file to the full class name.

Then you can access that class by calling Bundle.principalClass instead of searching for the class.

Note 2: We can explicitly load the plugin by calling bundle.load() , but that’s not really necessary as the plugin is automatically loaded when we try to load any class from it.

After loading the class, we’re creating a new instance using the initializer we defined in the shared protocol and then calling sayHello() . And if everything went well, you’ll see something like this:

And that’s it!