8

Context Menus Tutorial for iOS: Getting Started [FREE]

 4 years ago
source link: https://www.raywenderlich.com/6328155-context-menus-tutorial-for-ios-getting-started
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

With the official release of iOS 13, we got a new, simple, powerful and clean user interface paradigm — context menus . Context menus replace the standard Peek and Pop interaction used until iOS 12 and take it a step further. When you tap and hold on a supported view, a context menu provides a preview of some content as well as a list of actions. They’re used widely throughout iOS, such as in the Photos app. Tapping and holding a photo presents a context menu like this:

Simulator-Screen-Shot-iPhone-8-2019-12-03-at-12.28.50-281x500.png

The list of actions is customizable as well as the preview. Tapping the preview will open the photo. You can customize what happens when tapping a preview in your own context menus.

In this tutorial, you’ll build context menus and push them to the limit by:

  • Configuring actions.
  • Setting images for actions using the new SF Symbols collection.
  • Simplifying menus with nested and inline submenus.
  • Building better, custom previews with relevant info.
  • Adding context menus to each item in a table view.

Exploring Vacation Spots

Context menus are all about making existing content noticeable and easy to access. You’ll add menus to an existing app, Vacation Spots , from another great tutorial: UIStackView Tutorial for iOS: Introducing Stack Views .

Download the sample project using the Download Materials button at the top or bottom of this tutorial, open the begin project in Xcode, run the app and start planning your next holiday. :]

When opening the app, you’ll see a table view with a collection of different destinations.

1.-VacationSpots-first-screen-281x500.png

Tapping a vacation spot shows important information about the destination. You can also add a rating of the spot, view it on the map or go to its Wikipedia page.

2.-VacationSpots-second-screen-281x500.png

Your First Context Menu

Tap the Submit Rating button when viewing a vacation spot.

3.-Adding-ratings-281x500.png

The app displays a few different buttons representing 1-5 stars. Tap your choice and then Submit Your Rating . The Submit Rating button now changes to Update Rating, along with your chosen score. Tapping it again lets you change or review your rating.

This could become a tedious process for some of the eager world travelers using our app. It’s a great candidate for your first context menu.

Back in Xcode, open SpotInfoViewController.swift , where you’ll add the context menu.

At the bottom of the file, add this extension:

// MARK: - UIContextMenuInteractionDelegate
extension SpotInfoViewController: UIContextMenuInteractionDelegate {
  func contextMenuInteraction(
    _ interaction: UIContextMenuInteraction,
    configurationForMenuAtLocation location: CGPoint)
      -> UIContextMenuConfiguration? {
    return UIContextMenuConfiguration(
      identifier: nil,
      previewProvider: nil,
      actionProvider: { _ in
        let children: [UIMenuElement] = []
        return UIMenu(title: "", children: children)
    })
  }
}

The UIContextMenuInteractionDelegate protocol is the key to building context menus. It comes with a single required method — contextMenuInteraction(_:configurationForMenuAtLocation:) , which you’ve just implemented by creating and returning a new UIContextMenuConfiguration object.

There’s a lot to go over, but once you’re through, you’ll understand the basics of context menus in iOS. The UIContextMenuConfiguration initializer takes three arguments:

  1. identifier : Use the identifier to keep track of multiple context menus.
  2. previewProvider : A closure that returns a UIViewController . If you set this to nil , the default preview will display for your menu, which is just the view you tapped. You’ll use this later to show a preview that’s more appealing to the eye.
  3. actionProvider : Each item in a context menu is an action. This closure is where you actually build your menu. You can build a UIMenu with UIAction s and nested UIMenu s. The closure takes an array of suggested actions provided by UIKit as an argument. This time, you’ll ignore it as your menu will have your own custom items.

Note : Context menus use a modern Swift interface with a lot more closures than is typical of UIKit. Most of the code you’ll write in this tutorial heavily utilizes closures. It’s also normal Swift style to use what’s known as trailing closure syntax , omitting the final parameter name in the call. You’ll see this with the remaining calls in this tutorial.

You may have noticed that you never create a context menu directly. Instead, you’ll always create a UIContextMenuConfiguration object that the system uses to configure the items in the menu.

Generally, the UIMenu used to create a context menu doesn’t need a title, so you provide it with a blank string. But, so far, this creates an empty menu. It would be much more useful menu if it had some actions in it.

