Building Custom Menus for iOS Applications: A Comprehensive Tutorial
ShowNote initially featured an object editing option through the menu, utilizing UIMenuController. However, it is important to note that this approach has been deprecated since iOS 16.0.
So, I had to shift my focus to studying UIEditMenuInteraction as an alternative for displaying menus with UIKit.
UIEditMenuInteraction
I defined a menu member variable.
var menu: UIEditMenuInteraction!
The constructor of the menu needs a delegate.
So, I assigned the delegate to the view controller. (I will delve into the implementation details later)
extension MainViewController: UIEditMenuInteractionDelegate {
//
}
You can add interaction to any Views
I pressed the button but nothing happened. How do I present the context menu? 🤔 To show the menu, you should call presentEditMenu
.
and make a configuration as well.
I can’t see anything even calling this method.
menu.presentEditMenu(with: menuConfig)
and I saw this error emitted on the console log.
Delegate
How can we specify an action for the menu? How can I create a menu?
You would remember that I inherited UIEditMenuInteractionDelegate
.
This delegate has a method menuFor
. We can create a menu for this view controller.
extension MainViewController: UIEditMenuInteractionDelegate {
func editMenuInteraction(_ interaction: UIEditMenuInteraction, menuFor configuration: UIEditMenuConfiguration, suggestedActions: [UIMenuElement]) -> UIMenu? {
}
}
I attempted to discover how to define commands
or actions for the menu.
Let’s examine the constructor of the menu. I inferred that using children
is the only way to solve this problem.
However I couldn’t see any special in the constructor.
However, I couldn’t find anything specific in the constructor. There is no way to set a command or action for the UIMenuElement
either.
@available(iOS 13.0, *)
@MainActor open class UIMenuElement : NSObject, NSCopying, NSSecureCoding {
/// The element's title.
open var title: String { get }
/// The element's subtitle.
@available(iOS 15.0, *)
open var subtitle: String?
/// Image to be displayed alongside the element's title.
open var image: UIImage? { get }
public init?(coder: NSCoder)
}
UIAction
So I searched for another class inherited from UIMenuElement
and found it.
Now I could see menus using UIAction.
let actions: [UIAction] = [.init(title: "Copy") { act in
//
}]
let menu: UIMenu = .init(title: "menu",
children: actions)
return menu
Rect
However, the menu was presented in an unexpected location.
How can we specify the location of the menu? The delegate has a method to set the menu’s location as well. Initially, I assumed that I could position the menu in a manner similar to a popover.
func editMenuInteraction(_ interaction: UIEditMenuInteraction, targetRectFor configuration: UIEditMenuConfiguration) -> CGRect {
let rect = self.button.frame(in: self.view) ?? .zero
return rect.
}
Origin
I also attempted to set the menu’s location using the view’s center, but the result remained the same.
return .init(origin: self.view.center, size: .zero)
I didn’t understand why or how I had to do it. I tested by setting the origin to (0,0), and the menu is presented like this:
return .init(origin: .zero, size: .zero)
It seems like the origin of the menu is the origin of the button. But how can the menu know from where it was triggered? It was added through the addInteraction
method on the button.
button.addInteraction(menu)
If you want to present the menu centered horizontally, set the x-coordinate with half of the width, like this:
return .init(origin: .init(x: button.frame.width / 2, y: 0), size: .zero)
Size
What is the size for? Let’s say the button is located at the top. The menu will be presented like this:
If the height is the height of the button?
You might already know if you have used a popover. The size means specifying an area not to present the menu. If the button is located at the right edge and the menu title is long, iOS will move the menu to see the entire content automatically.
Of course, multiple menus would be shown properly as well.
Icon
I tried to set icons for the menu like this:
let actions: [UIAction] = [.init(title: "Copy", image: .init(systemName: "doc.on.doc")) { act in
//
}, .init(title: "Remove", image: .init(systemName: "trash.slash")) { act in
//
}, .init(title: "Clone", image: .init(systemName: "doc.badge.plus")) { act in
//
}]
Even when I made the menu a submenu, I couldn’t see the images because UIEditMenuInteraction doesn’t support images.
To present icons, you should use UIContextMenuInteraction instead. However, I won’t cover that in this article.
The full source is in this example repository.
SwiftUI
UIEditMenuInteraction is for UIKit, while Menu is for SwiftUI. In SwiftUI, a Button is not required to present a menu because the Menu itself works as a replacement for a Button
.
VStack {
Menu("Show Menu") {
Button("Copy") {
//
}
}
}
SwiftUI Menu is easy to use, but it is presented like a Context Menu, not an Edit Menu.
Menu("Show Menu") {
Button("Copy") {
//
}
Button("Remove") {
//
}
Button("Clone") {
//
}
}
Nested Menu
Nested menus are also easy to implement.
Menu("Show Menu") {
Button("Copy") {
//
}
Menu("Remove") {
Button("Remove to Trash") {
//
}
Button("Remove only Reference") {
//
}
}
Button("Clone") {
//
}
}
I also could add Icon
Menu("Show Menu") {
Button("Copy", systemImage: "doc.on.doc") {
//
}
Everything is okay? Hmm… something seems off. Copy is the first menu, but why is it presented at the bottom?
Order
The Menu has a menuOrder
modifier. I can have the first menu presented at the top with it.
Default order means to put first item at closest to location of user’s finger.
The full source is in this example repository.
If you found this post helpful, please give it a round of applause 👏. Explore more iOS-related content in my other posts.
For additional insights and updates, check out my LinkedIn profile. Thank you for your support!
Troubleshootings
did not have performable commands and/or actions; ignoring present.
extension MainViewController: UIEditMenuInteractionDelegate {
func editMenuInteraction(_ interaction: UIEditMenuInteraction, menuFor configuration: UIEditMenuConfiguration, suggestedActions: [UIMenuElement]) -> UIMenu? {
return .init...
}
}
References
https://www.hackingwithswift.com/quick-start/swiftui/how-to-show-a-menu-when-a-button-is-pressed