What’s new in Apphud: MRR/ARR Movement Charts, Experiments Improvements, Unity SDK ReleaseLet’s see
Apphud
Why Apphud?PricingContact
Ren
Ren
July 09, 2019
2 min read

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

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

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

Они выглядят так:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Простейшее контекстное меню для <code class='inline-code'>UIView</code>

Теперь, когда мы усвоили основные правила использования контекстных меню, перейдем к практике. Разумеется, меню работают только на 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Контекстные меню в 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 так:

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
}

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

Контекстные меню в 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
    }
}

Заключение

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

Ren
Ren
Co-founder at Apphud
Ex iOS app and game developer. 11 years in the industry since iOS 3. More than 50 apps are in the background with 4 exits. Entrepreneur and traveler.

Related Posts