How to Make Menu Buttons Easy to Tap in UIKit and SwiftUI

How to Make Menu Buttons Easy to Tap in UIKit and SwiftUI
Menu buttons in Swift are hard to tap

Previously in UIKit it felt like default UIButton's worked great out of the box. If you used system buttons they came with opacity animations when you tapped on them. It didn't require extra work to configure the touch interaction or size.

It is not as easy in SwiftUI to have great touch experiences. You can create tiny, hard to tap Button's without realizing it. I see a lot of developers using TapGesture instead of the Button, which loses out on the accessibility features that Button provides.

Recently, I was having a difficult time tapping on a SF Symbol based button in my Brew Coffee app. If I used a normal UIBarButtonItem it worked fine, but when I configured a UIMenu it failed to tap easily. I had a lot more miss-taps unless I was directly on top of the tiny 24x24 icon.

Brew Coffee Menu Button using UIButton with UIMenu in a UIBarButtonItem

You can solve this problem, but it requires more code to make it feel right. I've submitted #FB15347053 to Apple (Menu Button as UIBarButtonItem or Menu in SwiftUI Are Hard to Tap).

Here are my workarounds for both UIKit and SwiftUI:

UIKit Solution: UIMenu and UIBarButtonItem are Hard to Tap

To get the effect in UIKit, you need to wrap a UIButton in a UIBarButtonItem. It feels like more work than it should be to make a tab bar button easy to tap.

So far in my testing, the backgroundColor is required for the tap region to work across the background of a Menu based button. The collision detection logic seems different from standard UIButton's tap behavior.

I don't love setting the color, hopefully UIColor.systemBackground doesn't clash with your menu bar color. An alternative may be to create a custom UIButton subclass that implemented the hitTest() method to include transparent regions.

let menuButton = UIBarButtonItem(title: "Menu", image: UIImage(systemName: "line.3.horizontal"), primaryAction: nil, menu: menu)
self.navigationItem.leftBarButtonItem = menuButton

// Create a UIButton
let button = UIButton(type: .system)
button.setImage(UIImage(systemName: "line.3.horizontal"), for: .normal)

// Adjust button size
button.frame = CGRect(x: 0, y: 0, width: 44, height: 44) // Customize size as needed
button.backgroundColor = UIColor.systemBackground // Must set a color to get button size to be respected
//        button.backgroundColor = .blue // Must set a color to get button size to be respected

// Alternatively you could increase size with Auto Layout
//        button.translatesAutoresizingMaskIntoConstraints = false
//        button.addConstraints([
//            button.widthAnchor.constraint(equalToConstant: 44),
//            button.heightAnchor.constraint(equalToConstant: 44)
//        ])

// Setup the menu
let menu = UIMenu(title: "", children: [
    UIAction(title: "Action 1", image: UIImage(systemName: "star"), handler: { _ in
        print("Action 1 tapped")
    }),
    UIAction(title: "Action 2", image: UIImage(systemName: "heart"), handler: { _ in
        print("Action 2 tapped")
    })
])

// Assign the menu to the button
button.menu = menu
button.showsMenuAsPrimaryAction = true

// Wrap the UIButton in a UIBarButtonItem
let barButtonItem = UIBarButtonItem(customView: button)
self.navigationItem.leftBarButtonItem = barButtonItem

Now your button tap region should match the size you set for the width and height.

SwiftUI Problem: Menu Button Text is Hard to Tap

In SwiftUI you should avoid the Menu(_:content:) APIs that will make the tap area very small with text. It will not have any horizontal or vertical padding, thus making it very hard to tap.

Menu("Options") {
    Button("Order Now", action: placeOrder)

    Button("Adjust Order", action: adjustOrder)
    Button("Cancel", action: cancelOrder)

}
.padding() // No effect on the tap size, must use alternate API
.contentShape(Rectangle())
.background(Color.red)

Transparent regions in SwiftUI are not tappable by default, so you need to make sure to set either the clipShape() or contentShape() to make buttons tap across the entire surface.

SwiftUI Solution: Custom Menu Buttons

You should use the Menu(content:label:) API because that allows you to provide a customized label with padding.

Menu {
    Button("Order Now", action: placeOrder)
    Button("Adjust Order", action: adjustOrder)
    Button("Cancel", action: cancelOrder)
} label: {
    Image(systemName: "ellipsis.circle") // Customize the size
        .resizable()
        .frame(width: 24, height: 24)
        .padding(16)
        .background(Color.clear)
        .contentShape(Rectangle()) // Makes entire region tappable
}

Now you will have easy to tap buttons. You can make your buttons 50+ points wide to make them easier to tap, but that can extend the button size that may displace adjacent buttons (if you support multiple buttons in a row).

What is Causing the Tap Issues in SwiftUI?

To me, the SwiftUI tap behavior on transparent pixels has been a regression in usability because it's easy to create UI elements that are not easy to touch with real fingers. If you only test with a mouse in the iOS Simulator, you will not catch these real world issues.

Without this attention to detail for SwiftUI buttons your users will need to tap a button multiple times to get the expected behavior.

Please share this article if you learned how to handle Menu buttons on iOS.

Let me know if you discover a better way to make Menu buttons on iOS: @PaulSolt on X.