Как изменить UIMenu для UIBarButtonItem и UIButton

I OS 14 добавляет возможность отображать меню при касании или долгом нажатии UIBarButtonItem или UIButton, например:

let menu = UIMenu(children: [UIAction(title: "Action", image: nil) { action in
    //do something
}])
button.menu = menu
barButtonItem = UIBarButtonItem(title: "Show Menu", image: nil, primaryAction: nil, menu: menu)

Это чаще всего заменяет листы действий (UIAlertController с участием actionSheetстиль). Очень часто бывает динамический лист действий, где действия только включены или могут быть отключены в зависимости от некоторого состояния в момент, когда пользователь нажимает кнопку. Но с этим API меню создается во время создания кнопки. Как вы можете изменить меню до его представления или иным образом сделать его динамическим, чтобы гарантировать, что соответствующие действия доступны и в правильном состоянии, когда оно появится?

5 ответов

Решение

Одно решение, которое я нашел, - это сохранить ссылку на элемент или кнопку кнопки на панели и воссоздавать меню каждый раз, когда любое изменение состояния влияет на доступные действия в меню. menuявляется настраиваемым свойством, поэтому его можно изменить в любое время после создания кнопки. Вы также можете получить текущее меню и заменить его дочерние элементы следующим образом:button.menu = button.menu?.replacingChildren([])

Для сценариев, когда вы не получаете информацию об изменении состояния, вам действительно нужно иметь возможность обновлять меню прямо перед его появлением. Для UIButton вы можете изменить меню, когда пользователь касается кнопки с помощью target/action следующим образом:button.addTarget(self, action: #selector(buttonTouchedDown(_:)), for: .touchDown). Я подтвердил, что это работает с бета-версией iOS 14, по крайней мере, в настоящее время, но только еслиshowsMenuAsPrimaryActionложно, поэтому им нужно долго нажимать, чтобы открыть меню. Я не нашел решения для UIBarButtonItem, но вы можете использовать UIButton в качестве настраиваемого представления для UIBarButtonItem.

Они не кажутся отличными решениями, обновятся, если я найду что-то получше.

После некоторого испытания я обнаружил, что вы можете изменить UIButton с .menu установив menu собственность null сначала установите новый UIIMenu

вот пример кода, который я сделал

      @IBOutlet weak var button: UIButton!

func generateMenu(max: Int, isRandom: Bool = false) -> UIMenu {
    let n = isRandom ? Int.random(in: 1...max) : max
    print("GENERATED MENU: \(n)")
    let items = (0..<n).compactMap { i -> UIAction in
        UIAction(
            title: "Menu \(i)",
            image: nil
        ) {[weak self] _ in
            guard let self = self else { return }

            self.button.menu = nil // HERE

            self.button.menu = self.generateMenu(max: 10, isRandom: true)
            print("Tap")
        }
    }
 
    let m = UIMenu(
        title: "Test", image: nil,
        identifier: UIMenu.Identifier(rawValue: "Hello.menu"),
        options: .displayInline, children: items)
    return m
}

override func viewDidLoad() {
    super.viewDidLoad()
    button.menu = generateMenu(max: 10)
    button.showsMenuAsPrimaryAction = true
}

Нашел решение для случая с UIBarButtonItem. Мое решение основано на решении Jordan H , но я столкнулся с ошибкой - мой метод обновления меню regenerateContextMenu()не вызывался каждый раз, когда появляется меню, и я получал неактуальные данные в меню. Поэтому я немного изменил код:

      private lazy var threePointBttn: UIButton = {
    $0.setImage(UIImage(systemName: "ellipsis"), for: .normal)
    // pay attention on UIControl.Event in next line
    $0.addTarget(self, action: #selector(regenerateContextMenu), for: .menuActionTriggered)
    $0.showsMenuAsPrimaryAction = true
    return $0
}(UIButton(type: .system))


override func viewDidLoad() {
    super.viewDidLoad()
    threePointBttn.menu = createContextMenu()
    navigationItem.rightBarButtonItem = UIBarButtonItem(customView: threePointBttn)
}

private func createContextMenu() -> UIMenu {
    let action1 = UIAction(title:...
    // ...
    return UIMenu(title: "Some title", children: [action1, action2...])
}

@objc private func regenerateContextMenu() {
    threePointBttn.menu = createContextMenu()
}

протестировано на iOS 14.7.1

Я добавляю расширение для UIMenu:

      extension UIMenu {
    /// rebuilds menu on every access
    static func lazyMenu(builder: @escaping () -> UIMenu) -> UIMenu {
        return UIMenu(children: [
                UIDeferredMenuElement.uncached { completion in
                    let menu = builder()
                    completion([menu])
                }
            ])
    }
}

Тогда везде, где необходимо динамическое меню, используйте:

      UIMenu.lazyMenu {
    // will be called on every menu access
    return UIMenu(options: .displayInline, children: myDynamicMenuItems)
}

Изменена версия Джордана Х , чтобы разделить задание и построить действие.

Это будет создавать меню на лету каждый раз, когда нажимается кнопка.

      override func viewDidLoad() {
  super.viewDidLoad()
  navigationItem.rightBarButtonItem?.menu = UIMenu(children: [
    // build menu every time the button is tapped
            UIDeferredMenuElement.uncached { [weak self] completion in
                if let menu = self?.buildMenu() as? UIMenu {
                completion([menu])
                }
            }
        ])
}

func buildMenu() -> UIMenu {
    var actions: [UIMenuElement] = []
    // build actions
    UIAction(title: "Filter", image: UIImage(systemName: "line.3.horizontal.decrease.circle")) { _ in
                self.filterTapped()
    }
    actions.append(filterAction)
    return UIMenu(options: .displayInline, children: actions)
}
Другие вопросы по тегам