What’s new: Integrations Update, Connection Builder, Custom Parameters in Rule’s Push Payloads, and more.Let’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

By clicking on the «Allow all» button, you accept the use of cookies on this website. We use cookies to measure and analyze our traffic as well as for marketing purposes (e.g. for retargeting ads). You can edit your preferences at any time. For more information, refer to our privacy policy.