Хабрахабр

Как реализовать контекстные меню (Context Menu) в iOS 13

Всем привет, меня зовут Денис, мы разрабатываем сервис по аналитике подписок iOS-приложений – Apphud.

Они выглядят так: На WWDC 2019 Apple представила новый способ взаимодействия с интерфейсом вашего приложения: контекстные меню.

В этой статье мы рассмотрим некоторые тонкости их использования и научимся их делать.

Но между ними есть и несколько существенных отличий. Контекстные меню являются логичным продолжением технологии “Peek and Pop”, когда пользователь мог открыть предпросмотр элемента, сильно нажав на него.

  • Поддержка 3D touch от устройства не требуется. Контекстные меню работают на любых устройствах под управлением iOS 13. Поэтому, в частности, их можно применять на всех iPad.

  • Кнопки, позволяющие взаимодействовать с элементом, появляются сразу и не требуют свайпа вверх.

Чтобы открыть контекстное меню, пользователю достаточно удержать палец на нужном элементе или сильно на него нажать (если устройство поддерживает 3D Touch).

Рекомендации при использовании контекстных меню

Apple в Human Interface Guidelines рекомендует придерживаться следующих правил при проектировании контекстных меню.

Проектируйте правильно

Тогда пользователю будет казаться, что приложение работает неправильно. Будет нехорошо, если вы добавите меню для некоторых элементов в одних местах и не добавите его для похожих элементов в других.

Включайте в меню только необходимое

“Наиболее часто” – ключевая фраза. Контекстное меню – отличное место для наиболее часто использующихся команд. Не добавляйте туда все подряд.

Используйте вложенные меню

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

Используйте не более 1 уровня вложенности

Apple не рекомендует использовать более 1 уровня вложенности. Несмотря на то, что вложенные меню могут сделать навигацию проще, они ее могут и запросто усложнить.

Располагайте наиболее часто используемые пункты в верхней части

Люди в первую очередь фокусируются на верхней части меню, поэтому так им немного проще будет сориентироваться в вашем приложении.

Используйте группировку

Группируйте похожие пункты меню

Избегайте одновременного использования контекстного меню и меню редактирования на одном элементе

Они могут конфликтовать друг с другом, потому что оба вызываются долгим тапом.

Меню редактирования в iOS

Не добавляйте отдельную кнопку “Открыть” в меню

Дополнительная кнопка “Открыть” будет лишней. Пользователи могут открыть элемент, просто тапнув по нему.

Простейшее контекстное меню для UIView

Разумеется, меню работают только на iOS 13 и выше и для тестирования вам понадобится Xcode 11. Теперь, когда мы усвоили основные правила использования контекстных меню, перейдем к практике. Beta-версию Xcode 11 вы можете скачать здесь.

Давайте добавим контекстное меню, например, на UIImageView, как в анимации выше.

Для этого достаточно добавить объект UIImageView на контроллер и написать несколько строк кода, например в методе viewDidLoad:

class SingleViewController: UIViewController
}

Конструктор требует указать делегат, который будет отвечать за меню. В начале создается объект класса 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 }
}

Внутри самого метода мы создаем объект класса UIContextMenuConfiguration. Если в этом методе вернуть nil, то контекстное меню не будет вызвано. При создании мы передаем эти параметры:

  • 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

При долгом нажатии на ячейку пользователю будет показано меню с пунктом “Archive”, вот так: Давайте добавим контекстное меню в UICollectionView.

Вот, что у нас вышло: Добавление контекстного меню в 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

Нужно имплементировать метод contextMenuConfigurationForRowAt протокола UITableViewDelegate так: Здесь все аналогично UICollectionView.

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 инициализируется из сториборда и отображается в контекстном меню.

Для этого нужно имплементировать метод willCommitMenuWithAnimator протокола UITableViewDelegate. Осталось добавить обработку нажатия на этот ViewController. Сам обработчик поместим внутрь animator.addCompletion:

override func tableView(_ tableView: UITableView, willCommitMenuWithAnimator animator: UIContextMenuInteractionCommitAnimating) { animator.addCompletion { // Put handler here }
}

Заключение

И, как видите, их реализация довольно проста. Контекстные меню — это новый мощный инструмент взаимодействия пользователя с вашим приложением. Но не следует забывать, что методы могут измениться, пока не выйдет релизная версия iOS 13.

Что почитать?

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть