How to programmatically present a menu when single-tap a table view row, like in the iOS 16 Calendar app?

753 Views Asked by At

In the iOS 16 Calendar app, there is a new drop-down menu style for options like "repeat", when tapping any place of the row, a menu appeared. And there is a chevron up and chevron down icon at the right side of the table view cell.

How to do this in iOS 16? The context menu is triggered by long press, but this new style is by single-tap.

iOS 16 Calendar app create new event page with repeat option

5

There are 5 best solutions below

0
On

As of iOS 16, you can do it, albeit with a horizontally laid out edit menu, or, alternatively with long-press gesture, neither of which which the user may anticipate.

In the first case you provide the UIMenu to the edit menu delegate method, and in the latter, you use the context menu's delegate menu. For the horizontal menu, the shorter the menu names, the better, especially if you have more than 2 or 3 items, so the user doesn't have to click the right triangle at the end to expose more items.

The delegate methods respectively return a UIMenu, which you can create right in the delegate method, or you can set it up to reuse a mention that's triggered, perhaps with a pop-up UIButton() inside the tableView cell, that way you get better coverage in case the user doesn't tap the button itself.


Method 1: Custom 'edit' menu (e.g. present horizontal menu):
var editMenuInteraction : UIEditMenuInteraction! = nil

override func viewDidLoad() {
    editMenuInteraction = UIEditMenuInteraction(delegate: self)
}

extension MyViewController: UIEditMenuInteractionDelegate {
    
    func editMenuInteraction(_ interaction: UIEditMenuInteraction, menuFor configuration: UIEditMenuConfiguration, suggestedActions: [UIMenuElement]) -> UIMenu? {


        let myAction = UIAction(title: "Action1", image: UIImage(systemName: "bell"), identifier: nil) { action in
            print("MyAction tapped")
        }

        let anotherAction = UIAction(title: "Action2", image: UIImage(systemName: "person"), identifier: nil) { action in
            print("AnotherAction tapped")
        }

        .
        .
        .

        return UIMenu(title: "", children: [myAction, anotherAction])
    }
}

extension MyViewController : UITableViewDelegate {
   func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            cell.addInteraction(editMenuInteraction)
   }

   func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if let interaction = editMenuInteraction {
           let rect = tableView.rectForRow(at: indexPath)
           let absoluteRect = tableView.convert(rect, to: tableView.superview)
           let configuration = UIEditMenuConfiguration(identifier: nil, sourcePoint: absoluteRect.origin)
           interaction.presentEditMenu(with: configuration)
        }
    }
}

Method 2: Context Menu (e.g. long press gesture on cell)
// In your cellForRowAt tableView delegate method:

let interaction = UIContextMenuInteraction(delegate: self)
cell?.addInteraction(interaction)


extension MyViewController: UIContextMenuInteractionDelegate {
    func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
        return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { suggestedActions in
        
            let myAction = UIAction(title: "MyAction", image: UIImage(systemName: "bell"), identifier: nil) { action in
                print("MyAction tapped")
            }

            let anotherAction = UIAction(title: "AnotherAction", image: UIImage(systemName: "person"), identifier: nil) { action in
                print("AnotherAction tapped")
            }
         
            .
            .
            .

            return UIMenu(title: "", children: [myAction, anotherAction])
        }
    }
}
1
On

You can add UIButton display menu in the contentView of tableViewCell.

class TestCell: UITableViewCell {
  lazy var menuButton: UIButton = {
    let button = UIButton()
    button.showsMenuAsPrimaryAction = true
    // custom your menu
    button.menu = UIMenu(title: "menus", children: [])
    return button
  }()

  override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    super.init(style: .default, reuseIdentifier: reuseIdentifier)
    contentView.addSubview(menuButton)
  }
}
1
On

The question is asking about the popup menu being used in the Calendar app. The button is a popup button. In UIKit, this is implemented with UIButton. You need to set the button's menu, and set the showsMenuAsPrimaryAction and changesSelectionAsPrimaryAction properties to true.

Here is some sample code to create such a button and apply it to a table cell:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   let cell = ... // Create and setup your cell as needed

   // Create the popup button
   let cfg = UIButton.Configuration.plain()
   let button = UIButton(configuration: cfg)
   // Replace the following with your own code to support your own menu.
   // The important part here is that zero or one actions is set with a
   // state of on and the rest are set to off. This defines the initially
   // selected value, if any. The state of the actions are updated as the
   // user makes a selection.
   button.menu = UIMenu(options: [.singleSelection], children: [
        UIAction(title: "Option 1", state: self.option == .option1 ? .on : .off, handler: { [weak self] action in
            self?.option = .option1
        }),
        UIAction(title: "Option 2", state: self.option == .option2 ? .on : .off, handler: { [weak self] action in
            self?.option = .option2
        }),
    ])
    // After setting the menu, the following two properties signify that
    // the button is to look and act like a popup button. This includes
    // the up/down arrows on the right side of the button which is shown
    // by default for a popup button.
    button.showsMenuAsPrimaryAction = true
    button.changesSelectionAsPrimaryAction = true

    // This puts the button on the right end of the cell.
    cell.accessoryView = button

    return cell
}

The menu code shown above assumes there is some enum representing the various selectable options and that there is an option property for keeping track of the selected option. There are many possible ways and uses for the menu so I leave that to the needs of your app. The important part of this answer is showing the basic setup needed to replicate the popup button used in the Calendar app.

1
On

It seems this is SwiftUI only. I can't find changes in iOS 16 UIKit to achieve this. In SwiftUI:

import SwiftUI

struct SettingsView: View {
    @State private var selectedFlavor: Flavor = .chocolate

    var body: some View {
        List {
            Picker("Flavor", selection: $selectedFlavor) {
                Text("Chocolate").tag(Flavor.chocolate)
                Text("Vanilla").tag(Flavor.vanilla)
                Text("Strawberry").tag(Flavor.strawberry)
            }
        }
    }
}

struct SettingsView_Previews: PreviewProvider {
    static var previews: some View {
        SettingsView()
    }
}

enum Flavor: String, CaseIterable, Identifiable {
    case chocolate, vanilla, strawberry
    var id: Self { self }
}
2
On

You can add a UIButton to the TableViewCell and fill the entire space. Use UIButton's menu property to achieve the system calendar effect.

button.menu = menu

You can check the code I wrote, hope it helps you. https://github.com/zhi6w/TableViewCellWithMenuButton

enter image description here