
Combining gestures and animations with SwiftUI
Learn the first steps on how to implement gestures and animations together in a SwiftUI app.
One type of user input event in iOS is gestures. To handle gesture interactions, we use gesture modifiers in SwiftUI. They work by updating the state of a view or performing an action. The gesture-driven concept is included in this context.
Gestures can also trigger animations or, to provide a feeling of direct control, an animated interaction can be executed while the gesture is being performed, providing a better user experience overall. An example is when the user drags an object and then drops it in a new position. In practice, the object just needs to appear in two positions, the initial and the final, but rendering it while the user is interacting with the screen provides a better sense of control and responsiveness.
Simply moving from an initial position to a new position
// View representing the location in which the image must be positioned at.
struct GradientRectangle: View {
let gradientFill: LinearGradient = LinearGradient(
gradient: Gradient(stops: [
Gradient.Stop(color: Color.purple, location: 0.0),
Gradient.Stop(color: Color.red, location: 0.2),
Gradient.Stop(color: Color.red, location: 0.8),
Gradient.Stop(color: Color.yellow, location: 1.0)
]), startPoint: .leading, endPoint: .trailing)
var body: some View {
VStack {
Rectangle()
.fill(gradientFill)
.aspectRatio(16/9, contentMode: .fit)
.frame(height: 200)
.cornerRadius(10)
.overlay(
Circle()
.foregroundStyle(.white)
.frame(width: 40, height: 40)
.shadow(radius: 10)
)
}
}
}
struct ContentView: View {
// Current position of the object
@State private var objectPosition = CGPoint(x: 200, y: 200)
var body: some View {
VStack {
GradientRectangle()
Image("CreateWithSwiftIcon")
.resizable()
.frame(width: 50, height: 50)
// Positions the center of the view at the coordinate space of the parent view
.position(objectPosition)
// Gesture updating the position of the view
.gesture(
DragGesture()
.onChanged { value in
objectPosition = value.location
}
)
}
}
}
In this simple first example, the position of the element is defined by the objectPosition
state property and the position(_:)
modifier. The drag gesture updates the position based on the location of the user’s finger.
DragGesture
invokes an action as the drag-event sequence changes.gesture(_:including:)
attaches a gesture to a view.onChanged(_:)
when receiving a gesture change, an action is performed.
Applying a spring animation
Building on the previous example, the following one applies a spring animation to the view, returning it to its original position once the user stops dragging it.
import SwiftUI
struct ContentView: View {
// Current offset of the view
@State private var objectOffset = CGSize.zero
var body: some View {
ZStack {
GradientRectangle()
Image("CreateWithSwiftIcon")
.resizable()
.frame(width: 50, height: 50)
// Applies the offset to the position of the view
.offset(objectOffset)
// Updates the offset based on the translation from the gesture initial position
.gesture(
DragGesture()
.onChanged { value in
// Updates the offset value
objectOffset = value.translation
}
.onEnded { _ in
// Resets the offset value
withAnimation(.spring()) {
objectOffset = .zero
}
}
)
}
}
}
Instead of using the position(_:)
modifier to update the location of the object, we use the offset(_:)
modifier, just applying an offset to the original position based on the translation value of the gesture. When the offset is set to zero, the object comes back to the original position with a spring animation.
onEnded(_:)
: When the gesture ends, the action associated with it is triggered. In the example, the offset of the view is reset.
Applying a wave animation
In this example, we have an array of letters that the user can drag from. When the gesture ends, the letters return, one by one, to their original position with an animation:
struct ContentView: View {
// Array of letters to be animated
let letters = Array("Create with Swift")
// Offset to be applied
@State var offsetValue = CGSize.zero
let gradientFill = LinearGradient(gradient: Gradient(stops: [
Gradient.Stop(color: Color.purple, location: 0.0),
Gradient.Stop(color: Color.red, location: 0.2),
Gradient.Stop(color: Color.red, location: 0.8),
Gradient.Stop(color: Color.yellow, location: 1.0)
]), startPoint: .leading, endPoint: .trailing)
var body: some View {
ZStack {
Rectangle()
.fill(gradientFill)
.aspectRatio(16/9, contentMode: .fit)
.frame(height: 200)
.cornerRadius(10)
HStack(spacing: 0){
ForEach(0..<letters.count, id: \.self) { num in
Text("\(letters[num])")
.foregroundStyle(.white)
.background(.red)
.offset(offsetValue)
.animation(.linear.delay(Double(num) / 20), value: offsetValue)
}
.font(.title)
}
.gesture(
DragGesture()
.onChanged { value in
offsetValue = value.translation
}
.onEnded { _ in
offsetValue = .zero
}
)
}
}
}
By adding an action to be performed when the gesture starts and ends, the animation(_:value:)
modifier applies a delayed animation to each letter, generating a wave effect.
Check out the property wrapper @GestureState
for properties that keep track of the current state of a gesture. The instance method updating(_:body:)
updates the gesture state as the gesture’s value changes.
Once the gesture becomes inactive, the state resets to its initial value.