Add this method below contextMenuInteraction(_:configurationForMenuAtLocation:) :

func makeRemoveRatingAction() -> UIAction {
  // 1
  var removeRatingAttributes = UIMenuElement.Attributes.destructive
  
  // 2
  if currentUserRating == 0 {
    removeRatingAttributes.insert(.disabled)
  }
  
  // 3
  let deleteImage = UIImage(systemName: "delete.left")
  
  // 4
  return UIAction(
    title: "Remove rating",
    image: deleteImage,
    identifier: nil,
    attributes: removeRatingAttributes) { _ in 
      self.currentUserRating = 0 
    }
}

makeRemoveRatingAction() creates a UIAction to remove a user’s rating. Later, you’ll add it as the first item in your context menu. Here’s what your code does, step by step:

  1. An action can have a set of attributes that affect its appearance and behavior. Because this is a delete action, you use the destructive menu element attribute.
  2. If currentUserRating is 0, it means the user has no rating. There’s nothing to delete, so you add the disabled attribute to disable the menu item.
  3. A UIAction can have an image, and iOS 13’s SF symbols look particularly good, so you use the UIImage(systemName:) initializer with a symbol name from the new SF Symbols app.
  4. Create and return a UIAction . It doesn’t need an identifier, as you won’t need to refer to it later. The handler closure will fire when the user taps this menu item.

Back in contextMenuInteraction(_:configurationForMenuAtLocation:) , replace the line declaring the children variable with:

let removeRating = self.makeRemoveRatingAction()
let children = [removeRating]

This creates the remove rating action and places it in the children array.

Great work, your context menu has a useful action now!

Next, add the following at the end of viewDidLoad() :

let interaction = UIContextMenuInteraction(delegate: self)
submitRatingButton.addInteraction(interaction)

To display a context menu when tapping and holding a view, you add a UIContextMenuInteraction to that view using the `addInteraction` method. This creates an interaction and adds it to submitRatingButton .

You’re finally ready to see the context menu in action!

Build and run the app. Tap and hold Update Rating . If you’ve already added a rating, you can remove it. If not, the Remove rating menu item is disabled.

4.-First-context-menu-281x500.png

It’s a start, but this humble context menu still has a long way to go. You’ve already been over the most important concepts of context menus:

  • UIContextMenuInteraction : Adds a context menu to a view.
  • UIContextMenuConfiguration : Builds a UIMenu with actions and configures its behavior.
  • UIContextMenuInteractionDelegate : Manages the lifecycle of the context menu, such as building the UIContextMenuConfiguration .

But what about more aesthetic concerns, like customizing the menu’s appearance?

Adding Submenus

Submenus are a great way to keep the context menu clean and organized. Use them to group related actions.

Add the following to the UIContextMenuInteractionDelegate extension at the bottom of SpotInfoViewController.swift :

func updateRating(from action: UIAction) {
  guard let number = Int(action.identifier.rawValue) else {
    return
  }
  currentUserRating = number
}

This method uses the identifier of a UIAction to update the user’s rating.

Like a UIContextMenuConfiguration , a UIAction can have an identifier. updateRating(from:) attempts to convert the action’s identifier into an Int and sets the currentUserRating accordingly.

Add the following method below updateRating(from:) :

func makeRateMenu() -> UIMenu {
  let ratingButtonTitles = ["Boring", "Meh", "It's OK", "Like It", "Fantastic!"]
  
  let rateActions = ratingButtonTitles
    .enumerated()
    .map { index, title in
      return UIAction(
        title: title,
        identifier: UIAction.Identifier("\(index + 1)"),
        handler: updateRating)
    }
  
  return UIMenu(
    title: "Rate...",
    image: UIImage(systemName: "star.circle"),
    children: rateActions)
}

This method creates UIAction s with identifiers matching each user rating: One through five. Remember, the handler for a UIAction is a closure that fires when tapping the item. This sets updateRating(from:) which you wrote previously as each action’s handler. Then, it returns a UIMenu with all the actions as the menu’s children.

If you look at the declaration of UIAction and UIMenu , they’re both child classes of UIMenuElement . The children parameter is of type [UIMenuElement] . That means that when you configure the context menu, you can add both actions or entire child menus.

Head back to contextMenuInteraction(_:configurationForMenuAtLocation:) , find the line declaring children and replace it with this:

let rateMenu = self.makeRateMenu()
let children = [rateMenu, removeRating]

