Drag and drop is a common pattern in mobile applications. The UIPanGestureRecognizer
class makes detecting pan gestures fairly straightforward. The heavy lifting is handled by UIPanGestureRecognizer
and its superclass, UIGestureRecognizer
. In this post, I show you how to use a pan gesture recognizer in Swift. You learn how to drag and drop a view in its superview using the UIPanGestureRecognizer
class.
Setting Up the Project in Xcode
Fire up Xcode and create a blank project by choosing the App template from the iOS > Application section.
Name the project Panning, set Interface to Storyboard, and Language to Swift.
Open ViewController.swift and declare a private, constant property with name pannableView
of type UIView
. We use a self-executing closure to create and configure the UIView
instance. The view has a blue background color and is 200 points wide and 200 points tall. We set translatesAutoresizingMaskIntoConstraints
to false
because we won't be using Auto Layout to size and position the view.
import UIKit
class ViewController: UIViewController {
// MARK: - Properties
private let pannableView: UIView = {
// Initialize View
let view = UIView(frame: CGRect(origin: .zero,
size: CGSize(width: 200.0, height: 200.0)))
// Configure View
view.backgroundColor = .blue
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
}
}
In the view controller's viewDidLoad()
method, we add pannableView
to the view hierarchy and center it in its superview, the view controller's view, by setting its center
property.
import UIKit
class ViewController: UIViewController {
// MARK: - Properties
private let pannableView: UIView = {
// Initialize View
let view = UIView(frame: CGRect(origin: .zero,
size: CGSize(width: 200.0, height: 200.0)))
// Configure View
view.backgroundColor = .blue
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
// Add to View Hierarchy
view.addSubview(pannableView)
// Center Pannable View
pannableView.center = view.center
}
}
Adding a Pan Gesture Recognizer to a View
The user should have the ability to drag and drop the blue view in its superview. This is easier than you might think. We initialize a UIPanGestureRecognizer
instance by invoking the init(target:action:)
initializer. The target is the view controller and the action is a method with name didPan(_:)
.
import UIKit
class ViewController: UIViewController {
// MARK: - Properties
private let pannableView: UIView = {
// Initialize View
let view = UIView(frame: CGRect(origin: .zero,
size: CGSize(width: 200.0, height: 200.0)))
// Configure View
view.backgroundColor = .blue
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
// Add to View Hierarchy
view.addSubview(pannableView)
// Center Pannable View
pannableView.center = view.center
// Initialize Swipe Gesture Recognizer
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(didPan(_:)))
}
}
Before we implement the didPan(_:)
method, we add the pan gesture recognizer to pannableView
. We invoke the addGestureRecognizer(_:)
method on pannableView
, passing in the pan gesture recognizer.
import UIKit
class ViewController: UIViewController {
// MARK: - Properties
private let pannableView: UIView = {
// Initialize View
let view = UIView(frame: CGRect(origin: .zero,
size: CGSize(width: 200.0, height: 200.0)))
// Configure View
view.backgroundColor = .blue
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
// Add to View Hierarchy
view.addSubview(pannableView)
// Center Pannable View
pannableView.center = view.center
// Initialize Swipe Gesture Recognizer
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(didPan(_:)))
// Add Swipe Gesture Recognizer
pannableView.addGestureRecognizer(panGestureRecognizer)
}
}
The didSwipe(_:)
method accepts a single argument of type UIPanGestureRecognizer
, the pan gesture recognizer that detected and handles the pan gesture. We prefix the method with the objc
attribute to exposes it to the Objective-C runtime. This is a more advanced topic that I won't cover in this post.
// MARK: - Actions
@objc private func didPan(_ sender: UIPanGestureRecognizer) {
}
If we omit the objc
attribute, the compiler throws an error.
In the didPan(_:)
method, the view controller moves pannableView
by updating its center
property. The view controller asks the pan gesture recognizer for the location of the user's finger in the superview of pannableView
. You may want to read that sentence a few times. Let's break this down.
We invoke location(in:)
on the pan gesture recognizer, passing in the view controller's view, that is, the superview of pannableView
. The location(in:)
method returns a CGPoint
object and we assign it to the center
property of pannableView
. That's it.
// MARK: - Actions
@objc private func didPan(_ sender: UIPanGestureRecognizer) {
pannableView.center = sender.location(in: view)
}
Build and run the application to give it a try. You may notice that there is an issue. The center of the view snaps to the location below the user's finger. This isn't a major issue, but we can do better.
Improving the Pan Gesture
The reason this happens is simple. In didPan(_:)
, we set the center of pannableView
to the location of the user's finger. This isn't ideal because the user won't touch the blue view exactly at its center. That is why the blue view snaps to the location below the user's finger. We need to take the location of the user's finger in the blue view into account. This is easy to do.
We declare a private, variable property, initialCenter
, of type CGPoint
. We set the property to zero
. This is shorthand for CGPoint(x: 0.0, y: 0.0)
.
import UIKit
class ViewController: UIViewController {
// MARK: - Properties
private var initialCenter: CGPoint = .zero
...
}
We update the value of initialCenter
when the pan gesture recognizer detected a pan gesture. We use a switch
statement to inspect the value of the state
property of the pan gesture recognizer. If the state of the pan gesture recognizer is equal to began
, we set the initialCenter
property to the center of pannableView
.
// MARK: - Actions
@objc private func didPan(_ sender: UIPanGestureRecognizer) {
switch sender.state {
case .began:
initialCenter = pannableView.center
default:
break
}
}
Every time the user's finger moves, the pan gesture recognizer executes the didPan(_:)
method. The state of the pan gesture recognizer changes from began
to changed
. We ask the pan gesture recognizer for the distance the user's finger has travelled during the pan gesture by invoking the translation(in:)
method, passing in the view controller's view. We use the value of initialCenter
and translation
to calculate the new center of pannableView
.
// MARK: - Actions
@objc private func didPan(_ sender: UIPanGestureRecognizer) {
switch sender.state {
case .began:
initialCenter = pannableView.center
case .changed:
let translation = sender.translation(in: view)
pannableView.center = CGPoint(x: initialCenter.x + translation.x,
y: initialCenter.y + translation.y)
default:
break
}
}
Build and run the application one more time to see the result.
Restoring State
The rest of this post is an optional step to show you what is possible. Let's add a final enhancement to the implementation by resetting the position of pannableView
when the pan gesture ends or is cancelled. The view controller inspects the state
property of the pan gesture recognizer and resets the position of pannableView
if the state of the pan gesture recognizer is equal to ended
or cancelled
. We use a simple animation to move the blue view back to the center of its superview.
// MARK: - Actions
@objc private func didPan(_ sender: UIPanGestureRecognizer) {
switch sender.state {
case .began:
initialCenter = pannableView.center
case .changed:
let translation = sender.translation(in: view)
pannableView.center = CGPoint(x: initialCenter.x + translation.x,
y: initialCenter.y + translation.y)
case .ended,
.cancelled:
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.7, options: [.curveEaseInOut]) {
self.pannableView.center = self.view.center
}
default:
break
}
}
Build and run the application one last time to see the result.