Working With Pan Gesture Recognizers In Swift

Working With Pan Gesture Recognizers In Swift

Resources

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.

Setting Up the Project in Xcode

Name the project Panning, set Interface to Storyboard, and Language to Swift.

Setting Up the Project in Xcode

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.

We need to prefix the method with the objc attribute to expose it to the Objective-C runtime.

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.