Figure

A blog about Swift and iOS development.

Custom Menu Items for Table View Cells

Adding our own items to a table view cell's pop-up menu is actually pretty easy. But the documentation can be tricky to track down, and there's one weird gottcha, so let's break it down.

TL;DR

Check out the documentation for UIMenuController

Still TL; Still DR

There's a sample project for you to peruse on GitHub.

The Scenic Route

Adding a standard pop-up menu to our table view cells is a simple matter of implementing three methods in our table view delegate:

override func tableView(tableView: UITableView,
shouldShowMenuForRowAtIndexPath indexPath:
NSIndexPath) -> Bool { ... }

override func tableView(tableView: UITableView,
canPerformAction action: Selector,
forRowAtIndexPath indexPath: NSIndexPath,
withSender sender: AnyObject?) -> Bool { ... }

override func tableView(tableView: UITableView,
performAction action: Selector,
forRowAtIndexPath indexPath: NSIndexPath,
withSender sender: AnyObject?) { ... }

This will give us access to cut, copy, and paste (with selectors of cut:, copy:, and paste:, natch) right out of the box. But what if we want to add or own? UITableViewDelegate's documentation for these methods seem to indicate copy and paste are the only actions available to us.

That's where UIMenuController comes in. We can use it to add our own UIMenuItems to the list of possibilities sent to tableView(_: canPerformAction:...) and tableView(_: performAction:...).

Creating a menu item is straight-forward:

let item = UIMenuItem(title: "My Item",
  action: Selector("myItem:"))

And we work with UIMenuController through its singleton instance:

let menu = UIMenuController.sharedMenuController()

This means we can add a menu item to it wherever we like, but it should be someplace that only gets called once (if we don't want our menu getting flooded with duplicate items). The app delegate's application(_: didFinishLaunchingWithOptions:) is one such place.

We could just assign our item to UIMenuController's menuItems property. But because it's a singleton, we really can't reason about what might or might not have been added to it already. So we take measures to ensure we preserve any existing items:

var newItems = menuController.menuItems 
  ?? [UIMenuItem]()
newItems.append(item)
menu.menuItems = newItems

Gottcha

At this point, we'll see our custom action selector, "myItem:", sent to tableView(_: canPerformAction:...), and we might think we're done.

But wait, our menu item still isn't showing up? What's going on?

Here's the thing: canPerformAction... gets sent the selector, but tableView(_: performAction:...) never does. As far as the menu system is concerned, nothing responds to our action's selector, so it doesn't get displayed.

The solution is to step outside of the table's delegate setup and work directly with our cell's custom class. If we implement the selector as a method there, it will be found by the menu system and called whenever our custom item is tapped:

//In our table cell's custom class
func myItem(sender:AnyObject?){
  //handle menu tap here.
}

We might think that means we can leave out tableView(_: performAction:...) all together! Nope. It still needs to be there or the menu won't get displayed regardless of what item actions we actually care about.

So there we go! As long as:

  • tableView(_: shouldShowMenuForRowAtIndexPath:) returns true for the cell at the given index path, and
  • tableView(_: canPerformAction:...) returns true for your custom item's selector for the cell at the given index path, and
  • tableView(_: performAction:...) exists, and
  • The cell at the given index path implements a method with our action's selector

our custom menu item will appear and do the Right Thing.


Hit me up on twitter (@jemmons) to continue the conversation.

Latest | Archives