Integrate, analyze and improve subscriptions in your iOS app

Разработчику

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

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

Контекстное меню в iOS
Контекстное меню в iOS

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Меню редактирования в iOS
Меню редактирования в 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
Контекстные меню в 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

Здесь все аналогично 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
}

Но что, если мы хотим использовать кастомный экран в контекстном меню? Например, такой:

Контекстные меню в UITableView
Контекстные меню в UITableView

Для этого при создании 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.

Спасибо, если вы дочитали статью до конца. Надеюсь, она оказалась вам полезной. А еще полезным может оказаться сервис, который мы разрабатываем. Apphud — это удобная аналитика для подписок на iOS. Одна из функций Apphud — это отправка событий о подписках в вашу любимую систему аналитики (например, Amplitude, Flurry или Mixpanel). Проект сейчас находится на стадии Beta-тестирования, и вы можете поучаствовать в нем! Все что нужно — перейти на сайт Apphud и оставить свой email.

Want to discuss the article? Join our Slack community (in English 🇺🇸🇬🇧) and Telegram chat (in Russian 🇷🇺)

Subscribe to our newsletter!