Mac Catalyst: Interfacing Between UIKit and AppKit without Private APIs in Swift

Mac Catalyst is a great opportunity for iOS developers to bring their existing offerings to the Mac with minimal modification. However, once you dig deeper into what you can and can’t do, you’ll realize that you’re restricted to a small subset of functionality that Mac has to offer.

Image for post
Image for post
  • how we can call AppKit APIs from our Catalyst (UIKit) app;
  • how we can add native Mac UI elements (and more) to the UIKit view hierarchy.

UIWindow + NSWindow = UINSWindow

If you’ve done some iOS development, you’re familiar with UIWindow. On iOS, these generally do not represent anything UI-related and are used to separate different view hierarchies. NSWindow is a similar concept that has existed on the Mac since the beginning. Windows are native to the Mac and are a much more natural abstraction there, compared to iOS.

<UIWindow: 0x102a299c0; frame = (0 0; 1026 797); gestureRecognizers = <NSArray: 0x600000cbeca0>; layer = <UIWindowLayer: 0x60000027dfc0>><UINSWindow: 0x102b0d1e0>

Creating an AppKit Bundle

Part of the problem is that we can’t import AppKit from a Catalyst backed target, so we’ll have to get somewhat creative.

Image for post
Image for post
What you should see after completing Step 6

Adding Native UI

Now let’s use the fact that we can interface between UIKit and AppKit to do something more interesting. I have included the contentView computed property to the sample class I provided above. It will represent the base NSView of your window, and you can add the controls to it directly.

akInterface.perform(Selector("setTarget:"), with: self)akInterface.perform(Selector("setupSlider:"), with: "uiSliderValueChanged:")
Image for post
Image for post

Next Steps

Now that we have a common ground between the two frameworks, we’re free to use the best of both! I’m going to assume that you would like to add your own functionality and new methods — after all, as exciting as adding a slider sounds, I can think of many more possible applications. Here’s the general calling convention and workflow. Whenever you want to call a method in the bundle, you have to perform the selector instead of calling the method itself. If you have a method that takes no arguments, like toggleFullScreen, use the following:

object.perform(Selector("toggleFullScreen"))
object.perform(Selector("setWindow:"), with: nsWindow)

Written by

iOS Engineer, Entrepreneur, CS Student at Georgia Tech

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store