My iOS app uses UIMenuController to show the Copy/Paste context menu. When I launch the app on macOS 12.0 and control-click (right click) with the mouse or the trackpad, the app crashes upon showing the menu with this crash log:
Application Specific Information:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason:
'*** -[__NSDictionaryM setObject:forKey:]: object cannot be nil (key: title)'
Last Exception Backtrace:
0 CoreFoundation 0x1be758118 __exceptionPreprocess + 220
1 libobjc.A.dylib 0x1be4a9808 objc_exception_throw + 60
2 CoreFoundation 0x1be828464 -[__NSCFString characterAtIndex:].cold.1 + 0
3 CoreFoundation 0x1be835270 -[__NSDictionaryM setObject:forKey:].cold.3 + 0
4 CoreFoundation 0x1be691590 -[__NSDictionaryM setObject:forKey:] + 904
5 UIKitCore 0x1e5b85998 -[_UIMenuBarItem properties] + 124
6 UIKitMacHelper 0x1d3bc7058 UINSNSMenuItemFromUINSMenuItem + 96
7 UIKitMacHelper 0x1d3bc6d60 _insertUINSMenuItemsIntoNSMenu + 844
8 UIKitMacHelper 0x1d3bc67c0 UINSNSMenuFromUINSMenu + 152
9 UIKitMacHelper 0x1d3bc6690 -[UINSMenuController _createNSMenu:forContextMenu:] + 92
10 UIKitMacHelper 0x1d3c3505c -[UINSMenuController _prepareToShowContextMenu:activityItemsConfiguration:] + 144
11 UIKitMacHelper 0x1d3c349c0 -[UINSMenuController showContextMenu:inWindow:atLocationInWindow:activityItemsConfiguration:] + 312
12 libdispatch.dylib 0x1be44ce60 _dispatch_call_block_and_release + 32
13 libdispatch.dylib 0x1be44ebac _dispatch_client_callout + 20
14 libdispatch.dylib 0x1be45d0ac _dispatch_main_queue_callback_4CF + 944
15 CoreFoundation 0x1be719e60 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 16
I tried the same with several iOS apps from other developers and all of them crash on macOS when I right-click with the mouse.
Has anyone found a workaround?
It's possible to fix this by swizzling the properties
method on the private _UIMenuBarItem
class. Obviously this comes with the usual disclaimer that this might get you rejected by Apple (but in practice that doesn't seem to cause rejections that often).
Here's the fix: The basic idea is to wrap the original method call in a @try/@catch
block. The crash happens because the original implementation sometimes tries to insert a nil
value for the title
key into an NSDictionary. This workaround catches that exception and then returns a dummy dictionary to satisfy the caller.
UIMenuBarItemMontereyCrashFix.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// Helper class to apply a fix to prevent a crash on macOS Monterey when a user right-clicks in a text field
@interface UIMenuBarItemMontereyCrashFix : NSObject
/// Apply the crash fix. It will only be applied the first time it's called, subsequent calls are no-ops.
/// It will only have an effect when called on macOS Monterey or later.
+ (void)applyCrashFixIfNeeded;
@end
NS_ASSUME_NONNULL_END
UIMenuBarItemMontereyCrashFix.m
#import "UIMenuBarItemMontereyCrashFix.h"
#import <objc/runtime.h>
static BOOL hasCrashFixBeenApplied = NO;
@implementation UIMenuBarItemMontereyCrashFix
/// Apply the crash fix. It will only be applied the first time it's called, subsequent calls are no-ops.
+ (void)applyCrashFixIfNeeded
{
if (@available(macOS 12.0, *)) {} else {
// Bail if we are not running on Monterey
return;
}
if (!hasCrashFixBeenApplied) {
Class UnderscoreUIMenuBarItem = NSClassFromString(@"_UIMenuBarItem");
SEL selector = sel_getUid("properties");
Method method = class_getInstanceMethod(UnderscoreUIMenuBarItem, selector);
IMP original = method_getImplementation(method);
// The crash happens because in some instances the original implementation
// tries to insert `nil` as a value for the key `title` into a dictionary.
// This is how the fix works:
// We wrap the original implementation call in a @try/@catch block. When the
// exception happens, we catch it, and then return a dummy dictionary to
// satisfy the caller. The dummy has `isEnabled` set to NO, and `isHidden` set
// to YES.
IMP override = imp_implementationWithBlock(^id(id me) {
@try {
id res = ((id (*)(id))original)(me);
return res;
}
@catch(NSException *exception) {
return @{
@"allowsAutomaticKeyEquivalentLocalization" : @0,
@"allowsAutomaticKeyEquivalentMirroring" : @0,
@"defaultCommand" : @0,
@"identifier":@"com.apple.menu.application",
@"imageAlwaysVisible" : @0,
@"isAlternate" : @0,
@"isEnabled" : @0,
@"isHidden" : @1,
@"isSeparatorItem" : @0,
@"keyEquivalent" : @"",
@"keyEquivalentModifiers" : @0,
@"remainsVisibleWhenDisabled" : @0,
@"state" : @0,
@"title" : @""
};
}
});
method_setImplementation(method, override);
hasCrashFixBeenApplied = YES;
}
}
@end
Remember to add UIMenuBarItemMontereyCrashFix.h
to your bridging header so you can call it from Swift. Then simply call UIMenuBarItemMontereyCrashFix.applyIfNeeded()
somewhere during your app's startup sequence (for example in your AppDelegate).