- Dec 19, 2024
Mastering DragGesture in SwiftUI
- DevTechie
- SwiftUI
The DragGesture in SwiftUI, is a gesture recognizer which tracks the movement of a touch across the screen. Which capturing the movement of drag it also captures details about starting location, current location, and distance traveled. This gesture is commonly used to implement features like swiping, dragging views, or tracking movement for interactive UI elements.
Let’s look at an example to understand this gesture. We will start with a simple view which has a circle in the center with an overlay Text view.
struct DragGestureExample: View {
var body: some View {
Circle()
.fill(.orange)
.frame(width: 100, height: 100)
.overlay {
Text("DevTechie")
.bold()
.foregroundStyle(.white)
}
.preferredColorScheme(.dark)
}
}We will attach DragGesture to this view using gesture modifier.
struct DragGestureExample: View {
var body: some View {
Circle()
.fill(.orange)
.frame(width: 100, height: 100)
.overlay {
Text("DevTechie")
.bold()
.foregroundStyle(.white)
}
.gesture(DragGesture())
.preferredColorScheme(.dark)
}
}When the view is dragged, we want to update the offset value of the circle view to match the drag location. For that, we need to store the offset value and add an offset modifier to the circle view first.
struct DragGestureExample: View {
@State private var offset = CGSize.zero
var body: some View {
Circle()
.fill(.orange)
.frame(width: 100, height: 100)
.overlay {
Text("DevTechie")
.bold()
.foregroundStyle(.white)
}
.offset(offset)
.gesture(DragGesture())
.preferredColorScheme(.dark)
}
}Next, let’s add the onChange modifier to DragGesture and capture the translation of the gesture to update the offset value for the circle view. Let’s also use withAnimation block to animate the view to new location.
struct DragGestureExample: View {
@State private var offset = CGSize.zero
var body: some View {
Circle()
.fill(.orange)
.frame(width: 100, height: 100)
.overlay {
Text("DevTechie")
.bold()
.foregroundStyle(.white)
}
.offset(offset)
.gesture(
DragGesture().onChanged(
{ gestureValue in
withAnimation {
offset = gestureValue.translation
}
})
)
.preferredColorScheme(.dark)
}
}DragGesture offers another modifier called onEnded, which helps us in defining the final state when a gesture ends.
Let’s use this modifier to put circle view back to its original place.
struct DragGestureExample: View {
@State private var offset = CGSize.zero
var body: some View {
Circle()
.fill(.orange)
.frame(width: 100, height: 100)
.overlay {
Text("DevTechie")
.bold()
.foregroundStyle(.white)
}
.offset(offset)
.gesture(
DragGesture().onChanged(
{ gestureValue in
withAnimation {
offset = gestureValue.translation
}
})
.onEnded({ _ in
withAnimation {
offset = .zero
}
})
)
.preferredColorScheme(.dark)
}
}We can calculate the drag velocity from the closure DragGesture.value parameter to determine the speed of the gesture to create a throwing effect in the direction of the drag.We will also utilize the predicatedEndLocation value provided by the DragGesture API for this effect.
struct DragGestureExample: View {
@State private var offset = CGSize.zero
@State private var isDragging = false
var body: some View {
Circle()
.fill(.orange)
.frame(width: 100, height: 100)
.overlay {
Text("DevTechie")
.bold()
.foregroundStyle(.white)
}
.offset(offset)
.gesture(
DragGesture()
.onChanged({ gestureValue in
isDragging = true
withAnimation {
offset = gestureValue.translation
}
})
.onEnded({ gestureValue in
isDragging = false
let velocity = gestureValue.velocity
let predictedEndLocation = CGSize(
width: offset.width + 0.1 * velocity.width,
height: offset.height + 0.1 * velocity.height
)
withAnimation(.spring(response: 0.6, dampingFraction: 0.3, blendDuration: 0)) {
if abs(velocity.width) > 100 || abs(velocity.height) > 100 {
offset = predictedEndLocation
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
withAnimation(.spring(response: 0.6, dampingFraction: 0.7, blendDuration: 0)) {
offset = .zero
}
}
} else {
offset = .zero
}
}
})
)
.preferredColorScheme(.dark)
}
}Now that we have a grasp of DragGesture and its states, let’s build a complex user interface to demonstrate the drag-and-drop functionality. Specifically, we want to create a scenario where the center view is dropped onto four predefined hotspots. If it remains on top of one of these hotspots, it should stay there. However, if it is dropped onto another hotspot or blank space, it should revert to its original position.
Start by creating State properties.
struct DragGestureExample: View {
@State private var offset = CGSize.zero
@State private var isDragging = false
@State private var activeDropPoint: Int? = nil
@State private var hasAnimated = falseWe will create dropPoints locations next
struct DragGestureExample: View {
@State private var offset = CGSize.zero
@State private var isDragging = false
@State private var activeDropPoint: Int? = nil
@State private var hasAnimated = false
let dropPoints = [
CGPoint(x: -150, y: -250),
CGPoint(x: 150, y: -250),
CGPoint(x: -150, y: 150),
CGPoint(x: 150, y: 150)
]Let’s define a function to determine if the dragged circle falls within the designated drop zone.
struct DragGestureExample: View {
@State private var offset = CGSize.zero
@State private var isDragging = false
@State private var activeDropPoint: Int? = nil
@State private var hasAnimated = false
let dropPoints = [
CGPoint(x: -150, y: -250),
CGPoint(x: 150, y: -250),
CGPoint(x: -150, y: 150),
CGPoint(x: 150, y: 150)
]
func isWithinDropArea(currentPosition: CGPoint, dropPoint: CGPoint) -> Bool {
let distance = sqrt(pow(currentPosition.x - dropPoint.x, 2) +
pow(currentPosition.y - dropPoint.y, 2))
return distance < 50
}We’ll create a ZStack to position all circles at the center of the screen and animate them once the view appears.
struct DragGestureExample: View {
@State private var offset = CGSize.zero
@State private var isDragging = false
@State private var activeDropPoint: Int? = nil
@State private var hasAnimated = false
let dropPoints = [
CGPoint(x: -150, y: -250),
CGPoint(x: 150, y: -250),
CGPoint(x: -150, y: 150),
CGPoint(x: 150, y: 150)
]
func isWithinDropArea(currentPosition: CGPoint, dropPoint: CGPoint) -> Bool {
let distance = sqrt(pow(currentPosition.x - dropPoint.x, 2) +
pow(currentPosition.y - dropPoint.y, 2))
return distance < 50
}
var body: some View {
ZStack {
ForEach(0..<4) { index in
Circle()
.fill(activeDropPoint == index ? .green : .red)
.frame(width: 100, height: 100)
.position(
x: UIScreen.main.bounds.width/2 + (hasAnimated ? dropPoints[index].x : 0),
y: UIScreen.main.bounds.height/2 + (hasAnimated ? dropPoints[index].y : 0)
)
}
Circle()
.fill(.orange)
.frame(width: 100, height: 100)
.overlay {
Text("DevTechie")
.bold()
.foregroundStyle(.white)
}
.offset(offset)
.gesture(
DragGesture()
.onChanged({ gestureValue in
isDragging = true
withAnimation {
offset = gestureValue.translation
}
})
.onEnded({ gestureValue in
isDragging = false
let currentPosition = CGPoint(
x: gestureValue.translation.width,
y: gestureValue.translation.height
)
var droppedOnPoint = false
for (index, dropPoint) in dropPoints.enumerated() {
if isWithinDropArea(currentPosition: currentPosition,
dropPoint: dropPoint) {
withAnimation(.spring()) {
offset = CGSize(width: dropPoint.x,
height: dropPoint.y)
activeDropPoint = index
}
droppedOnPoint = true
break
}
}
if !droppedOnPoint {
withAnimation(.spring()) {
offset = .zero
activeDropPoint = nil
}
}
})
)
}
.preferredColorScheme(.dark)
.onAppear {
withAnimation(.easeOut(duration: 1.0)) {
hasAnimated = true
}
}
}
}



