What does a Mac Catalyst build compile against, and how do you add Mac menus and keep AppKit out?

20.8.5 · L3 · Interview

Predict first

Is NSWindow usable in the Catalyst slice?

#if canImport(AppKit)
import AppKit
let w = NSWindow()
#endif
  • Yes, AppKit is importable
  • No, unavailable in Mac Catalyst

Predict first

Does the UIMenuBuilder code compile in the Catalyst slice?

import UIKit
#if targetEnvironment(macCatalyst)
func buildMenu(with builder: UIMenuBuilder) {
    builder.remove(menu: .format)
    let item = UIKeyCommand(title: "Export", action: #selector(NSObject.description), input: "e", modifierFlags: .command)
    builder.insertChild(UIMenu(title: "", options: .displayInline, children: [item]), atEndOfMenu: .file)
}
#endif
print("macabi slice compiles UIKit")
  • Yes
  • No

Mental model. Catalyst is an iPad app wearing a Mac suit: the suit has pockets for menus and a toolbar, but it does not have the tailor’s machinery in the lining.

The Catalyst slice is arm64-apple-ios17.0-macabi: iOS source built against the macOS SDK’s iOSSupport frameworks. UIKit is present and the conditions are os(iOS) plus targetEnvironment(macCatalyst) (Q837). AppKit’s module can be imported but its types do not compile: NSWindow() produced “'NSWindow' is unavailable in Mac Catalyst”, and the idiom of a bare process was .pad, not .mac (scaled iPad mode; the Mac idiom needs “Optimize interface for Mac”). The check shows the supported way to reach the Mac menu bar from UIKit: override buildMenu(with:) and use UIMenuBuilder to remove system menus (.format) and insert a UIMenu with UIKeyCommands at .file.

Guard the Mac-only code with #if targetEnvironment(macCatalyst), not with canImport(AppKit), which is true there. Features you add for the Mac build: a toolbar (UINavigationItem.style = .editor), pointer and keyboard support, a minimum window size on the scene’s sizeRestrictions, and entitlements (sandbox, outgoing network) set per target. When you need real AppKit (a status item, an NSOpenPanel subclass), the supported route is a separate AppKit bundle plug-in loaded by the Catalyst app; the plug-in is a macOS target and talks to the app through a protocol both sides compile. Not run: the app in a Mac window (menu rendering needs a running scene) and the Mac idiom; the checks here are compile-time.

The module imports but its types are marked unavailable.

UIKit’s menu builder API is how Catalyst reaches the menu bar.

Common traps

  • Adding import AppKit under canImport(AppKit) in shared code. Catalyst compiles the import and then fails on the first type.
  • Using the phone layout in a resizable Mac window. Set size restrictions and support narrow and wide widths.

They might follow up with

  • Where do menus live in SwiftUI on Catalyst? .commands map to the Mac menu bar.
  • How do I reach AppKit from Catalyst? A separate macOS bundle loaded at run time behind a protocol.

Related questions

  • 20.3.1 Mac Catalyst, native AppKit, SwiftUI for macOS, or an iPad app on the Mac: how do you choose, and what…
  • 20.7.1 What are os(), canImport() and targetEnvironment() actually true for on each Apple platform, and where does…
  • 20.3.2 What does a Mac app need that an iOS app does not: the menu bar, windows, the App Sandbox, Hardened Runtime…

← How does a menu item find its target on the responder chain, what do…