How to: DragGesture in SwiftUI

A quick tutorial on how I built out the Tinder swipe animation using SwiftUI.

Assets.xcassets in Xcode
Asset catalog in Xcode (Assets.xcassets)
import SwiftUIstruct ContentView: View {var body: some View {
// 1
GeometryReader { proxy in
// 2
ZStack {
// 3
CardView(proxy: proxy)
}
}
}
}
  1. Inside of this GeometryReader let's add a ZStack and now you can think of the ZStackas a bunch of our cards laid up one on top of the other.
  2. We will be creating a new card view, and we’re going to be passing in this proxy.
struct CardView: View {
// 1
let proxy: GeometryProxy
var body: some View {
// 3
Rectangle()
// 4
.cornerRadius(10)
// 5
.frame(
maxWidth: proxy.size.width - 28,
maxHeight: proxy.size.height * 0.8
)
// 6
.position(
x: proxy.frame(in: .global).midX,
y: proxy.frame(in: .local).midY - 30
)
}
}
struct CardView_Previews: PreviewProvider {
static var previews: some View {
// 2
GeometryReader { proxy in
CardView(proxy: proxy)
}
}
}
  1. And our previews also going to expect a proxy. So what we can do is create a geometry reader. We now have data from our parent that gives us positioning in size that our body can use.
  2. The first is to create a Rectangle, which will be our backdrop for this card.
  3. Let’s give it a corner radius.
  4. Now we’re going to give a frame to our rectangle. First, we’re going to use the argument maxWidth, which will be our proxy.size.width. And let’s give it a bit of margin on each side. I’m just going to say 28, 14 on each side. Next, we’re going to give a maxHeight using our proxy again and multiply it by 0.8 (it takes about 80% of the screen).
  5. When we do this, the rectangle is up in the corner and we want to center it. We can give it a position and what we can do now is reaccess our proxy. Then use this frame function in coordinate space. We can use global, and we are going to access the midX. When I do that, it centers, and we’re going to do the same thing for the Y. I want to push this up a little bit, so I’m just going to say minus 30ish for a little bit of margin.
Rectangle()
// 1
.overlay(
GeometryReader { proxy in
// 2
ZStack {
// 3
Image("me")
.resizable()
.aspectRatio(contentMode: .fill)
.clipped()
// 4
VStack(alignment: .leading) {
// 5
Text("Gary")
.foregroundColor(.white)
.fontWeight(.bold)
Text("iOS Dev")
.foregroundColor(.white)
.fontWeight(.bold)
}
// 6
.position(
x: proxy.frame(in: .local).minX + 75,
y: proxy.frame(in: .local).maxY - 50
)
}
}
)
  1. Next, add a geometry reader that we can read the local coordinate space of this ZStack. We’re going to be adding an image then we’re going to lay some text on top of the image.
  2. We’re going to create an image first and then adding some modifiers to it. The first one is going to be resizable. We’re going to add some aspect ratio, too, and setting it to fill. Last, let’s add clipped that way, it clips to the bounds of the rectangle.
  3. Let’s create a VStack for the text on top of the image.
  4. Next, let's add some text. The first one will have a name, and the other one can have like your occupation or whatever you want this to be. Let’s make them leading. Finally, let’s add a foreground color of white, then let’s add some font-weight to both.
  5. Last, we need to position everything again. Let’s use the position modifier, and it takes the X and Y. Next, we’re going to be using the proxy, and we’ll position it to the bottom left.
// var body ...
// 1
let dragGesture = DragGesture()

Rectangle()
.overlay()
...
// 2
.gesture(dragGesture)
// 3
.animation(.interactiveSpring())
  1. Add the drag gesture, and we want this to animate as we drag. Let's add that real quick.
  2. The animation and this is going to be an interactive spring. It’s Apple’s default animation for interactive animations.
// 2
@GestureState var translation: CGSize = .zero
var body: some View {
let dragGesture = DragGesture()
// 1
.updating($translation) { (value, state, _) in
// 3
state = value.translation
}
  1. This is a new property wrapper called GestureState. In short, it’s going to track the translation, which is the distance that you pan when you’re dragging. Also, it’s going to be a CGSize and we’re going to initialize it to zero.
  2. In the closure body, we get this value, state, and transaction, which we’re not going to be using. So you can ignore it. Essentially all we have to do here is set our state. Our state is this translation, and we’re setting it to a new value. It’s really that easy. You can imagine in UIKITthis would take hours upon hours to do, but SwiftUI makes it so simple.
// Add to Rectangle()// 1
.offset(x: translation.width, y: 0)
.gesture(dragGesture)
// ...
// 1
@GestureState var degrees: Double = 0

var body: some View {
let dragGesture = DragGesture()
.updating($translation) { (value, state, _) in
state = value.translation
}
// 2
.updating($degrees) { (value, state, _) in
// 3
state = value.translation.width > 0 ? 2 : -2
}
  1. And again, we’re going to add this to the updating function, and we’re going to be updating our degrees.
  2. We’re going to have a ternary operator to see if the translation is positive or negative (That means they’re swiping, right else it’s negative). If it’s greater than zero, the degrees are set to two else negative two.
// 1
.rotationEffect(.degrees(degrees))
Tinder swipe animation in Xcode
Tinder swipe animation in Xcode
Tinder card animation in Xcode

Shameless plug: If you enjoyed this tutorial get part two on my on Patreon in video form or the link below

PS. follow for more tutorials 😎

iOS Software Engineer ⚡️ Creator