Apphud – Integrate, analyze and improve subscriptions in your iOS app
Development

How to implement context menus in iOS 13

In WWDC 2019 and iOS 13 Apple introduced a new way to interact with your app: context menu. They look like this:

Context menu for UIImageView in iOS 13
Context menu for UIImageView in iOS 13

In this article I write about how to implement them.

Context menus is a logical continuation of “Peek and Pop” technology where a user may open some element’s preview using 3D touch. But there are several differences between them.

  • Context menus are available on any device on iOS 13 or later. 3D touch support is not required. Thus, you can implement them on iPads.
  • Action buttons appear immediately and don’t require swipe gesture.

In order to open context menu, user should touch and keep a finger on some element or press this element hard (if a device supports 3D touch).

How to use context menus?

In Human Interface Guidelines Apple recommends following several rules while prototyping context menus.

Adopt context menus consistently

It won’t be good if you add context menu for some elements somewhere and – don’t for other similar elements elsewhere. User may think that your app works incorrectly.

Include only necessary buttons to a context menu

Context menu is a great place for the most frequently used commands. “Most frequently used” is a key phrase. Don’t add everything into a menu.

Use submenus

Use submenus, so it would be easier for a user to find necessary command. Apply simple and clear names to commands.

Don’t use more than one level of submenus

Despite the fact that the submenus can make navigation easier, they can easily complicate it. Apple doesn’t recommend to use more than one level of submenus.

Put the most frequently used commands to the top of context menu

People mostly focus on the top of a menu, so it is wise to keep the most important commands at the top.

Group menu items

Use grouping to combine similar actions.

Don’t use context and edit menus for one UI element at the same time

There might be a conflict between them, because both of them are being triggered with a long tap.

Edit menu
Edit menu

Don’t add “Open” button to the menu

Users can open the element with a single tap. “Open” button will be redundant in this case.

Implementation of a simple context menu for UIView

Now let’s move to programming. Context menus are available for iOS 13+. You will also need Xcode 11.

You may download a full source code here.

Let’s add a context menu for UIImageView, like in animation shown at the top of this article.

You should put UIImageView object to the controller’s view and write a few lines of code in viewDidLoad method:

class SingleViewController: UIViewController {
    @IBOutlet var imageView: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()

        imageView.isUserInteractionEnabled = true

        let interaction = UIContextMenuInteraction(delegate: self)
        imageView.addInteraction(interaction)
    }
}

Firstly, we create an object of UIContextMenuInteraction class. We provide a context menu’s delegate while initializing. We’ll look into it later. Using addInteraction method we add our menu to the image.

It’s left to implement UIContextMenuInteractionDelegate protocol now. There is only one required method, where we build our context menu:

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

If we returned nil, context menu wouldn’t be created. We create an object of UIContextMenuConfiguration class inside this method. We pass the following parameters during initialization:

  • identifier – context menu’s identifier.
  • previewProvider – custom controller that may be optionally shown instead of selected element. We’ll look into it later.
  • we pass elements of a menu into actionProvider.

It’s very easy to create a menu’s items: you should provide a name, optional icon and a handler. That’s all!

Adding a submenu

Let’s complicate a task. I want to add a context menu with 2 items: “Save” and “Edit…”. If user taps “Edit…” button, a submenu with “Rotate” and “Delete” items should be shown. The result looks like this:

Submenu in a context menu
Submenu in a context menu

To implement this we should rewrite UIContextMenuInteractionDelegate in this way:

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
}

Here we create “Save”, “Rotate” and “Delete” buttons, add the last two buttons into “Edit…” submenu and put everything to the main context menu.

Creating a context menu for UICollectionView

Let’s add a context menu into UICollectionView. A user will see a menu with “Archive” button, like this:

Context menu in UICollectionView
Context menu in UICollectionView

To implement this you should implement optional method func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? of UICollectionViewDelegate protocol. Like this:

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
}

Here we create an action and a menu. Now a user will see a context menu after a long tap.

Adding a menu to UITableView

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
}

But what if we want to show custom preview screen, like this:

Context menu in UITableView
Context menu in UITableView

To do this, we should pass necessary UIViewController into previewProvider while creating UIContextMenuConfiguration:

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

In this example PreviewViewController is being initialized from a storyboard.

Let’s implement a tap onto this ViewController. We should implement willCommitMenuWithAnimator method of UITableViewDelegate protocol. We will put the handler inside animator.addCompletion:

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

Conclusion

Context menu is a new powerful way to interact with your app. And, as you can see, it doesn’t take much time to implement it. But don’t forget that their implementation may be changed while iOS 13 is not being released.