Search code examples
swiftuikituibuttondropdownuimenu

Can I set a Fixed Order for a UIMenu?


I have a UIMenu in my app that I am populating in a specific order. I want the menu to always appear in the order that I populated it in.

However, the automatic behavior of UIMenu displays the menu with this order as the "priority" order. Meaning, it will sometimes change the order of the list to position the first item (highest priority) closest to the button my menu is opened from.

So with this default behavior, I am seeing my list sometimes displayed in reverse order.

I know UIContextMenu has a .fixed option that can be set using menu.preferredMenuElementOrder. I'm not seeing anything of the sort for UIMenu though.

Any ideas on how I can set my menu order to .fixed or prevent this automatic ordering behavior?

Here's a code snippit to show how I am creating and populating my menu within a custom textfield class:

    private var dropdownMenu = UIMenu() {
    didSet {
        dropdownButton.menu = dropdownMenu
    }
}

/// configureDropdownMenu() is used to set the menu contents for your dropdown textfield.
/// - Parameter viewModel: Must set the data of your ViewModel in the View layer. The ViewModel data will be used to populate the dropdown menu.
func configureDropdownMenu(with viewModel: CustomPulldownTextFieldViewModel) {
    // Set the field type and the delegate
    self.customFieldType = viewModel.dropdownType
    self.isEnabled = true

    var dropdownOptions: [String: [UIAction]] = [:]
    var dropdownChildren: [UIMenuElement] = []
    
    switch viewModel.dropdownType {
    case .categorized:
        guard let dropdownHeaders = viewModel.pulldownHeaders else {
            fatalError("\nFatal Error: Must pass in headers for categorized dropdown type\n")
        }
        
        for (index, section) in viewModel.sectionOptions.enumerated() {
            let sectionHeader = dropdownHeaders[index]
            dropdownOptions[sectionHeader] = section.map { title in
                UIAction(title: title, state: .off) { [self] _ in
                    textField.text = title
                    self.delegate?.selectedOptionFromList(pulldownField: self, selection: title)
                }
            }
        }
        
        for header in dropdownHeaders {
            // Check if the section is not empty before adding it
            if let sectionActions = dropdownOptions[header], !sectionActions.isEmpty {
                dropdownChildren.append(UIAction(title: header, attributes: [.disabled], state: .off, handler: { _ in }))
                dropdownChildren.append(contentsOf: sectionActions)
            }
        }
        
        let categorizedMenu = UIMenu(options: .singleSelection, children: dropdownChildren)
        self.dropdownMenu = categorizedMenu
        
    case .uncategorized:
        // Uncategorized list MUST only have one section in sectionOptions
        dropdownChildren = viewModel.sectionOptions[0].map { title in
            UIAction(title: title, state: .off) { [self] _ in
                textField.text = title
                self.delegate?.selectedOptionFromList(pulldownField: self, selection: title)
            }
        }
        
        let uncategorizedMenu = UIMenu(options: .displayInline, children: dropdownChildren)
        self.dropdownMenu = uncategorizedMenu
    case .none:
        fatalError("\nFatal Error: Must set a dropdownType in the view model\n")
    }
}

Solution

  • Since this menu is added to a button, you should set the preferredMenuElementOrder property of the UIButton.

    dropdownButton.menu = dropdownMenu
    dropdownButton.preferredMenuElementOrder = .fixed