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

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


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



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


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


    • Контекстные меню работают на любых устройствах под управлением iOS 13. Поддержка 3D touch от устройства не требуется. Поэтому, в частности, их можно применять на всех 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 {
        @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 так:


    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
        }
    }

    Заключение


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


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


    Поделиться публикацией

    Комментарии 0

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

    Самое читаемое