How to increase a SwiftUI view tap area without sacrificing the layout

Volodymyr Dudchak
Arcush Tech
Published in
3 min readDec 24, 2023

When you implement custom controls for mobile, it is important to make sure that user will be able to easily tap them. According to Apple’s user interface guide, buttons should be at least 44x44 points in size, but the actual visual frame of a button can be smaller in your design. For example, in my app Arcush, I have a few controls that are visually a bit smaller than a recommended size:

Red rectangles highlight controls with smaller sizes

This problem is relatively easy to fix in UIKit where you can manipulate the hit test directly. For example, here is a fix for a button using a single override in a custom subclass:

class CustomButton: UIButton {
var tapAreaInsetSize: CGSize = .zero

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return bounds
.insetBy(dx: -tapAreaInsetSize.width, dy: -tapAreaInsetSize.height)
.contains(point)
}
}

But in the SwiftUI world, you are not able to manipulate the hit test mechanism directly. One of the articles I found suggests using the .contentShape modifier, but it only changes the tappable portion of the view. For example, if you have a round button, you can use a rectangular shape to make the whole frame receive a tap, not only the round part.

Here is a little trick I came up with to fix the tap area while preserving the same small frame of the control used in the layout:

struct CloseButton: View {
var onTap: () -> Void

var body: some View {
Button {
onTap()
} label: {
Image(systemName: "xmark.circle.fill")
.frame(width: 20, height: 20)
}
.padding(12)
.onTapGesture {
onTap()
}
.padding(-12)
}
}

In the example above, the result frame of the CloseButton is still 20x20, but the tappable area is 44x44 points. The idea is to apply a padding to the View content, add tap gesture modifier to the View with the increased frame, and after that, apply a negative padding, thus preserving the original frame.

I also have a small snippet that allows adding the tap action to any view, while having its frame intact:

struct TappablePadding: ViewModifier {
let insets: EdgeInsets
let onTap: () -> Void

func body(content: Content) -> some View {
content
.padding(insets)
.contentShape(Rectangle())
.onTapGesture {
onTap()
}
.padding(insets.inverted)
}
}

extension View {
func tappablePadding(_ insets: EdgeInsets, onTap: @escaping () -> Void) -> some View {
self.modifier(TappablePadding(insets: insets, onTap: onTap))
}
}

extension EdgeInsets {
var inverted: EdgeInsets {
.init(top: -top, leading: -leading, bottom: -bottom, trailing: -trailing)
}
}

With this view modifier, you can increase the tap area of any view and still position it using its original size.

I hope you enjoyed the little hack I shared and will be able to use it in your projects. If you find this article useful, follow this blog to get more tips in the future. And if you are interested in productivity or want to learn more about the app I’m working on, visit our non-technical blog.

--

--

Volodymyr Dudchak
Arcush Tech

I'm a passionate iOS / macOS developer. Ex-Lyft, ex-Macpaw, currently developing the daily planner app: https://arcush.com