Rather than adding five new items for each possible rating, you add the rateMenu as a submenu.

Build and run the app and test out your context menu by setting the user rating.

5.-Update-rating-submenu-1.gif

Inline Menus

The nested submenu cleans things up, but it means the user needs an extra tap to reach the actions.

To make things a little easier, you can display the menu inline. The displayInline menu option will show all the items in the root menu, but separated with a dividing line.

To achieve that, replace the end of makeRateMenu() that creates and returns a UIMenu with this:

return UIMenu(
  title: "Rate...",
  image: UIImage(systemName: "star.circle"),
  options: .displayInline,
  children: rateActions)

Except for adding the .displayInline menu option, it’s the same as what you had before.

Build and run the app to see the result:

6.-Update-rating-inline.png

Custom Previews

Context menus generally show a preview of the content. Right now, tapping and holding the Submit Rating button shows the Submit Rating button itself, or its Update Rating alter ego, as shown in the previous screenshot. It’s not exactly eye-appealing.

Next, you’ll set your own preview. Add the following method at the bottom of your UIContextMenuInteractionDelegate extension:

func makeRatePreview() -> UIViewController {
  let viewController = UIViewController()
  
  // 1
  let imageView = UIImageView(image: UIImage(named: "rating_star"))
  viewController.view = imageView
  
  // 2
  imageView.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
  imageView.translatesAutoresizingMaskIntoConstraints = false
  
  // 3
  viewController.preferredContentSize = imageView.frame.size
  
  return viewController
}

This makes a simple UIViewController that shows a rating star.

Here’s what’s happening, step-by-step:

  1. The app already has a rating_star image that’s used for stars in the app. Create a UIImageView with that image and set it as a blank UIViewController ‘s view.
  2. Set the frame of the image to specify its size. Setting the frame is enough, you don’t need any Auto Layout constraints set for you. Set translatesAutoresizingMaskIntoConstraints to false .
  3. You need to specify a preferredContentSize to show the view controller as a preview. If you don’t, it’ll take all the space available to it.

Back in contextMenuInteraction(_:configurationForMenuAtLocation:) , find the previewProvider parameter of your UIContextMenuConfiguration initializer. Replace the line with the following to pass in makeRatePreview as the preview provider:

