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:

Context-menu demo UIKit

Why Use Context Menus?

Context menus are great for keeping things tidy while still offering useful features. They:

  1. Keep your UI minimal
  2. Only show options when needed
  3. Feel natural with the iOS long-press gesture
  4. 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

  1. Start a new iOS App project (target iOS 16 or later).

  2. Select UIKit (instead of SwiftUI).

    In practice that means:

    1. Delete Main.storyboard from the project navigator.
    2. Remove the Storyboard Name reference under Application Scene Manifest → Scene Configuration in Info.plist.
    3. Create a UIWindow yourself in AppDelegate or SceneDelegate:
     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 UIActions. 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.