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.
I will share my method of using both
AppKit in a single Catalyst application to provide the native experience. All of this works in both macOS Catalina and macOS Big Sur. A word of caution: you’re just getting started with a new macOS project, my advice is to go with either
SwiftUI. They offer the native experience out of the box and are likely to be better documented. If your goal is to create a complex application that utilizes lots of native macOS features, definitely go with
AppKit. The method I’m presenting here is primarily for developers who either a) have an existing Catalyst app, b) an iOS app they want to port to the Mac, or c) who have large iOS codebases.
In this article, I will discuss:
- how Catalyst interfaces between
- how we can call
AppKitAPIs from our Catalyst (
- how we can add native Mac UI elements (and more) to the
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.
NSWindow when using Mac Catalyst and allow you to present your scenes as familiar Mac windows, Apple created a different class — called
UINSWindow — that is used for Catalyst only. This is a private class, so you cannot use it directly — and even if you try, your app will get rejected by the App Store Review.
UINSWindowinstance conveniently inherits from
NSWindow, so it supports all the functionality that a standard
AppKitwindow supports. This suggests that there is a possibility to somehow mix the
AppKitlayers together in one view hierarchy.
And indeed, that’s possible. For this, we can use the following snippet:
Note several things here. Obviously, we can’t access any internal properties directly, so we use key paths to access the
NSApplication class. We know it exists, since it exists for every GUI-based Mac app, no matter what technology you’re using. One more thing: we return
AnyObject without casting, since
UIKit doesn’t know either
UINSWindow exist. If you print out the value, you will see that it’s an instance of
Next, we need a way to find the current window in
UIKit. Here’s a foolproof way of doing that in iOS 13+. Note that I placed the computed property as a
UIApplication extension, but you can place it anywhere you want:
To test this part out, navigate to any of your view controllers and add the following to your
When you run the target, your output should be something like this:
<UIWindow: 0x102a299c0; frame = (0 0; 1026 797); gestureRecognizers = <NSArray: 0x600000cbeca0>; layer = <UIWindowLayer: 0x60000027dfc0>><UINSWindow: 0x102b0d1e0>
This is the “secret connection” between
AppKit that we will need to make the rest of this work!
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.
Luckily, we can add a new Mac target to our project to overcome this. We will then load it from our app and it will serve as the “link” between
Step 1. Go to File > New >Target…
Step 2. Make sure “macOS” is selected, and choose “Bundle” (you can search for it, too). Enter the details and hit create. Name it what you want (I chose “
AKTest” for this).
Step 3. Now create a new Swift file inside the newly created bundle. The name again doesn’t matter, I chose “
Step 4. Inside the file, declare a class and inherit from
NSObject. Then add the following:
Now let’s do some setup to make sure we can load the bundle from our main app.
Step 5. In the bundle’s
Info.plist, locate the “Principal class” property. Set its value to be something like
$(PRODUCT_BUNDLE_IDENTIFIER).AKTest , where instead of
AKTest you should use the name of the class you’ve just created.
Step 6. Now go to the project settings and drag the bundle (it should be in the Products group) to Frameworks and Libraries section of the General tab of your main target. Finally, choose the platform to be “macOS.”
Step 7. Now let’s make sure our bundle can be loaded. Navigate back to the
viewDidAppear(_:) method, where we referenced our
UINSWindow. Together with the existing code, your method should look like this:
In the first line, replace
AKTest.bundle with your bundle’s name. Note that the interfacing object (
akInterface) is stored as an instance variable. This is done in order to prevent it from being deallocated once control reaches the end of the method. We will need to keep this object for later.
When you build and run, your window will go to full screen mode automatically. This was done by calling an
AppKit API from our bundle!
Adding Native UI
Now let’s use the fact that we can interface between
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.
In the demo app, I have a
UILabel. Note that you will need to have the label created in your hierarchy — I am not covering that basic setup here. If you are having trouble following these steps, consider cloning/downloading the demo project that is provided at the end.
Let’s pretend I want to insert an
NSSlider (available only in
AppKit) into my
UIKit view hierarchy, and then update the text of that
UILabel whenever the value changes. This use case should cover the majority of potential use cases for interfacing
I will add some new properties and methods into my
AKTest class located inside the
AppKit bundle, like this:
I have decided to follow the familiar target-action pattern — it is common for event handling in both
AppKit. First, I will set the target using the
setTarget(_:) method to be the object that is going to handle the callbacks. Then, I will set up the slider and pass the identifier of the selector that I want to handle the callbacks from the slider. Next, I am going to set up the basic event handling and forward the call whenever the value of the slider changes — this happens inside
Inside our view controller on the
UIKit side of things, I am going to add two calls to the
AppKit bundle interface: one to set the target, and one to set up the slider:
akInterface.perform(Selector("setTarget:"), with: self)akInterface.perform(Selector("setupSlider:"), with: "uiSliderValueChanged:")
And, of course, I need to create the method that is going to handle the change of the value on the slider. (It should be located in the same class.)
If I build and run the app, I can see that my
UIKit label updates whenever I change the value on the
AppKit slider. We have successfully used
AppKit in one view hierarchy — even more than that, we’ve been able to handle events too!
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:
If you do have arguments, like the
setWindow(_:) method, do this:
object.perform(Selector("setWindow:"), with: nsWindow)
Now you can go ahead and create new methods in the
AppKit bundle. Make sure any new methods you add have
@objc in front of them — otherwise, you’ll get a runtime error.
Finally, here’s the link to the demo project.
I just started this blog, so if you like this, consider giving me a follow. I’m an iOS dev and will post stuff related to it, as well as other (fun) technical things.