How to port your iPad app to Mac using Catalyst

Sergey ZenchenkoApril 12, 2023


Let me tell you; I was super excited when I saw the first Marzipan presentation, and who wasn’t? We could finally make Mac apps without rewriting the entire UI.

And it was a natural, logical next step since Apple already had UIKit working on Mac inside the iOS simulators.

So today, we’re going to check out the Marzipan. I mean Catalyst. Funny, it's still called ‘Marzipan’ in Apple's code.

Thanks for @steipete for the screenshot

Initially, I was going to port our AppSpector SDK to Catalyst. It would be great to allow Catalyst developers to debug remotely and monitor their desktop apps like AppSpector lets mobile developers do with their iOS and Android apps.

What we need is a helpful app for SDK testing. Some real app. Not just a sample app from Apple.

My pick is an application called Morning Pages.

I was involved in developing this app, and it would be cool to see how it would work on a Mac. This app was already using AppSpector, so it was a perfect candidate. Network requests, CoreData, intensive logging, and all the stuff we needed for our test were already there.

Time to check the checkbox

Let's check the magic checkbox and find out how our app would look on a Mac.

A bunch of libs for analytics like Heap, Google Analytics, Fabric, and Crashlitics distribute as static libs. We need to remove them all until they get updated for Catalyst.

Why can't we link against libraries with a slice for the iOS simulator? It's the same x86. The problem is that Mac Catalyst is a different platform than the iOS Simulator, and linker refuses to use the iOS Simulator slice when linking binary for macOS returning the following message:

libGoogleAnalytics.a(GAITrackerImpl.o), building for UIKitForMac, but linking in object file built for iOS Simulator

I had to remove the AppSpector SDK for the same reason: we have not updated it for Catalyst yet. We will go over the SDK migration in one of the upcoming articles.

The iOS SDK APIs are not available on macOS.

Below are the errors I received concerning the Keychain APIs being unavailable in Mac Catalyst. I had to remove it until it temporarily updated the KeychainAccess pod.

Depending on your codebase, this step could be a piece of cake if you only need a few changes. It could become hell if those APIs are crucial for your application.

A quick note on the compatibility

When reading through Apple documentation, one might notice this availability table on the right side:

That’s just platform availability for the selected API, you might say.

True, but why do we have the ‘Mac Catalyst’ among the other 4 OSs?

It’s not an OS, just a bunch of iOS SDK frameworks ported to macOS. How does it affect developers struggling to build their iOS apps for macOS?

In the pre-Catalyst times, if you decided to have a macOS app alongside the iOS one, one of the most critical choices you would have to make is which part of the codebase to reuse and how to do it.

One approach is to remove all business logic to a different framework and make abstractions for platform-specific APIs. Then, build a separate UI and link the logic framework.

Separation of platform-specific things made it possible. Modifying business logic involves only your ‘core’ part, which is the same across platforms. You should implement UI modifications for each platform. Approach Apple suggests treating the Mac Catalyst (part of the iOS SDK) as a meta-platform.

What it means is:

  • We need one more separation inside our UI code. One part, written with the Mac Catalyst, could be used on all platforms, while platform-specific UI parts should be kept separate and wrapped in some availability macro. NSWindow-related things like Dock, TouchBar, NSStatusItem, and others live here.
  • We should build all non-UI code we would like to reuse for one extra platform because the Mac Catalyst is a separate binary type.

Build succeeded

In the end, I was finally able to build the project. It took me only 10 minutes to make it work:

Everything seems to be working fine, except for a few minor things:

UI and Autolayout

Some of the 3rd party UI components you might use may need to be revised with the Mac Catalyst.

MorningPages onboarding stopped responding to gestures on macOS. Autolayout is working fine, except for runtime warnings on conflicting constraints. You would never have any issues. Your project would always compile without warnings, and you’d have a 'WerrorCompiler.

But sometimes, you must roll up your sleeves and implement a few “dirty hacks” to make your UI work as advertised. Doing that will most certainly bite you back on macOS. UI that rendered perfectly on iOS but triggered Autolayout runtime warnings looked utterly screwed up on my Mac.

Bundle ID

Apps built with the Mac Catalyst can’t have the same bundle ID as their iOS counterparts.

Xcode adds the ‘uikitformac’ prefix to your bundle ID, breaking a lot. I’m talking about InApp purchases, CloudKit, and anything that depends on bundle ID. You can use the ‘DERIVE_UIKITFORMAC_PRODUCT_BUNDLE_IDENTIFIER’ setting and set it to ’NO’ to disable this behavior, but you still need another bundle ID. Good luck syncing your user CloudKit databases.

iOS versions

You are limited to the latest OS versions (iOS 13 and macOS 10.15). If you still want to support users on older OS versions (and I bet you do), you will have availability macro spread across your entire codebase.

Final thoughts

I understand. I am satisfied with Catalyst. We all should know that any new tech introduced by Apple is a Beta and will remain for the next few years.

I suggest you think of Catalyst as a direction. An ecosystem is moving in, and we must prepare our codebases and ourselves as developers to start shipping apps to all Apple platforms simultaneously.

In my next post, I’ll look at adopting AppSpector SDK to be used in Catalyst apps.