Search code examples
iosmacosmacos-monterey

UIMenuController crashes on macOS Monterey


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?


Solution

  • 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).