How to: DragGesture in SwiftUI
A quick tutorial on how I built out the Tinder swipe animation using SwiftUI.
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.
Back to our
ContentView.swift
It would help if you had a basic understanding of HStack
, VStack
and ZStack
and 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)
}
}
}
}
GeometryReader
is 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.- Inside of this
GeometryReader
let's add aZStack
and now you can think of theZStack
as a bunch of our cards laid up one on top of the other. - 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)
}
}
}
- And the first thing we need to add
proxy
as a property. - 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.
- The first is to create a
Rectangle
, which will be our backdrop for this card. - Let’s give it a corner radius.
- Now we’re going to give a
frame
to our rectangle. First, we’re going to use the argumentmaxWidth
, which will be ourproxy.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 amaxHeight
using our proxy again and multiply it by 0.8 (it takes about 80% of the screen). - 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 themidX
. 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
)
}
}
)
- We’re going to be using the
overlay
modifier because this allows us to put any content inside of it. - Next, add a geometry reader that we can read the
local
coordinate space of thisZStack
. We’re going to be adding an image then we’re going to lay some text on top of the image. - 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. - Let’s create a
VStack
for the text on top of the image. - 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.
- 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())
- This
DragGesture
has a bunch of modifiers we are going to use. What we want to do first is add it to our rectangle. - Add the drag gesture, and we want this to animate as we drag. Let's add that real quick.
- 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 = .zerovar body: some View {
let dragGesture = DragGesture()
// 1
.updating($translation) { (value, state, _) in
// 3
state = value.translation
}
Updating
takes a gesture state as the first argument. Let's add that above thebody
.- 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 aCGSize
and we’re going to initialize it to zero. - In the closure body, we get this
value
,state
, andtransaction
, 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 inUIKIT
this 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)
// ...
- 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
}
- 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. - And again, we’re going to add this to the updating function, and we’re going to be updating our degrees.
- 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))
- 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!
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 😎