Episode #484

Table View Menus

Series: Working with Context Menus

8 minutes
Published on March 31, 2021

This video is only available to subscribers. Get access to this video and 572 others.

Another common use of context menus is with table views and collection views. In this episode we will explore adding a menu to a table view cell that allows copying a font or toggling it from a favorites list.

Another common use of context menus is with table views and collection views. Here I have a view controller that lists the fonts available on the system.

class FontListViewController: UITableViewController {
    var fonts: [UIFont]! {
        didSet {
            tableView.reloadData()
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "fontCell")
        fonts = UIFont.familyNames.compactMap { family in
            UIFont(name: family, size: 16)
        }
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        fonts.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "fontCell", for: indexPath)
        cell.textLabel?.text = fonts[indexPath.row].familyName
        cell.textLabel?.font = fonts[indexPath.row]
        return cell
    }
}

We can embed this simple view controller in our main view controller using an embed segue.

setting up the embed segue

override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
    let font = fonts[indexPath.row]
    return UIContextMenuConfiguration(identifier: id, previewProvider: nil) { _ -> UIMenu? in
        let copyAction = UIAction(title: "Copy", image: UIImage(named: "doc.on.clipboard"), identifier: nil) { _ in
            let font = self.fonts[indexPath.row]
            UIPasteboard.general.string = font.familyName
        }
        let menu = UIMenu(title: font.familyName, image: nil, identifier: nil, options: [], children: [copyAction])
        return menu
    }
}

embedded font list

This looks great. Let's now add a context menu to copy the selected font family name to the paste board.

We'll need to implement a method defined by UITableViewDelegate to provide the menu for a given row:

override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
}

Here we're given the index path as well as the point the user is tapping on, in case that is relevant for the given row. In our case it is not, so we can use the index path to find the font we wish to copy.

return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { suggestedActions -> UIMenu? in
    let font = self.fonts[indexPath.row]
    let copyAction = UIAction(title: "Copy", image: UIImage(systemName: "doc.on.doc"), identifier: nil, discoverabilityTitle: nil) { action in
        UIPasteboard.general.string = font.familyName
    }
    return UIMenu(title: font.familyName, image: nil, identifier: nil, options: [], children: [copyAction])
}

Now we can tap and hold on a row in the tableview and get the action to copy a font family.

copy menu

The approach is similar for collection views.

Next let's add the ability to toggle a favorite. First we'll create a rudimentary model:

private var favorites: Set<String> = []

Next we'll add the appropriate action based on whether the current font is a favorite or not.

let favoriteAction = UIAction(title: "Favorite", image: UIImage(systemName: "heart.fill"), identifier: nil, 
state: .off) { _ in
    self.favorites.insert(font.familyName)
}
let unfavoriteAction = UIAction(title: "Unfavorite", image: UIImage(systemName: "heart.slash.fill"),
identifier: nil, state: .on) { _ in
    self.favorites.remove(font.familyName)
}
let favAction: UIAction = self.favorites.contains(font.familyName) ? unfavoriteAction : favoriteAction
return UIMenu(title: font.familyName, image: nil, identifier: nil, options: [], children: [
                copyAction, favAction
])

Note the use of state as either .on or .off. This controls whether the menu item has a checkmark next to it or not.

Toggling favorites

This episode uses Swift 5.3, Xcode 12.4.