"Import from iPhone or iPad" in UITextView context menu under Mac Catalyst

While working on the Mac Catalyst version of my iOS app, I noticed something interesting. I have a UITextView with the allowsEditingTextAttributes property enabled. When running the app on a Mac, the context menu that appears when right-clicking inside the UITextField includes the menu item "Import from iPhone or iPad". That brings up a menu with 3 options each for my iPhone and iPad that I happen to connected to my Mac recently. There options include "Take Photo", "Scan Documents", and "Add Sketch".

I created a brand new iOS app project and simply added a UITextView to the main view controller. After setting allowsEditingTextAttributes to true, it shows the same behavior.

Some questions:

  1. Is this documented anywhere? I'm guessing this is related to Continuity Camera in some way. But there's no mention of this anywhere that I've seen so far.

  2. How can I prevent this menu from appearing? Nothing related to these menus comes through the canPerformAction(_:withSender:) method. And nothing related to these menus is part of the menu item array sent to the UITextViewDelegate textView(_:editMenuForTextIn:suggestedActions:) method. I need to remove this menu in my app because while I support some text attributes (bold, italic, underline), I do not want to allow pictures to be added.

  3. Does anything else in iOS under Mac Catalyst automatically get similar support? If so, what?

Accepted Reply

Success! (mostly).

I finally found a way to remove the "Take a Photo", "Scan Documents", and "Add Sketch" menus. The solution doesn't result in any stack traces either which is nice. The only minor piece left is that this solution does leave the grayed out device name menu item in the context menu. And if there is more than one device, the "Import from iPhone or iPad" menu still appears but only with the grayed out device names.

Here's the code that removes the three unwanted menu items (for each device):

override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
    if action == Selector(("importFromDevice:"))  {
        // sender will be an NSMenuItem. There will be one each for the 3 menus we are trying to hide
        if let menu = sender as? NSObject, menu.responds(to: Selector(("setHidden:"))) {
            menu.setValue(true, forKey: "hidden")
        }
        return false
    }

    return super.canPerformAction(action, withSender: sender)
}

Add this to any view controller that has a UITextView to eliminate the 3 context menu options. Or create a custom subclass of UITextView and put the code in there. Then use the custom text view class anywhere you don't want these menus.

Replies

I have not found any way to remove the "Import from iPhone or iPad" menu. But I have at least found a way to prevent the ensuing image from being added as an attachment.

When a user selects a menu, such as "Take Photo", and then takes a picture with their iOS device and chooses "Use Photo", the UITextViewDelegate textView(_:shouldChangeTextIn:replacementText:) delegate method is called. The replacement text will contain the character NSTextAttachment.character. So to prevent the picture from being added as an attachment to the text view, return false if you find that the replacement text contains NSTextAttachment.character.

It's still a terrible user experience. The user sees the menu, goes through all of the steps to take a photo, and then nothing happens. It would be so much better if there was a way to remove the menu when not wanted.

One more slight step forward. I have found a way to make the "Take Photo", "Scan Documents", and "Add Sketch" menus do nothing. It's still a poor user experience having the menus at all, but at least now the user doesn't get to go through all of the motions of taking a photo or scanning a document with their iOS device just to have the results ignored.

I was able to make this happen by adding the following code:

@objc func importFromDevice(_ sender: Any) {
    // no-op
}

override func target(forAction action: Selector, withSender sender: Any?) -> Any? {
    if action == #selector(importFromDevice) {
        return self
    }

    return super.target(forAction: action, withSender: sender)
}

You can add this to a custom UITextView subclass or to the view controller class containing a text view.

The only issue with this code is that it results in assertion failures and big old stack traces in the console each time the menu gets validated. There's a message about:

-[UINSResponderProxy validateMenuItem:]: We're being asked to validate a menu item whose proxy isn't the one we wrapped.

But I've tested via TestFlight and the app continues to run just fine.

Success! (mostly).

I finally found a way to remove the "Take a Photo", "Scan Documents", and "Add Sketch" menus. The solution doesn't result in any stack traces either which is nice. The only minor piece left is that this solution does leave the grayed out device name menu item in the context menu. And if there is more than one device, the "Import from iPhone or iPad" menu still appears but only with the grayed out device names.

Here's the code that removes the three unwanted menu items (for each device):

override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
    if action == Selector(("importFromDevice:"))  {
        // sender will be an NSMenuItem. There will be one each for the 3 menus we are trying to hide
        if let menu = sender as? NSObject, menu.responds(to: Selector(("setHidden:"))) {
            menu.setValue(true, forKey: "hidden")
        }
        return false
    }

    return super.canPerformAction(action, withSender: sender)
}

Add this to any view controller that has a UITextView to eliminate the 3 context menu options. Or create a custom subclass of UITextView and put the code in there. Then use the custom text view class anywhere you don't want these menus.