previewProvider: makeRatePreview) { _ in

Build and run. When you tap and hold on the Submit Rating button, you should see the preview of the rating star:

7.-Custom-preview-281x500.png

Great work! That wraps everything up for this context menu. Now you’re ready for something completely different.

Context Menus in Table Views

Wouldn’t it be useful if tapping a vacation spot in the main table view showed a list of common actions? When viewing a vacation spot, the View Map button opens a map view on the vacation spot’s location:

8.-Map-view-281x500.png

By adding a View Map action in a context menu on the list of vacation spots, users can open the map before opening the spot info view controller. You’ll also add an action that makes it easy to share your favorite vacation spot with a friend. So far, you’ve learned the necessary steps to add a context menu to a view:

  1. Add a UIContextMenuInteraction to a view.
  2. Implement contextMenuInteraction(_:configurationForMenuAtLocation:) , the one required method of UIContextMenuInteractionDelegate .
  3. Build a UIContextMenuConfiguration with all your menu items.

The list of vacation spots exists in SpotsViewController as a table view. Each row in a table view is a view itself, or more specifically, a UITableViewCell .

To add a context menu to each row, you could do as you did it before — but there’s a more straightforward approach.

Open SpotsViewController.swift and add the following code at the bottom of the class:

// MARK: - UITableViewDelegate

override func tableView(
  _ tableView: UITableView,
  contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint)
    -> UIContextMenuConfiguration? {
  // 1
  let index = indexPath.row
  let vacationSpot = vacationSpots[index]
  
  // 2
  let identifier = "\(index)" as NSString
  
  return UIContextMenuConfiguration(
    identifier: identifier, 
    previewProvider: nil) { _ in
      // 3
      let mapAction = UIAction(
        title: "View map",
        image: UIImage(systemName: "map")) { _ in
          self.showMap(vacationSpot: vacationSpot)
      }
      
      // 4
      let shareAction = UIAction(
        title: "Share",
        image: UIImage(systemName: "square.and.arrow.up")) { _ in
          VacationSharer.share(vacationSpot: vacationSpot, in: self)
      }
      
      // 5
      return UIMenu(title: "", image: nil, children: [mapAction, shareAction])
  }
}

With a table view, adding a UIContextMenu to each row is as easy as implementing this method on UITableViewDelegate .

Tapping and holding any row will call tableView(_:contextMenuConfigurationForRowAt:point:) , allowing it to provide a context menu for the specific row.

You’re building a useful, functional menu for each row of the table view, so there’s a lot going on. In the above code, you:

  1. Get the vacation spot for the current row.
  2. Add an identifier to the context menu which you’ll use momentarily. You have to convert it to NSString , as the identifier needs to conform to NSCopying .
  3. The first action in the menu is for the map. Tapping this item calls showMap(vacationSpot:) , which opens the map view for this spot.
  4. Add another action to share the spot. VacationSharer.share(vacationSpot:in:) is a helper method that opens a share sheet.
  5. Finally, construct and return a UIMenu with both items.

And that’s all. Build and run the app, then tap and hold a vacation spot to try your new context menu.

9.-UITableView-context-menu-1.gif

Custom Previews in Table Views

Once again, the default preview leaves some room for improvement. The context menu just uses the entire table view cell as a preview.

In your first context menu, you used the previewProvider parameter of the UIContextMenuConfiguration to show a custom preview. previewProvider lets you create a whole new UIViewController as a preview. But, there’s another way.

Add the following UITableViewDelegate method:

override func tableView(_ tableView: UITableView,
  previewForHighlightingContextMenuWithConfiguration 
  configuration: UIContextMenuConfiguration)
    -> UITargetedPreview? {
  guard
    // 1
    let identifier = configuration.identifier as? String,
    let index = Int(identifier),
    // 2
    let cell = tableView.cellForRow(at: IndexPath(row: index, section: 0))
      as? VacationSpotCell
    else {
      return nil
  }
  
  // 3
  return UITargetedPreview(view: cell.thumbnailImageView)
}

Rather than creating a new UIViewController , this uses a UITargetedPreview to specify an existing view. Here’s what’s happening, step-by-step:

UIContextMenuConfiguration
UITargetedPreview

This tells the context menu to use the cell’s image view as a preview, rather than the entire cell.

Build and run to see your new preview:

10.-Preview-with-UITargetedPreview-281x500.png

That looks a lot better, doesn’t it? Now, tap the preview.

11.-Tapping-on-preview-1.gif

The list of vacation spots animates onto the screen again. Well, the purpose of a preview is to preview something. In this case, it would make sense to preview the spot info view controller for the vacation spot. You’ll address that next.

Handling Preview Actions

Add this final UITableViewDelegate method in SpotsViewController :

override func tableView(
  _ tableView: UITableView, willPerformPreviewActionForMenuWith
  configuration: UIContextMenuConfiguration,
  animator: UIContextMenuInteractionCommitAnimating) {
  // 1
  guard 
    let identifier = configuration.identifier as? String,
    let index = Int(identifier) 
    else {
      return
  }
  
  // 2
  let cell = tableView.cellForRow(at: IndexPath(row: index, section: 0))
  
  // 3
  animator.addCompletion {
    self.performSegue(
      withIdentifier: "showSpotInfoViewController",
      sender: cell)
  }
}

This UITableViewDelegate method fires when tapping a context menu’s preview. Tapping the preview dismisses the context menu, and tableView(_:willPerformPreviewActionForMenuWith:animator:) gives you a chance to run code when the animation completes. Here’s what’s going on:

identifier
animator

Build and run, and see what happens when you tap the preview. While you’re at it, have some fun using your new context menus, because you’ve finished the tutorial. Congratulations!

12.-Previewing-spot-info-1.gif

Where to Go From Here?

In this tutorial, you improved the user experience of an existing app by:

  • Adding two distinct context menus: One on a button and one on each cell of a table view.
  • Adding useful actions to the menus.
  • Using submenus, which can keep things organized.
  • Customizing the menus further by adding your own previews.
  • Handling taps on the preview.

Context menus also interact seamlessly with drag and drop. You can learn more by watching WWDC 2019’s Modernizing Your UI for iOS 13 video.

Context menus are a different beast in SwiftUI, but much simpler. Why not get a taste with our SwiftUI: Getting Started tutorial?

I hope you’ve enjoyed this tutorial on context menus in UIKit. If you have any questions, feel free to leave them in the discussion forum below.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK