惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog RSS Feed
量子位
Y
Y Combinator Blog
大猫的无限游戏
大猫的无限游戏
B
Blog
U
Unit 42
C
Check Point Blog
I
InfoQ
aimingoo的专栏
aimingoo的专栏
雷峰网
雷峰网
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 【当耐特】
人人都是产品经理
人人都是产品经理
The Cloudflare Blog
H
Help Net Security
MongoDB | Blog
MongoDB | Blog
博客园 - Franky
H
Hackread – Cybersecurity News, Data Breaches, AI and More
J
Java Code Geeks
Microsoft Azure Blog
Microsoft Azure Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
云风的 BLOG
云风的 BLOG
宝玉的分享
宝玉的分享
爱范儿
爱范儿

Peter Steinberger

OpenClaw, OpenAI and the future | Peter Steinberger Shipping at Inference-Speed | Peter Steinberger The Signature Flicker | Peter Steinberger Just Talk To It - the no-bs Way of Agentic Engineering | Peter Steinberger Claude Code Anonymous | Peter Steinberger Live Coding Session: Building Arena | Peter Steinberger My Current AI Dev Workflow | Peter Steinberger Essential Reading for Agentic Engineers - August 2025 | Peter Steinberger Just One More Prompt | Peter Steinberger Poltergeist: The Ghost That Keeps Your Builds Fresh | Peter Steinberger Don't read this Startup Slop | Peter Steinberger Essential Reading for Agentic Engineers - July 2025 | Peter Steinberger Self-Hosting AI Models After Claude's Usage Limits | Peter Steinberger Logging Privacy Shenanigans | Peter Steinberger VibeTunnel's first AI-anniversary | Peter Steinberger Making AppleScript Work in macOS CLI Tools: The Undocumented Parts | Peter Steinberger Peekaboo 2.0 – Free the CLI from its MCP shackles | Peter Steinberger Command your Claude Code Army, Reloaded | Peter Steinberger Essential Reading for Agentic Engineers | Peter Steinberger Slot Machines for Programmers: How Peter Builds Apps 20x Faster with AI | Peter Steinberger My AI Workflow for Understanding Any Codebase | Peter Steinberger stats.store: Privacy-First Sparkle Analytics | Peter Steinberger Showing Settings from macOS Menu Bar Items: A 5-Hour Journey | Peter Steinberger VibeTunnel: Turn Any Browser into Your Mac's Terminal | Peter Steinberger Vibe Meter 2.0: Calculating Claude Code Usage with Token Counting | Peter Steinberger llm.codes: Make Apple Docs AI-Readable | Peter Steinberger Automatic Observation Tracking in UIKit and AppKit: The Feature Apple Forgot to Mention | Peter Steinberger Peekaboo MCP – lightning-fast macOS screenshots for AI agents | Peter Steinberger Migrating 700+ Tests to Swift Testing: A Real-World Experience | Peter Steinberger Commanding Your Claude Code Army | Peter Steinberger
Forbidden Controls in Catalyst: Optimize Interface for Ma...
Peter Steinberger · 2020-09-23 · via Peter Steinberger

While working on our PDF Viewer update for Big Sur and switching to the new Catalyst Mac Interface Idiom, I was greeted with a new exception coming directly from UIKit:

[General] UIStepper is not supported when running Catalyst apps in the Mac idiom.
[General] (
	0   CoreFoundation                      0x00007fff2067fbdf __exceptionPreprocess + 242
	1   libobjc.A.dylib                     0x00007fff2029f469 objc_exception_throw + 48
	2   UIKitCore                           0x00007fff464af1e5 -[UIView(UICatalystMacIdiomUnsupported_Internal) _throwForUnsupportedNonMacIdiomBehaviorWithReason:] + 0
	3   UIKitCore                           0x00007fff45c7b5b6 -[UIStepper _didMoveFromWindow:toWindow:] + 218

Catalyst Mac Idiom

Let’s take a step back — what’s the Catalyst Mac Idiom? With macOS 11 Big Sur, Catalyst learned a new presentation mode. Next to the classic mode where Catalyst apps are scaled to 77 percent and retain their iPad look, there’s a new Optimize Interface for Mac mode that doesn’t use scaling and replaces various UIKit controls with AppKit counterparts.

The new mode is available with Big Sur, and apps can be built so that they use scaling on Catalina and the new Mac mode on Big Sur. We’ll be releasing a new version of PDF Viewer for Mac using the new optimized mode as soon as Apple finalizes Big Sur.

If we write code that works on both Catalina and Big Sur, a category like this will be useful:

extension UIDevice {
    /// Checks if we run in Mac Catalyst Optimized For Mac Idiom
    var isCatalystMacIdiom: Bool {
        if #available(iOS 14, *) {
            return UIDevice.current.userInterfaceIdiom == .mac
        } else {
            return false
        }
    }
}

This can later be used, for example, in SwiftUI (SwiftUI’s Stepper maps to UIStepper, which is disallowed):

// UIStepper is not allowed for Catalyst Mac Idiom.
if !UIDevice.current.isCatalystMacIdiom {
    Stepper("Current Page: \(pageIndex + 1)", value: $pageIndex, in: 0...document.pageCount - 1)
    .padding()
}

AppKit In UIKit

Internally, Apple uses a private _UINSView class to host an actual NSView. It’s too bad Apple didn’t consider making this class public API, which would’ve allowed us to freely mix AppKit with UIKit, but it’s a start.

UIButton Mac Idiom

If we look into the runtime, the class does pretty much what we’d expect (I omitted some less useful methods for the sake of brevity):

lldb) po [NSClassFromString(@"_UINSView") _shortMethodDescription]
<_UINSView: 0x7fff86fac238>:
in _UINSView:
	Properties:
		@property (readonly) struct CGSize _intrinsicFrameSize;
		@property (readonly) NSView* contentNSView;  (@synthesize contentNSView = _contentNSView;)

