Beyond the Checkbox with Catalyst and AppKit

June 07 2019

WWDC 2019 is here, and with it, a new way to write apps on the Mac: Catalyst. Catalyst is the developer-facing name for the technologies — formerly known as Marzipan — bringing iPad apps to the Mac by introducing UIKit and dozens more frameworks from iOS to the platform and unifying the substrate layer between iOS and macOS.

Catalyst apps are built from the iOS 13 SDK, with relatively few changes to the API set; simply click the 'Mac' checkbox for your project in Xcode to get started.

Click the checkbox

I previously documented just how far one could go with Mojave's version of UIKit if you jumped through enough hoops, with means to integrate AppleScript and Services. There are many reasons why it will benefit your Catalyst-based Mac app to deeply integrate with system features.

So now that Catalyst is here and the SDK in our hands, how do we go about integrating with the Mac and AppKit to build the best possible Mac app with UIKit? Let's take a look…

Not available on UIKit for macOS

Catalyst apps are granted access to NSToolbar (part of AppKit) in the SDK, so that apps can create rich window toolbars, and NSTouchBar to integrate with the Touch Bar, but what if you wanted to spawn an AppKit window and AppKit view hierachy to augment your app? Try to use any other AppKit class in your Catalyst app, and you'll rapidly run up against API_UNAVAILABLE_BEGIN(ios).

'NSWindow' is unavailable: not available on UIKit for macOS

Most AppKit classes are marked as unavailable to a UIKit app; at first glance, this seems pretty clear cut: the iOS SDK doesn't let you touch AppKit in your app.

However, the new unified process model in macOS Catalina means that your UIKit app is also an AppKit app. So how, then, could we use it?

Loading an AppKit bundle

Since Apple's making a point to treat Catalyst apps as 'true Mac apps', that means that your app can do what any other Mac app can do: embed an AppKit-based bundle or framework, and load it at runtime as a plugin.

Simply add a new macOS Bundle target to your project.

Add Target: macOS Bundle


Embed your new macOS bundle in your app and make its inclusion conditional to macOS only.

Embed bundle

It's as simple as that! Now you have a loadable bundle that is built with the Mac SDK, and thus has access to all of AppKit. All you have to do is load it manually when you detect you're running on macOS.

NSString *pluginPath = [[[NSBundle mainBundle] builtInPlugInsPath] stringByAppendingPathComponent:@"AppKitGlue.bundle"];
[[NSBundle bundleWithPath:pluginPath] load];

Create a principal class for your bundle (remember to add it to your Info.plist too), then treat it like your entrypoint into AppKit land. Instantiate it from your UIKit code and you're good to go!


Using AppKit

With the unified process model in macOS Catalina, your UIKit app will have a [NSApplication sharedInstance] as you might expect in AppKit, and it lists your app's windows just like it would for any NSWindow in an AppKit app. This gives you a bridge to modify your app window in ways that otherwise Catalyst makes impossible, like changing the window style mask, setting its position & frame onscreen, and setting a minimum/maximum window size.

You are free to spawn new windows as you like, and load them up with AppKit NIBs or storyboards or whatever suits you best.

As everything is running in the same app, you're free to call selectors between UIKit and AppKit, but it might be best to use an intermediary class to pass messages in between the two, since you won't be able to import UIKit from AppKit code.

One thing you can't do is mix UIKit and AppKit layers in the same view hierarchy. It may be technically possible, but it's the kind of thing I would expect to break at any moment.

Another thing you cannot quite do is spawn a new NSWindow with a UIKit view hierarchy. However, your UIKit code has the ability to spawn a new window scene, and your AppKit code has the ability to take the resulting NSWindow it's presented in and hijack it to do whatever you want with it, so in that sense you could spawn UIKit windows for auxiliary palettes and all kinds of other features. This is clearly not an intended use of Catalyst, but we're here to push the boundaries and make the best software we can — like we expect from our fellow Mac developers!


So what else does using AppKit in your UIKit app enable you to do…? Let's go through Martin Pilkington's fantastic list of things to appreciate in AppKit and see where your Catalyst app can improve.


Auxiliary Panels & Sheets

You can spawn NSPanel instances as floating palettes for the controls in your app, like a tool palette. You can also present complex AppKit windows as sheets from your UIKit window.

If you want to be a little more adventurous, you can listen for new window spawns and use that to hijack UIKit-spawned windows for your own purposes — perhaps to make them a non-activating floating panel, or to present them as a window sheet.

Proxy Icons

Set representedURL or representedFilename on your primary window to add a proxy icon to your currently open document. This will add an icon to your window titlebar to represent the document you point to, and a user can drag and drop from this proxy icon to another app or somewhere in Finder. You can also command-click the proxy icon to show the document's full path.

Color Pickers

You now have a bridge to use NSColorPanel in your UIKit app, so that you get a rich, native color picker and use it to drive your UIKit UI.

The color picker will also load third-party color picker plugins, of which many great examples exist on macOS.

Cursor

You can change the mouse cursor with NSCursor, as you might expect, to any of the system cursors, or a custom image. Many Mac apps change the cursor based on the tool or content you're interacting with, and now you have a way to do this in your Catalyst app.

This also gives you a way to show/hide the mouse cursor if you are a game, or capture it if you wish.

Dock

You can update the Dock icon for your app at runtime with custom or generated icons (like a progress bar for a long export operation), or add a badge with a string. You can also customize your Dock menu with options at runtime.

Menu Bar Status Items

You can implement NSStatusItem from your AppKit bundle to provide a menu bar icon, like you may be used to with apps like Dropbox. A status item can has a dropdown menu, but many apps spawn a borderless window to simulate a mini-app UI invoked by a status item.

Theoretically, you might be able to present your UIKit app window from a status item in a little iPhone-sized region if it's not quite appropriate to be used as a full Mac or iPad app.

NSWorkspace

You can use NSWorkspace to move items to the trash, reveal a file in Finder, or load the icon for a file from disk, among many other things.

NSTask & system()

NSTask allows you to run shell commands or command-line apps, and pipe things between processes. With an AppKit bundle, your UIKit app can support these interactions too.

AppleScript

AppleScript & Services are now easier than ever to use. You can follow the previous instructions under the AppleScript section of my earlier post to add Apple Events support to your app, as only one tiny thing has changed since Mojave's implementation.

You no longer need the private aeInstallRunLoopDispatcher(); call in -setupAppleEvents, as Apple Events are already set up as you might expect for AppKit.

Other than that, you should be good to go; you can decide how best to relay Apple Events to your UIKit code, and what actions are appropriate for your app.

Services

Services let you vend actions to the system for various types of content, like files, images, text, etc. The Services menu shows up in the context menu, and if your app offers a service for the currently selected type of content, its service will show in the menu.

Simply follow Apple's legacy tutorial to implement Services in your AppKit bundle. Everything just works 😃.


As you can see, a bridge to AppKit like this can make a world of a difference to how Mac-like your app feels to its users.

There are many ways a hybrid UIKit/AppKit app can give you the best of both worlds: compatibility with your existing iOS codebase as well as being able to do as much as possible to give your Mac users the things they expect from any good Mac app.

Apple is starting to use little bits of Catalyst in its own Mac apps, like the fullscreen iMessage Effects in macOS Catalina, and I'm sure that will only accelerate in the future, even with something as exciting and new as SwiftUI in the cards: Apple has a lot of things that are built on UIKit, as do third-party developers, and the Mac deserves to be able to use all of it.

If UIKit is the way you need or want to build things for Mac, the future is very bright for Catalyst apps.