Они выглядят так:
Контекстные меню являются логичным продолжением технологии “Peek and Pop”, когда пользователь мог открыть предпросмотр элемента, сильно нажав на него. Но между ними есть и несколько существенных отличий.
Чтобы открыть контекстное меню, пользователю достаточно удержать палец на нужном элементе или сильно на него нажать (если устройство поддерживает 3D Touch).
Apple в Human Interface Guidelines рекомендует придерживаться следующих правил при проектировании контекстных меню.
Будет нехорошо, если вы добавите меню для некоторых элементов в одних местах и не добавите его для похожих элементов в других. Тогда пользователю будет казаться, что приложение работает неправильно.
Контекстное меню – отличное место для наиболее часто использующихся команд. “Наиболее часто” – ключевая фраза. Не добавляйте туда все подряд.
Используйте вложенные меню, чтобы пользователю было проще сориентироваться. Дайте пунктам меню простые и понятные названия.
Несмотря на то, что вложенные меню могут сделать навигацию проще, они ее могут и запросто усложнить. Apple не рекомендует использовать более 1 уровня вложенности.
Люди в первую очередь фокусируются на верхней части меню, поэтому так им немного проще будет сориентироваться в вашем приложении.
Группируйте похожие пункты меню
Они могут конфликтовать друг с другом, потому что оба вызываются долгим тапом.
Пользователи могут открыть элемент, просто тапнув по нему. Дополнительная кнопка “Открыть” будет лишней.
Теперь, когда мы усвоили основные правила использования контекстных меню, перейдем к практике. Разумеется, меню работают только на iOS 13 и выше и для тестирования вам понадобится Xcode.
Вы можете скачать полный пример кода здесь.
Давайте добавим контекстное меню, например, на UIImageView
, как в анимации выше.
Для этого достаточно добавить объект UIImageView
на контроллер и написать несколько строк кода, например в методе viewDidLoad
:
class SingleViewController: UIViewController { @IBOutlet var imageView: UIImageView! override func viewDidLoad() { super.viewDidLoad() imageView.isUserInteractionEnabled = true let interaction = UIContextMenuInteraction(delegate: self) imageView.addInteraction(interaction) } }
В начале создается объект класса UIContextMenuInteraction
. Конструктор требует указать делегат, который будет отвечать за меню. Вернемся к этому чуть позднее. А методом `addInteraction` мы добавляем наше меню к картинке.
Теперь осталось реализовать протокол UIContextMenuInteractionDelegate
. В нем только один обязательный метод, который отвечает за создание меню:
extension SingleViewController: UIContextMenuInteractionDelegate { func contextMenuInteraction(\_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { actions -> UIMenu<UIAction>? in let save = UIAction(\_\_title: "My Button", image: nil, options: \[\]) { action in // Put button handler here } return configuration } }
Если в этом методе вернуть nil
, то контекстное меню не будет вызвано. Внутри самого метода мы создаем объект класса UIContextMenuConfiguration
. При создании мы передаем эти параметры:
identifier
– идентификатор меню.previewProvider
– кастомный контроллер, который опционально может быть отображен вместо текущего элемента в меню. Мы рассмотрим это чуть позднее.actionProvider
мы передаем элементы контекстного меню.Сами элементы создаются проще некуда: указывается название, опциональная иконка и обработчик нажатия на пункт меню. Вот и все!
Давайте немного усложним. Добавим к нашей картинке меню с двумя пунктами: “Save” и “Edit…”. По нажатии на “Edit…” откроется подменю с пунктами “Rotate” и “Delete”. Это должно выглядеть так:
Для этого надо переписать метод протокола UIContextMenuInteractionDelegate
следующим образом:
func contextMenuInteraction(\_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { actions -> UIMenu<UIAction>? in // Creating Save button let save = UIAction(\_\_title: "Save", image: UIImage(systemName: "tray.and.arrow.down.fill"), options: \[\]) { action in // Just showing some alert self.showAlert(title: action.title) } // Creating Rotate button let rotate = UIAction(\_\_title: "Rotate", image: UIImage(systemName: "arrow.counterclockwise"), options: \[\]) { action in self.showAlert(title: action.title) } // Creating Delete button let delete = UIAction(\_\_title: "Delete", image: UIImage(systemName: "trash.fill"), options: .destructive) { action in self.showAlert(title: action.title) } // Creating Edit, which will open Submenu let edit = UIMenu<UIAction>.create(title: "Edit...", children: \[rotate, delete\]) // Creating main context menu return UIMenu<UIAction>.create(title: "Menu", children: \[save, edit\]) } return configuration }
Здесь мы создаем последовательно кнопки “Save”, “Rotate” и “Delete”, добавляем последние две в подменю “Edit…” и оборачиваем все в главное контекстное меню.
UICollectionView
Давайте добавим контекстное меню в UICollectionView
. При долгом нажатии на ячейку пользователю будет показано меню с пунктом “Archive”, вот так:
Добавление контекстного меню в UICollectionView
проще простого: достаточно реализовать опциональный метод func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration?
протокола UICollectionViewDelegate
. Вот, что у нас вышло:
override func collectionView(\_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { actions -> UIMenu<UIAction>? in let action = UIAction(\_\_title: "Archive", image: UIImage(systemName: "archivebox.fill"), options: .destructive) { action in // Put button handler here } return UIMenu<UIAction>.create(title: "Menu", children: \[action\]) } return configuration }
Здесь, как и прежде, создается элемент и само меню. Теперь при долгом (сильном) нажатии на ячейку пользователь увидит контекстное меню.
UITableView
Здесь все аналогично UICollectionView
. Нужно имплементировать метод contextMenuConfigurationForRowAt` протокола `UITableViewDelegate
так:
It’s almost the same as `UICollectionView.` You should implement `contextMenuConfigurationForRowAt` method of `UITableViewDelegate` protocol: override func tableView(\_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { actions -> UIMenu<UIAction>? in let action = UIAction(\_\_title: "Custom action", image: nil, options: \[\]) { action in // Put button handler here } return UIMenu<UIAction>.create(title: "Menu", children: \[action\]) } return configuration }
Но что, если мы хотим использовать кастомный экран в контекстном меню? Например, такой:
Для этого при создании UIContextMenuConfiguration
следует передать нужный UIViewController
в previewProvider
. Вот пример кода, реализующего это:
class PreviewViewController: UIViewController { static func controller() -> PreviewViewController { let storyboard = UIStoryboard(name: "Main", bundle: nil) let controller = storyboard.instantiateViewController(withIdentifier: "PreviewViewController") as! PreviewViewController return controller } } extension TableViewController: UITableViewDelegate { override func tableView(\_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: { () -> UIViewController? in // Return Preview View Controller here return PreviewViewController.controller() }) { \_ -> UIMenu<UIAction>? in let action = UIAction(\_\_title: "Custom action", image: nil, options: \[\]) { action in // Put button handler here } return UIMenu<UIAction>.create(title: "Menu", children: \[action\]) } return configuration } }
В примере PreviewViewController
инициализируется из сториборда и отображается в контекстном меню.
Осталось добавить обработку нажатия на этот ViewController
. Для этого нужно имплементировать метод willCommitMenuWithAnimator
протокола UITableViewDelegate
. Сам обработчик поместим внутрь animator.addCompletion
:
override func tableView(\_ tableView: UITableView, willCommitMenuWithAnimator animator: UIContextMenuInteractionCommitAnimating) { animator.addCompletion { // Put handler here } }
Контекстные меню — это новый мощный инструмент взаимодействия пользователя с вашим приложением. И, как видите, их реализация довольно проста.