Context Menus for Tables

Starting in iOS 16, you can customise the context menus you add to lists and tables based on what’s selected.

Last updated: Mar 29, 2023

Context Menus

Apple added context menus back in iOS 13 and macOS 10.15. You can add a context menu to any view. The way a user sees the menu depends on the platform:

  • A touch and hold (long press) gesture on iOS.
  • A control-click using a mouse on macOS and iPadOS.
  • A secondary click on a trackpad on macOS or iPadOS.

You can add sub-menus and sections to the context menu though the Human Interface Guidelines suggest you aim for a small number of menu items and no more than one level of submenu. For example, adding a context menu to a row in a SwiftUI list:

CountryCell(country: country)
.contextMenu {
    faveButton
    shareButton
    deleteButton
}

A context menu for table view cell showing favorite, share and delete actions

You can add a context menu to a table or list row but there was no way, until iOS 16, to take into account the number of items selected.

Item Based Context Menus

In iOS 16, Apple added a new item based context menu. This context menu only works when added to a container like a table or list that supports selection. If you add this type of context menu to a view hierarchy that doesn’t support selection it never activates.

Let’s add item-based context menus to my SwiftUI table example:

@State private var selected = Set<Country.ID>()

var body: some View {
  Table(store.countries, selection: $selected) {
    TableColumn("Name", value: \.name)
    ...
  }
  .contextMenu(forSelectionType: Country.ID.self) { items in
     // menu items...
  }
}

The first parameter to the context menu is the type of the identifier for the items shown in the table. It must match the type you’re using to manage the selection state. In my case, that’s the type of the country identifier.

The closure that produces the menu receives the set of items to act on. This is not always the same as the set of selected items. For example, tapping or clicking on an empty part of the container calls the closure with an empty set even if you have selected items. On iPadOS, Apple recommends you show a context menu in that case to create new items:

.contextMenu(forSelectionType: Country.ID.self) { items in
  if items.isEmpty {
    Button { } label: {
      Label("New country", systemImage: "plus")
    }
  }
}

Context menu with add new country action

We can now show a different context menu if the user selects a single item or multiple items:

.contextMenu(forSelectionType: Country.ID.self) { items in
  if items.isEmpty {
    Button { } label: {
      Label("New country", systemImage: "plus")
    }
  } else if items.count == 1 {
    Button {} label: { 
      Label("Share", systemImage: "square.and.arrow.up")
    }
    Button {} label: { 
      Label("Favourite", systemImage: "heart")
    }
    Button(role: .destructive) {} label: {
      Label("Delete", systemImage: "trash")
    }
  } else {
    Button {} label: {
      Label("Share Selected", systemImage: "square.and.arrow.up")
    }
    Button {} label: {
      Label("Favourite Selected", systemImage: "heart")
    }
    Button(role: .destructive) {} label: {
      Label("Delete Selected", systemImage: "trash")
    }
  }
} 

The context menu item with a single item selected:

Context menu with single item selected showing share, favourite and delete actions

And with five items selected:

Context menu with five items selected showing share selected, favourite selected and delete selected actions

Edit Mode

Since the item based context menus rely on selecting items I should mention some changes to edit mode in iOS 16. Edit mode for a table or list allows you to make multiple selections by tapping on individual rows. In iOS 16, this is mostly only needed when working without a keyboard. If you have a keyboard connected the ⌘ and ⇧ keys allow you to select multiple rows.

Edit mode with three rows selected

A two-finger swipe gesture will also allow you to enter edit mode though it’s probably still a good idea to include the edit button in the toolbar:

.toolbar(id: "table.toolbar") {
  ToolbarItem(id: "table.edit", placement: .navigationBarLeading) {
    EditButton()
  }
}

Primary Action

One final option is to add a primary action for the context menu. On macOS the primary action runs when the user double clicks on a row. On iOS, the primary actions runs when the user single taps on a row.

.contextMenu(forSelectionType: Country.ID.self) { items in
  ...
} primaryAction: { items in
  store.favourite(items)
}

Note: On iOS, this means you can no longer select a row by tapping on it as that immediately activates the primary action. If you want to select a row without running the primary action use edit mode or the keyboard.