Episode #482

Basic Context Menus

Series: Working with Context Menus

12 minutes
Published on March 29, 2021
Context menus are a great affordance for performing related actions to a UI element. Users can tap and hold to view the context menu, and the gesture is consistent across the OS so users will likely already be familiar with it. In this episode we'll show how to set up a basic context menu with a custom preview with normal and destructive actions.

Context menus are a great affordance for performing related actions to a UI element. Users can tap and hold to view the context menu, and the gesture is consistent across the OS so users will likely already be familiar with it.

Let's see how we can add a basic context menu to this avatar view to set a custom avatar.

First we need to add a context menu interaction:

private func setupContextMenu() {
    let interaction = UIContextMenuInteraction(delegate: self)
    avatarImageView.isUserInteractionEnabled = true
    avatarImageView.addInteraction(interaction)
}

Note that we have to enabled user interaction on our image view, otherwise it won't receive any touch events. This is typically only true for UIImageView, but if you find a view is not responding to the tap-and-hold gesture, make sure that this property is set to true.

Next we need to conform our view controller to the UIContextMenuInteractionDelegate. This allows us to define the menu on-demand, which is useful if the data we need to display is dynamic.

extension ViewController: UIContextMenuInteractionDelegate {
    func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {

    }
}

The only required method is one that asks for a UIContextMenuConfiguration. Since this method could be used to serve up many different context menus in our view controller, we'll first check which view this was for.

        guard interaction.view === avatarImageView else { return nil }

Next we'll create a UIContextMenuConfiguration, which accepts a couple of params (that will be nil for now) as well as a block that is used to build the menu.

return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (suggestedActions) -> UIMenu? in
    let chooseAvatar = UIAction(title: "Choose Avatar", image: UIImage(systemName: "pencil"), identifier:
nil) { _ in
        self.selectAvatar()
    }

    return UIMenu(title: "Avatar", image: nil, identifier: nil, options: [], children: [
        chooseAvatar
    ])

Next we can utilize UIImagePickerController to select a photo from the library and use that as an avatar.

select an avatar from the photo library

This works, but it would be nice to be able to remove that avatar as well. Let's add another context menu action to remove it.

let removeAvatar = UIAction(title: "Remove", image: UIImage(systemName: "trash"), identifier: nil,
attributes: [.destructive]) { _ in
    self.setDefaultAvatar()
}

// don't forget to add it as a child
return UIMenu(title: "Avatar", image: nil, identifier: nil, options: [], children: [
    chooseAvatar, removeAvatar
])

Note that we used the .destructive attribute, so our menu item becomes red.

Destructive style shows red

And tapping the Remove button will remove our custom avatar. Awesome!

Ok one last thing that is bugging me is the white rectangle around our avatar view. Let's see how we can remove that.

Back in the UIContextMenuInteractionDelegate we can implement another method, one that asks for a preview for highlighting:

func contextMenuInteraction(_ interaction: UIContextMenuInteraction,
previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration) ->
UITargetedPreview? {
    let params = UIPreviewParameters()
    params.backgroundColor = .clear
    let preview = UITargetedPreview(view: avatarImageView, parameters: params)
    return preview
}

Here we can specify custom parameters like shadow path, visible shape, and background color, as well as whatever view we want to show in the highlighted state. Setting the background to .clear and using our existing avatarImageView works great, and now we don't have that white rectangle.

Providing a preview with a clear background

So that's the basics of how context menus work. In the next few episodes we will explore some more advanced topics and see what else we can do with these context menus. Stay tuned!

This episode uses Swift 5.3, Xcode 12.4.