In this post we’re going to add context menus to a UIKit UITableView
. The end goal is to have something that looks like this:
Why Use Context Menus?
Context menus are great for keeping things tidy while still offering useful features. They:
- Keep your UI minimal
- Only show options when needed
- Feel natural with the iOS long-press gesture
- Save you adding extra buttons all over the UI – the menu keeps them tucked out of sight until needed.
What We’re Making
We’ll show a short list of fruit. When we long-press any item we will show a context menu with three options:
- Favorite – toggles a star
- Delete – removes the row
- Share – brings up the system share sheet
That’s it.
Getting Set Up
Start a new iOS App project (target iOS 16 or later).
Select UIKit (instead of SwiftUI).
In practice that means:
- Delete Main.storyboard from the project navigator.
- Remove the Storyboard Name reference under Application Scene Manifest → Scene Configuration in Info.plist.
- Create a
UIWindow
yourself inAppDelegate
orSceneDelegate
:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } let window = UIWindow(windowScene: windowScene) window.rootViewController = UINavigationController(rootViewController: FruitTableViewController()) self.window = window window.makeKeyAndVisible() }
Those three steps are standard when using programmatic UIKit.
If you’re working in an older project, iOS 13 is the minimum for context menus, but iOS 16’s life cycle setup is nicer.
The Model
struct Fruit: Identifiable {
let id = UUID() // Unique per fruit - `Identifiable`
var name: String // What the user will see in the cell
var isFavorite: Bool = false // Toggle via the context menu
}
First we build a small lightweight model of Fruit
, making sure it is Identifiable
. The name
and isFavorite
property will be used to in our context menu.
Building the Table View
final class FruitTableViewController: UITableViewController {
private var fruits: [Fruit] = [
.init(name: "🍎 Apple"),
.init(name: "🍌 Banana"),
.init(name: "🍑 Peach"),
.init(name: "🍍 Pineapple")
]
override func viewDidLoad() {
super.viewDidLoad()
title = "Fruit Basket"
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
fruits.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let fruit = fruits[indexPath.row]
var content = cell.defaultContentConfiguration()
content.text = fruit.name
cell.contentConfiguration = content
cell.accessoryType = fruit.isFavorite ? .checkmark : .none
return cell
}
}
Next we add a very simple UITableViewController
with four rows of fruit.
Adding the Context Menu
From iOS 13 onwards, UITableView
has native support for context menus. Just override one method:
extension FruitTableViewController {
override func tableView(_ tableView: UITableView,
contextMenuConfigurationForRowAt indexPath: IndexPath,
point: CGPoint) -> UIContextMenuConfiguration? {
UIContextMenuConfiguration(identifier: indexPath as NSIndexPath) {
self.preview(for: indexPath)
} actionProvider: { _ in
self.menu(for: indexPath)
}
}
}
That’s all it takes to hook up the context menu to the UITableView
.
The Menu Itself
extension FruitTableViewController {
// Build and return a context menu for the pressed row
private func menu(for indexPath: IndexPath) -> UIMenu {
// Grab the backing model for the tapped row
let fruit = fruits[indexPath.row]
// 1. Toggle the favourite star
let favorite = UIAction(
title: fruit.isFavorite ? "Unfavorite" : "Favorite",
image: UIImage(systemName: fruit.isFavorite ? "star.slash" : "star")) { _ in
self.toggleFavorite(at: indexPath) // flip the bool + reload row
}
// 2. Delete the row – '.destructive' makes it red
let delete = UIAction(
title: "Delete",
image: UIImage(systemName: "trash"),
attributes: .destructive) { _ in
self.delete(at: indexPath) // delete from data + table
}
// 3. Share the fruit name via UIActivityViewController
let share = UIAction(
title: "Share",
image: UIImage(systemName: "square.and.arrow.up")) { _ in
self.share(fruit) // start the share sheet
}
// Package the actions into a menu and return it
return UIMenu(title: "", children: [favorite, delete, share])
}
// Return nil to skip the preview step (perfectly fine for simple lists)
private func preview(for indexPath: IndexPath) -> UIViewController? {
nil
}
}
Here’s what’s going on: we create three UIAction
s. Each UIAction
represents an item in the menu. We have one to toggle the favourite flag, one to delete, and one to share.
The .destructive
attribute hands UIKit the job of colouring the Delete option red, matching native styles.
Finally, we bundle the actions into a single UIMenu
and hand it back to the system.
preview(for:)
give you a chance to show something with the context menu—think a larger image, a detail card, or anything that helps the user commit to an action.For example, if this table listed photos rather than fruit, you could return a larger version of the tapped image so the user can decide whether to share, favourite, or bin it.
Our list is super simple so we will return
nil
. The menu appears instantly.
Updating the Data
extension FruitTableViewController {
private func toggleFavorite(at indexPath: IndexPath) {
fruits[indexPath.row].isFavorite.toggle()
tableView.reloadRows(at: [indexPath], with: .automatic)
}
private func delete(at indexPath: IndexPath) {
fruits.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
}
}
Toggling favourites (yes I am using American spelling code but English here 🇬🇧) shows a checkmark, and deletes animate as expected.
Sharing a Fruit
extension FruitTableViewController {
private func share(_ fruit: Fruit) {
let activity = UIActivityViewController(activityItems: [fruit.name], applicationActivities: nil)
present(activity, animated: true)
}
}
These few lines give a FruitTableViewController
to ability to show the system share sheet.
Bonus: Collection View Support
Switching to a grid? UICollectionView
supports context menus too. Just conform to UICollectionViewDelegate
and implement:
collectionView(_:contextMenuConfigurationForItemsAt:point:)
The menu and actions work the same way.
The delegate method works in a very similar way to so you can copy-paste the menu(for:)
and preview(for:)
helpers.
The biggest difference is dealing with items rather than rows.
Wrap-Up
Context menus are quite straightforward to add to UIKit apps. They instantly give your app a more native feel and delight users. Give it a try.
Happy coding.