Holy Swift

Holy Swift

Dragging views around in Swift

Dragging views around in Swift

Gestures for everyone!

Subscribe to my newsletter and never miss my upcoming articles

Hello dear colleagues, Leo here.

Today we'll explore some dragging views and a particular problem that I had when trying to do that. The code will be the simple as it can be to focus on what is interesting about dragging views in iOS. We'll be using UIPanGestureRecognizer to literally get the translation of the gesture.

Special shoutout for this week supporter: Milko - You are awesome!

The result we are trying to achieve is in the gif below:

Let's code! But first...

The Painting

The La Carrera de Cuádrigas( or Chariot Race) is a 1909 painting from Ulpiano Checa . He was a Spanish painter, sculptor, poster designer and illustrator. He used both impressionistic and academic techniques, and mainly painted historical subjects and this painting is the roman chariot races. I choose this painting because the horses are dragging theirs chariots, so a post about dragging views around is more than suitable for this.

The Problem

You need to move some view dragging it around with the user finger input.

First thing we start a new project. Then in the ViewController viewDidLoad you add this lines:

    override func viewDidLoad() {
        super.viewDidLoad()

        for cardNumber in 0...2 {
            let cardView = UIView()

            switch cardNumber {
                case 0:
                    cardView.backgroundColor = .systemTeal
                case 1:
                    cardView.backgroundColor = .systemRed
                case 2:
                    cardView.backgroundColor = .systemBlue
                default:
                    break
            }

            cardView.frame = CGRect(x: 0, y: 0, width: 200, height: 300)
            view.addSubview(cardView)
            cardView.center = view.center

            cardView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handleCardTapped))) // we'll add this func later, just setup this here for now
        }
    }

The code above is pretty straightforward and the only thing it isn't so standard is the UIPanGestureRecognizer. The docs explain exactly what pan gesture recognizer will recognize:

Clients of this class can, in their action methods, query the UIPanGestureRecognizer object for the current translation of the gesture (translation(in:)) and the velocity of the translation (velocity(in:)). They can specify a view’s coordinate system to use for the translation and velocity values. Clients can also reset the translation to a desired value.

One very important part is the last phrase: Clients can also reset the translation to a desired value. But now you are thinking: why I should reset the translation value? Well... Let's look the code above and we'll discuss more about this.

Add this func below the viewDidLoad:

    @objc private func handleCardTapped(_ gesture: UIPanGestureRecognizer) { // mark 1

        if let cardView = gesture.view { // mark 2

            let point = gesture.translation(in: view) // mark 3

            cardView.center = CGPoint(x: cardView.center.x + point.x, y: cardView.center.y + point.y) // mark 4 
            gesture.setTranslation(CGPoint.zero, in: view) // mark 5

        }
    }

Let's break it down because this is very important and I struggled a lot with it:

  1. Mark 1 - Here is the function will be called every time the user drag your view. It will receive a UIPanGestureRecognizer a very useful object that we will explore some attributes of them.

  2. Mark 2 - One of the attributes of UIPanGestureRecognizer is the view the gesture recognizer is attached to. This is important because we want to have access to the view's attributes so we can modify the way we want.

  3. Mark 3 - This is important. The return of this method is a CGPoint. The CGPoint returned is relative to a view. In this example it's irrelevant if you choose the UIViewController's view or the cardView. This attribute is additive, meaning that as long you drag your finger it will just add to the returning CGPoint value. Every time the system call this function if the user don't end the dragging it will add just add the new dragged value to the old. This is very useful when you just want the initial and final places of the dragging, but it's not very good for real time dragging.

  4. Mark 4 - We just add the the X and Y of the view, with the X and Y of the gesture point. This is very simple but can be problematic because the additive feature of gesture translation, the view will move way faster than it should if you don't manage the gesture value. Imagine this, if you drag your finger 2 points in X axis, this method will be called several times because it will call it with CGPoint(x: 0.1 , y:0) , CGPoint(x: 0.2 , y:0), CGPoint(x: 0.3 , y:0), CGPoint(x: 0.4 , y:0), CGPoint(x: 0.5 , y:0) and so on until get to CGPoint(x: 2 , y:0). This means will be adding all this values to the cardView X axis and this is far from what we wanted.

  5. Mark 5 - This is where we correct the gesture value. As the docs says we can RESET the value of the translation. So every time the view call this gesture recognizer it will be a blank state this way doing what we wanted.

Other features of UIPanGestureRecognizer

You can get the velocity of the dragging if this is relevant for you code too:

            let velocity = gesture.velocity(in: view)

This will result in a CGPoint. But differently the translation property this will interprets the velocity of the pan gesture in the coordinate system of the specified view. The velocity of the pan gesture, which is expressed in points per second. The velocity is broken into horizontal and vertical components. So this is very good if you are doing a tinder-like app that a fast swipe left already indicates a big NO for someone.

You also can set minimumNumberOfTouches and maximumNumberOfTouches that is related to how many fingers need to touch to begin the gesture and the maximum number that can touch for gesture recognition.

States of pan gesture

The declaration of UIPanGestureRecognizer inside UIKit is:

open class UIPanGestureRecognizer : UIGestureRecognizer {...}

This gives access to all the UIGestureRecognizer properties, one in particular is called state. The state is very important because it allows you control all the life cycle of the dragging. Let's see what states are and a example of use:

extension UIGestureRecognizer {

    public enum State : Int {

        case possible = 0
        case began = 1
        case changed = 2
        case ended = 3
        case cancelled = 4        
        case failed = 5

        public static var recognized: UIGestureRecognizer.State { get }
    }
}

An example of use is just add this lines in the handleCardTapped callback method:

            if gesture.state == .ended {
                print("gesture ended")
            }

This way you can respond to the end of the gesture. Imagine that you have a view representing a trash can and you need to know if the position of the view dragged is within the trash can area to delete some file. This could be done with the gesture state recognizer, pretty neat isn't?

iOS 13.4 New Feature - Scroll types Mask

The final feature that I want to show here is the property allowedScrollTypesMask of UIPanGestureRecognizer. Setting this mask enables the pan gesture to recognize scroll events, like a mouse scroll movement or a two-finger scroll on a track pad. See UIScrollType.

The allowedScrollTypesMask are a OptionSet struct. This for will be a topic for future posts but what you need to understand is that every static var stands for a unique value that can be combined.

@available(iOS 13.4, *)
public struct UIScrollTypeMask : OptionSet {

    public init(rawValue: Int)


    public static var discrete: UIScrollTypeMask { get }

    public static var continuous: UIScrollTypeMask { get }

    public static var all: UIScrollTypeMask { get }
}

There's three values in the UIScrollTypeMask. The continuous type that you should allow if you want that the view could be scrolled by a track pad movement, like the two-finger scroll. The discrete type that allows your view to translate the scroll movement of a mouse. And the all type is the union of both properties above mentioned.

The only negative thing is that this feature is only iOS 13.4 or above so some of us will have to wait to use it.

Conclusion

Today we read about the pan gesture in iOS. Some features of UIPanGestureRecognizer and the states of a GestureRecognizer.

That's all my people, I hope you liked this as I liked writing. If you want to support this blog you can Buy Me a Coffee or just leave a comment saying hello.

Thanks for the reading and... That's all folks.

credits: image

Interested in reading more such articles from Leonardo Maia Pugliese?

Support the author by donating an amount of your choice.

#swift#ios#coding#apple
 
Share this