SwiftUI gives us lots of gestures for working with views, and does a great job of taking away most of the hard work so we can focus on the parts that matter. We already used onTapGesture()
in an earlier project, but there are several others, and there are also interesting ways of combining gestures together that are worth trying out.
I’m going to skip past the simple onTapGesture()
because we’ve covered it previously, but before we try bigger things I do want to add that you can pass a count
parameter to these to make them handle double taps, triple taps, and more, like this:
Text("Hello, World!")
.onTapGesture(count: 2) {
print("Double tapped!")
}
Okay, let’s look at something more interesting than simple taps. For handling long presses you can use onLongPressGesture()
, like this:
Text("Hello, World!")
.onLongPressGesture {
print("Long pressed!")
}
Like tap gestures, long press gestures are also customizable. For example, you can specify a minimum duration for the press, so your action closure only triggers after a specific number of seconds have passed. For example, this will trigger only after two seconds:
Text("Hello, World!")
.onLongPressGesture(minimumDuration: 2) {
print("Long pressed!")
}
You can even add a second closure that triggers whenever the state of the gesture has changed. This will be given a single Boolean parameter as input, and it will work like this:
Use code like this to try it out for yourself:
Text("Hello, World!")
.onLongPressGesture(minimumDuration: 1) {
print("Long pressed!")
} onPressingChanged: { inProgress in
print("In progress: \(inProgress)!")
}
For more advanced gestures you should use the gesture()
modifier with one of the gesture structs: DragGesture
, LongPressGesture
, MagnifyGesture
, RotateGesture
, and TapGesture
. These all have special modifiers, usually onEnded()
and often onChanged()
too, and you can use them to take action when the gestures are in-flight (for onChanged()
) or completed (for onEnded()
).
As an example, we could attach a magnify gesture to a view so that pinching in and out scales the view up and down. This can be done by creating two @State
properties to store the scale amount, using that inside a scaleEffect()
modifier, then setting those values in the gesture, like this:
struct ContentView: View {
@State private var currentAmount = 0.0
@State private var finalAmount = 1.0
var body: some View {
Text("Hello, World!")
.scaleEffect(finalAmount + currentAmount)
.gesture(
MagnifyGesture()
.onChanged { value in
currentAmount = value.magnification - 1
}
.onEnded { value in
finalAmount += currentAmount
currentAmount = 0
}
)
}
}
Exactly the same approach can be taken for rotating views using RotateGesture
, except now we’re using the rotationEffect()
modifier:
struct ContentView: View {
@State private var currentAmount = Angle.zero
@State private var finalAmount = Angle.zero
var body: some View {
Text("Hello, World!")
.rotationEffect(currentAmount + finalAmount)
.gesture(
RotateGesture()
.onChanged { value in
currentAmount = value.rotation
}
.onEnded { value in
finalAmount += currentAmount
currentAmount = .zero
}
)
}
}
Where things start to get more interesting is when gestures clash – when you have two or more gestures that might be recognized at the same time, such as if you have one gesture attached to a view and the same gesture attached to its parent.
For example, this attaches an onTapGesture()
to a text view and its parent:
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, World!")
.onTapGesture {
print("Text tapped")
}
}
.onTapGesture {
print("VStack tapped")
}
}
}
In this situation SwiftUI will always give the child’s gesture priority, which means when you tap the text view above you’ll see “Text tapped”. However, if you want to change that you can use the highPriorityGesture()
modifier to force the parent’s gesture to trigger instead, like this:
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, World!")
.onTapGesture {
print("Text tapped")
}
}
.highPriorityGesture(
TapGesture()
.onEnded {
print("VStack tapped")
}
)
}
}
Alternatively, you can use the simultaneousGesture()
modifier to tell SwiftUI you want both the parent and child gestures to trigger at the same time, like this:
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, World!")
.onTapGesture {
print("Text tapped")
}
}
.simultaneousGesture(
TapGesture()
.onEnded {
print("VStack tapped")
}
)
}
}
That will print both “Text tapped” and “VStack tapped”.
Finally, SwiftUI lets us create gesture sequences, where one gesture will only become active if another gesture has first succeeded. This takes a little more thinking because the gestures need to be able to reference each other, so you can’t just attach them directly to a view.
Here’s an example that shows gesture sequencing, where you can drag a circle around but only if you long press on it first:
struct ContentView: View {
// how far the circle has been dragged
@State private var offset = CGSize.zero
// whether it is currently being dragged or not
@State private var isDragging = false
var body: some View {
// a drag gesture that updates offset and isDragging as it moves around
let dragGesture = DragGesture()
.onChanged { value in offset = value.translation }
.onEnded { _ in
withAnimation {
offset = .zero
isDragging = false
}
}
// a long press gesture that enables isDragging
let pressGesture = LongPressGesture()
.onEnded { value in
withAnimation {
isDragging = true
}
}
// a combined gesture that forces the user to long press then drag
let combined = pressGesture.sequenced(before: dragGesture)
// a 64x64 circle that scales up when it's dragged, sets its offset to whatever we had back from the drag gesture, and uses our combined gesture
Circle()
.fill(.red)
.frame(width: 64, height: 64)
.scaleEffect(isDragging ? 1.5 : 1)
.offset(offset)
.gesture(combined)
}
}
Gestures are a really great way to make fluid, interesting user interfaces, but make sure you show users how they work otherwise they can just be confusing!
SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure and A/B test your entire paywall UI without any code changes or app updates.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.