Forbidden Controls

Back to our crash — things make a bit more sense now. There’s no great equivalent for UIStepper in AppKit, so the folks at Apple decided it’s better to throw an exception if this control is used (FB8727188).

The problem: It isn’t documented which controls are disallowed, and what’s even more problematic is some controls are allowed, but customizations are disallowed. What does UISlider map toward? We can get the pointer from the visual debugger and then use our knowledge of the class structure to call directly into the AppKit view:

(lldb) po [0x7f9503c488f0 contentNSView]
<NSSlider: 0x7f9503c37a40>

NSSlider is the obvious choice, however, the Mac version lacks the appearance customization options UIKit has. Calling any of these customization methods will simply throw (crash) at runtime:

setMinimumTrackImage:forState: is not supported on PSPDFBrightnessSlider when running Catalyst apps in the Mac idiom.
 (
	0   CoreFoundation                      0x00007fff2067fbdf __exceptionPreprocess + 242
	1   libobjc.A.dylib                     0x00007fff2029f469 objc_exception_throw + 48
	2   UIKitCore                           0x00007fff464af1e5 -[UIView(UICatalystMacIdiomUnsupported_Internal) _throwForUnsupportedNonMacIdiomBehaviorWithReason:] + 0
	3   UIKitCore                           0x00007fff4582af70 -[UISlider setMinimumTrackImage:forState:] + 197

This is problematic, as it’s yet another restriction that isn’t documented. A better choice would’ve been to keep the UIKit variant in case customization exceeds what AppKit can do. It’s a surprising late change, and folks working on Catalyst apps are frustrated:

{% twitter https://twitter.com/_imaginethis/status/1308412481375801344?s=21 %}

Finding What’s Forbidden

Since there’s no documentation and there are no release notes about any of these behaviors, that just leaves Hopper and decompiling UIKitCore.

We find references to following controls:

  • UIStepper (styling properties such as setMinimumTrackImage:forState:)
  • UIPickerView
  • UIRefreshControl
  • UISwitch (inside setTitle:)
  • UIButton

This includes any subclasses like AVScrubber (part of AVPlayerView), so this can become a real problem.

The main method throwing is _throwForUnsupportedNonMacIdiomBehaviorWithReason, so it makes sense to search for it. It checks if the bundle identifier starts with “com.apple”, and if it does, it just logs an error, while all other apps get an exception. There’s yet another check for _allowsUnsupportedMacIdiomBehavior, which is interesting. It seems the above controls at least have partial support in Big Sur. This can be enabled via calling _setAllowsUnsupportedMacIdiomBehavior:] on them. And indeed, calling [UIStepper _setAllowsUnsupportedMacIdiomBehavior:1]; (I’m using Objective-C here since it’s easier to just redeclare the method in the header) does result in a working app.

UIStepper on macOS via a hack

There’s a stepper (next to the “Current Page: 2” label), however, clicking it doesn’t work. This is obviously not something we should ever use for shipping, but it’s fun to play with the internals!

Digging Deeper

However most of the throw calls seem to be missing. I’ve been looking at the iOS version this whole time, when clearly this is conditional code and we need to look at the macOS version instead.

iOS Path: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework1

macOS Path: /System/iOSSupport/System/Library/PrivateFrameworks/UIKitCore.framework

However, when we open the framework on macOS, there’s no binary. WTH? I remembered reading that Big Sur ships with a built-in dynamic linker cache of all system-provided libraries. Luckily, Hopper has already been updated to be informed of that shared cache, so we can load it via /System/Library/dyld/ and then select UIKitCore as the target.

Wait until everything is loaded and then select File > Produce Pseudo-Code File for All Procedures. This might take a few hours. Once the file is generated, pick a fast text editor (my weapon of choice for something like this is Sublime Text) and load the file. There, search for _throwForUnsupportedNonMacIdiomBehaviorWithReason: again.

The problem: The file is heavily obfuscated; Hopper can’t read the selector names. We can search for the string “Unsupported iOS or Mac Catalyst iPad Idiom” to find the selector matching _throwForUnsupportedNonMacIdiomBehaviorWithReason:. In my case, that’s sub_7fff465801e5.

Conclusion

However, that’s the end of the story for now. The data is there, but the tools can’t yet (!) get a useful format out. We know there are at least five controls that throw an exception on some usage at runtime, however, which one it is exactly is currently hard to know. Shipping a Catalyst app in the new Mac idiom is definitely an adventure.

Decompile via LLDB

Jeff Johnson points out that one can abuse LLDB to decompile methods individually. That approach would take far too long to find all the calls that throw here, but it’s a start.

The dyld-shared-cache-big-sur project uses modifications to Apple’s dyld project to fix Objective-C information when extracting the dyld_shared_cache from macOS Big Sur to help Hopper generate readable pseudocode. (Thanks @lclhrst for the hint!)

Using this project, we can extract the dyld cache into a folder:

./dyld_shared_cache_util -extract ~/Developer/macOS\ Big\ Sur /System/Library/dyld/dyld_shared_cache_x86_64

And then we can decompile UIKitCore with selector names. This doesn’t resolve the individual selector calls, but it’s a step forward.

Update: Apple added the list of disallowed controls in the macOS Big Sur 11.0.1 Release Notes

This answers which controls are affected. Curiously this currently is the only place, and there’s no mention in the headers or compile-time support to warn when these methods are used - but it’s better than no docs.

  1. In the early days, it was just UIKit. A few years ago, Apple created an internal framework called UIKitCore, which exports more APIs and can be used for internal apps. UIKit is the smaller API for external developers (us). ↩