Mac Catalyst, native AppKit, SwiftUI for macOS, or an iPad app on the Mac: how do you choose, and what changes in your code?

20.3.1 · L3 · Interview

Predict first

UIKit code builds for Mac Catalyst and detects it at compile time:

import UIKit

#if targetEnvironment(macCatalyst)
let platform = "catalyst"
#else
let platform = "ios"
#endif
let w = UIWindow()
  • It compiles for Catalyst
  • It fails: UIKit is unavailable on the Mac

Predict first

AppKit’s NSWindow in the Catalyst build:

import AppKit

let w = NSWindow()
  • It compiles
  • It is an error: NSWindow is unavailable in Mac Catalyst

Predict first

A macOS-only file API on iOS:

import Foundation

print(FileManager.default.homeDirectoryForCurrentUser.path)
  • It compiles on every platform
  • It is an error on iOS: homeDirectoryForCurrentUser is unavailable

Mental model. The four routes are like opening a branch in a new city: shipping the existing shop as is (iPad on Mac), re-fitting it with local signage (Catalyst), building a new shop from the same blueprints (SwiftUI) or designing a local flagship (AppKit).

Four routes exist. iPad app on Apple silicon Macs: an unmodified iOS or iPadOS app can run on the Mac (“Designed for iPad”), with no new target, using the iPad layout in a resizable window, and ProcessInfo.isiOSAppOnMac tells you at run time. It is the cheapest route and the least Mac-like: no menu-bar customisation beyond the basics, no Mac-only APIs, and developer control of availability in the Mac App Store. Mac Catalyst adds a Mac target to your UIKit project (a checkbox) that builds the iOS code against the Mac: the build is arm64-apple-ios-macabi, UIKit exists and AppKit does not (NSWindow is “unavailable in Mac Catalyst” in the Catalyst slice), and you adapt it with #if targetEnvironment(macCatalyst), the Optimize interface for Mac mode (which switches controls to Mac idioms and scales the UI), menu-bar items through UIMenuBuilder, toolbars, a pointer and keyboard support, multiple windows and a Mac-specific entitlement set. It shares most code with an existing iPad app and is the usual route for a UIKit codebase. SwiftUI multiplatform: one SwiftUI codebase compiles for iOS, iPadOS and macOS natively (not via Catalyst), with Settings, MenuBarExtra, commands and Window scenes for the Mac conventions, and AppKit where SwiftUI is not enough through NSViewRepresentable. Native AppKit gives full control (document windows, text system, inspectors, drag and drop, services, AppleScript) and is needed for pro tools or deep system integration, at the cost of a separate UI codebase.

How to choose: if you already have a UIKit app and want a good-enough Mac app quickly, Catalyst; if you are building new and want the most sharing, SwiftUI multiplatform with AppKit escapes; if the product is a Mac-first professional tool, AppKit (possibly with SwiftUI inside); to test demand cheaply, the iPad-on-Mac route. Code differences to expect: AppKit uses NSView, NSWindow, NSViewController and a responder chain with first-responder menu actions, flipped or unflipped coordinates (AppKit’s origin is bottom-left by default, UIKit’s is top-left), NSImage instead of UIImage and NSColor instead of UIColor; UIApplication becomes NSApplication; and some iOS-only APIs are absent (FileManager.homeDirectoryForCurrentUser is macOS-only: it is a compile error on iOS, while NSHomeDirectory() returns a user path on the Mac). Use typealias shims (#if canImport(UIKit) chooses UIColor or NSColor) in the shared core, and keep the platform views in the leaf. Be careful that Catalyst and the iPad-on-Mac route run an iOS process model on the Mac (no sandbox escape, system frameworks differ), so hardware-specific features such as the camera or motion may be absent.

Catalyst builds the UIKit code for the Mac, and the compile-time condition tells the two builds apart.

The Catalyst slice has UIKit, not AppKit, so Mac-only AppKit types are unavailable.

Some Foundation conveniences exist only on the Mac, where a user home directory is meaningful.

Common traps

  • Importing AppKit in code that also builds for Catalyst or iOS. It is unavailable in the Catalyst slice; guard with canImport or keep it in a Mac-only target.
  • Assuming an iPad layout is a Mac UI. Without menus, keyboard shortcuts, resizing and pointer behaviour it feels like a port; plan Mac idioms.

They might follow up with

  • How do you detect Catalyst at compile time? #if targetEnvironment(macCatalyst).
  • Which route shares the most code with an existing UIKit app? Mac Catalyst.

Related questions

  • 20.3.2 What does a Mac app need that an iOS app does not: the menu bar, windows, the App Sandbox, Hardened Runtime…
  • 20.2.1 How do document-based apps and multiple scenes work on iPad, and how should state be organised?
  • 20.1.2 How do the platforms differ in input, process model and capabilities, and which entitlements and APIs change…

What does a Mac app need that an iOS app does not: the menu bar… →