How to: DragGesture in SwiftUI

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

Gary Tokman
8 min readFeb 21, 2021

Do you want to learn how to make the swipe animation similar to Tinder and Bumble in SwiftUI? Here’s a tutorial on how I was able to accomplish it in less than 10 minutes using SwiftUI’s declarative APIs like DragGesture and animating various view modifiers. Follow along below!

Create a new SwiftUI project in Xcode or get the starter project here.

Alright, we’re in Xcode, and the first thing you want to do is add some assets. These will be the images that the user’s going to see as they’re swiping through your app. Right now, this is just going to be local, but in the future, you can imagine we have some API calls go out that download profile data.

Drag in an image, make sure it’s not too large because when Swift is drawing this view, if it’s 40, 50, 60 megabyte PNG, it’s gonna take up a lot of CPU and cause the app to lag. In practice, you would downsample our image to a few megabytes; that way, it loads really fast or even more efficiently request a smaller image from the API.

Assets.xcassets in Xcode
Asset catalog in Xcode (Assets.xcassets)

Back to our ContentView.swift

It would help if you had a basic understanding of HStack, VStackand ZStackand what state is and how we can modify it. When that state is modified, you know, that part of the UI is re-rendered again. So those are the two concepts kind of prerequisites to this video. The only new concept really here is GeometryReader and gestures.

import SwiftUIstruct ContentView: View {var body: some View {
// 1
GeometryReader { proxy in
// 2
ZStack {
// 3
CardView(proxy: proxy)
}
}
}
}
  1. GeometryReaderis the first view that we will add. This proxy has dimension data like positioning and size, and we’re going to be using that in our child views later on to position them correctly.
  2. 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.
  3. We will be creating a new card view, and we’re going to be passing in this proxy.

We’re going to be creating one card in this example, but if you get part two, I’m gonna be showing you how you can slide through multiple cards, but let’s start small. Let’s start with one card.

I’m going to come up here, create a new file. It’s going to be a Swift UI view called CardView.

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 the first thing we need to add proxy as a property.
  2. 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.
  3. The first is to create a Rectangle, which will be our backdrop for this card.
  4. Let’s give it a corner radius.
  5. 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).
  6. 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.

At this point, we already have our card built out. Now, we need to add our image as an overlay and I say overlay because that’s exactly what we’re going to be doing.

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. We’re going to be using the overlay modifier because this allows us to put any content inside of it.
  2. 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.
  3. 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.
  4. Let’s create a VStack for the text on top of the image.
  5. 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.
  6. 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.

The geometry proxy is not that hard once you’ve done a few examples.

Cool. We have our card, essentially. Now we need to add the swipe feature that Bumble and Tinder both have using a drag gesture.

// var body ...
// 1
let dragGesture = DragGesture()

Rectangle()
.overlay()
...
// 2
.gesture(dragGesture)
// 3
.animation(.interactiveSpring())
  1. This DragGesture has a bunch of modifiers we are going to use. What we want to do first is add it to our rectangle.
  2. Add the drag gesture, and we want this to animate as we drag. Let's add that real quick.
  3. The animation and this is going to be an interactive spring. It’s Apple’s default animation for interactive animations.

You can customize the animation if it doesn’t fit your requirements here. Now this drag gesture has a few modifiers and the main one that we’re going to be using is called updating.

// 2
@GestureState var translation: CGSize = .zero
var body: some View {
let dragGesture = DragGesture()
// 1
.updating($translation) { (value, state, _) in
// 3
state = value.translation
}
  1. Updating takes a gesture state as the first argument. Let's add that above the body.
  2. 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.
  3. 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.

We can try running this (you can click the play button inside of your simulator). When we try to drag the card, nothing happens. What are we missing here? We’re missing actually modifying some property on our rectangle to make it move and what we’re going to be modifying is called the offset.

// Add to Rectangle()// 1
.offset(x: translation.width, y: 0)
.gesture(dragGesture)
// ...
  1. This offset has an X and Y, and we’re going to be modifying the X with the translation, which is our gesture state. And we’re going to be accessing its width, which is like the X. You can modify the Y as well, but I’m going to set it to zero. I don’t want the user to be panning up and down.

We already have our animation here. We can start dragging it, and just like that, it works! That’s the bulk of the animation. The last thing we want to do is add some rotation to this when a user starts panning left and right.

// 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. We can add some gesture state again, and we can call it degrees, for example, because we’re going to be setting the degrees that we rotate the card. Last let's make it a Double and initialize it to zero.
  2. And again, we’re going to add this to the updating function, and we’re going to be updating our degrees.
  3. 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))
  1. Finally, add the rotation modifier and pass in the degree state.

All right, you can see if we run on the simulator. It rotates a little bit as we drag it left and right!

Tinder swipe animation in Xcode
Tinder card animation in Xcode

So that’s pretty much it, folks. I hope this gets you in a good spot to implement inside of your app or project. I created part two that will go into a little more depth on some more animations and more importantly, show you how to swipe a stack of cards like Tinders UX.

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 😎

--

--