Menu items grayed out using split view controller - Catalyst

Viewed 1081

I'm working to bring my iPad app to the Mac using Catalyst. My app uses a split view controller. The master view controller has two rows that can be tapped that let a user take a photo or select a photo from the camera roll. I am trying to add two menu items with keyboard shortcuts for each of these actions.

After adding the menu items following the steps in a WWDC video, the menu items are grayed out when the app first launches. Below is a screenshot showing the app first launched and Take Photo and Select From Camera Roll are both grayed out. Grayed out menu items

However, if I toggle/tap any item in the master view controller, the menu items become enabled and work as intended. Once I select a menu item, it goes back to being grayed out and I need to toggle/tap an item in the master view controller to make them enabled again. enabled menu items

I can't figure out why the items are being grayed out. I'm thinking it may be related to the split view controller but have not been able to figure anything out.

The code I'm using to add the menu items is pretty simple. I added @IBAction to the methods that take a photo and select from the camera roll. I added a Main Menu to my Storyboard file with two new Inline Menu items and connected each to to the methods.

3 Answers

UPDATE: I mixed it up. This is only valid for a macOS but not for a Catalyst app !!!.

For Catalyst the best way is to use a menu builder and related functionality.

------ Ignore this if its about Catalyst -------

Did you try switch off "Auto Enables Items" for the menu. If this will not help or you want to control activation I would follow the documentation from Apple here or try with something like below in your view controller.

 override func validate(_ command: UICommand) {

      switch command.action {
      case #selector(doSomething):

          command.title = "Change the title"
          command.attributes = [.disabled]
            // command.attributes = []

        default:
        break
      }
    }

enter image description here

I've encountered a perhaps related problem in my Catalyst app.

I'm using menu the menu-builder method, but I find that the app will request a menu for one view controller but try to validate it against another view controller. I presume that this is because I have two view controllers presiding over parts of the same screen.

The workaround I used is to handle queries for one view controller's menu items in the other view controller. For example, let's say you have controllers sharing the screen, called

  • MainVC
  • PlayViewController

The app will call the menu builder for MainVC, but you want to have some hotkeys that call PlaybackVC methods. So you populate the Playback menu with an item that calls PlayViewController.nextClip(), for example.

@available(iOS 13.0, *)
class func playbackMenu() -> UIMenu
{
let prevCmd =
    UIKeyCommand(title: "Previous clip",
                 image: nil,
                 action: #selector(PlayViewController.cmdPrevClip(_:)),
                 input: UIKeyCommand.inputLeftArrow,
                 modifierFlags: .control,
                 propertyList: nil)

let nextCmd =
    UIKeyCommand(title: "Next clip",
                 image: nil,
                 action: #selector(PlayViewController.cmdNextClip(_:)),
                 input: UIKeyCommand.inputRightArrow,
                 modifierFlags: .control,
                 propertyList: nil)

let loopCmd =
    UIKeyCommand(title: "Loop",
                 image: nil,
                 action: #selector(PlayViewController.cmdToggleLoopClip(_:)),
                 input: "L",
                 modifierFlags: .control,
                 propertyList: nil)
let theMenu =
    UIMenu(title: "Playback",
           image: nil,
           identifier: UIMenu.Identifier("com.atomos.AtomRemoteMenus.playbackMenu"),
           options: .destructive,
           children: [prevCmd, nextCmd, loopCmd])

return theMenu
}

Later, when the OS asks MainVC to validate items in the Playback menu, MainVC's canPerformAction() method needs to handle it like so:

else if action == #selector(PlayViewController.cmdNextClip(_:))
{
    if m_playController != nil
    {
        return m_playController!.canPerformAction(#selector(PlayViewController.cmdNextClip(_:)), withSender: sender)
    }
    else
    {
        return false
    }
}

So yes I have a redundant canPerformAction method in PlayViewController because I don't know if it might someday be called.

You might think hey, I'll just make stub methods in MainVC and put calls to those in the Playback menu, and in turn call the PlayViewController's methods. Nope, because each menu item's command targets what it thinks the active view controller is at any given point... which wasn't MainVC in my case.

I've resolved this issue by using a custom subclass of UISplitViewController which:

  1. Overrides canPerformAction(_:withSender:) . This function checks with both its master view controller AND its details view controller if they can handle the action, and returns the result.
  2. Includes a stub @IBAction for every menu command (yes, painful!) that then passes it off to the appropriate child view controller.

This is my implementation:

class CustomSplitViewController: UISplitViewController {
    
    enum MasterDetail: Int {
        case master = 0
        case detail = 1
    }
    
    func child(ofType type: MasterDetail) -> UIViewController? {
        let childTypeIndex = type.rawValue > viewControllers.count - 1 ? 0 : type.rawValue
        if let navController = viewControllers[childTypeIndex] as? UINavigationController {
            if let viewController = navController.topViewController {
                if viewController.navigationController?.visibleViewController == viewController {
                    return viewController
                }
            }
        }
        
        return nil
    }
    
    override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
        return child(ofType: .master)?.canPerformAction(action, withSender: sender) ?? child(ofType: .detail)?.canPerformAction(action, withSender: sender) ?? false
    }
    
    @IBAction func someMasterControllerAction(_ sender: Any?) {
        (child(ofType: .master) as? MasterController)?. someMasterControllerAction(sender)
    }
    
    @IBAction func someDetailControllerAction(_ sender: Any?) {
        (child(ofType: .detail) as? MasterController)?. someDetailControllerAction(sender)
    }

    ...
    ...
    ...

}
Related