如何为UIMenu设置固定排序,避免自动调整显示顺序?
问题
我在应用中创建了一个按特定顺序填充的UIMenu,希望菜单始终保持填充时的顺序显示。但UIMenu的默认行为会将填充顺序视为「优先级顺序」,有时会调整列表顺序,把优先级最高的第一项靠近打开菜单的按钮,导致列表偶尔显示为倒序。
我知道UIContextMenu可以通过menu.preferredMenuElementOrder = .fixed来固定顺序,但没找到UIMenu的类似设置方法。请问如何为UIMenu设置固定排序或阻止这种自动排序行为?
附上自定义文本字段类中创建和填充菜单的代码片段:
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") } }
解决方案
1. iOS 16+ 直接设置固定顺序
从iOS 16开始,UIMenu新增了preferredElementOrder属性,用法和UIContextMenu一致,设置为.fixed即可强制菜单保持你定义的元素顺序,不会自动调整:
// 分类菜单修改 let categorizedMenu = UIMenu(options: .singleSelection, children: dropdownChildren) categorizedMenu.preferredElementOrder = .fixed self.dropdownMenu = categorizedMenu // 无分类菜单同理 let uncategorizedMenu = UIMenu(options: .displayInline, children: dropdownChildren) uncategorizedMenu.preferredElementOrder = .fixed self.dropdownMenu = uncategorizedMenu
2. 兼容iOS 15及以下:优化结构+启用内联展示
iOS 15及更早版本没有preferredElementOrder,可通过以下两点解决:
- 用子
UIMenu替代禁用的UIAction做分组标题:你当前用禁用的UIAction作为分组头,系统会将其与普通UIAction视为同一层级,容易触发排序调整。改成将每个分组做成独立的子UIMenu,系统会保留子菜单的顺序。 - 给父菜单添加
.displayInline选项:该选项会强制菜单以内联方式展示,不会根据按钮位置反转顺序。
修改后的分类菜单代码如下:
case .categorized: guard let dropdownHeaders = viewModel.pulldownHeaders else { fatalError("\nFatal Error: Must pass in headers for categorized dropdown type\n") } var sectionMenus: [UIMenu] = [] for (index, section) in viewModel.sectionOptions.enumerated() { let sectionHeader = dropdownHeaders[index] let sectionActions = section.map { title in UIAction(title: title, state: .off) { [self] _ in textField.text = title self.delegate?.selectedOptionFromList(pulldownField: self, selection: title) } } // 将每个分组封装为子UIMenu,标题作为菜单标题 let sectionMenu = UIMenu(title: sectionHeader, children: sectionActions) sectionMenus.append(sectionMenu) } // 父菜单添加.displayInline选项固定顺序 let categorizedMenu = UIMenu(options: [.singleSelection, .displayInline], children: sectionMenus) self.dropdownMenu = categorizedMenu
无分类菜单的.displayInline已正确设置,保持即可。无论iOS版本,菜单都会严格按照你定义的顺序展示,不会出现倒序问题。
内容的提问来源于stack exchange,提问作者zach2161
相关产品推荐
相关产品推荐

