Drag and Drop on iOS using Swift

Drag and Drop on iOS using Swift

Hello folks, it's been a while since I wrote my last article. Today we're going to learn how to implement drag and drop feature on iOS using Swift.

To see in advance what we're going to achieve by the end of this tutorial, please watch the following video,

To summarize the goal of this project, we have multiple blue squares and one red square. Our goal is to be able to drag green squares across the screen. If they intersect with red square, remove them from view (i.e. We moved them to the trash can). In case we release green square without ever intersecting it with red square,  it will get back to its original position
  1. Creating green squares (folders) and red square (trash can)

First step in our project is to create a UI containing all the squares. In order to create these squares, we will have two separate functions - One for green squares, and other for red square. We will place both these functions into viewDidLoad method.

override func viewDidLoad() {
    super.viewDidLoad()
    ...
    ...
    addMovableViews(count: 4)
    addTrashView()
}

Before we start creating custom views, we need to define some constants on the file level. They specify padding in both directions and size of each square.

private struct Constants {
    static let padding: CGFloat = 10
    static let blockDimension: CGFloat = 50
}

1.1 Adding trash view

Adding a single trash view is relatively simple. All we have to do is to create a square of fixed dimension and pin it to the bottom-right part of the screen.

private func addTrashView() {
    trashView = UIView(frame: CGRect(x: view.frame.width - Constants.padding - Constants.blockDimension, y: view.frame.height - Constants.padding - Constants.blockDimension, width: Constants.blockDimension, height: Constants.blockDimension))
    trashView?.backgroundColor = .red
    view.addSubview(trashView!)
}

1.2 Adding folder views

While adding folder views on UI, we want to make it flexible in terms of number of squares that can be created on the view. Thus, if you look into method signature for addMovableViews, you will see that we are passing the count, which corresponds to number of squares we want to create.

private func addMovableViews(count: Int)

Since we have relatively small number of squares, each square is identical in terms of dimension and y position. However, since we are stacking them in the horizontal direction, we increase the x position of successive square by square dimension and padding between squares.

for _ in 0..<count {
    let movableView = UIView(frame: CGRect(x: xOffset, y: 64, width: Constants.blockDimension, height: Constants.blockDimension))
    movableView.backgroundColor = .green
    view.addSubview(movableView)
    
    ...
    ...
    
    xOffset += Constants.blockDimension + Constants.padding
}

Since we want each of these squares to be draggable, we will also add a gesture of type UIPanGestureRecognizer to each of them.

let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(touched(_:)))
movableView.addGestureRecognizer(gestureRecognizer)

And this is the target method that gets called every time the user tries to drag the view around.

@objc private func touched(_ gestureRecognizer: UIGestureRecognizer) {
    ....
    ....
}

And here is the full method that creates folder views, adds them to the superview and adds the UIPanGestureRecognizer to each of them.

private func addMovableViews(count: Int) {
    var xOffset = Constants.padding
    for _ in 0..<count {
        let movableView = UIView(frame: CGRect(x: xOffset, y: 64, width: Constants.blockDimension, height: Constants.blockDimension))
        movableView.backgroundColor = .green
        view.addSubview(movableView)
        let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(touched(_:)))
        movableView.addGestureRecognizer(gestureRecognizer)
        xOffset += Constants.blockDimension + Constants.padding
    }
}

Once we are here, try to run the app and you will see the series of green squares on top and a single red square in bottom-right corner.

2. Making views draggable and movable to the trash (Red square)

As we saw in the previous paragraph, we already have a target method that gets called every time the user tries to drag the green square around - Thanks to UIPanGestureRecognizer added to every movable view.

In this method we have access to gestureRecognizer of type UIGestureRecognizer which has a state associated with it. Based on the state whether it is began, ended, or changed, we can add appropriate logic to make these green squares move with the pan gesture.

First off, we will look at the interesting case when gestureRecognizer state is changed. Here's the series of events that will take place from the moment view is dragged to be able to make it look like view is actually moving with the touch.

  1. Inside the target method we will first grab the view (touchedView) associated with the touch action
  2. Then, we will get the location of touch (locationInView) inside the touchedView
  3. Next, we will move the touchedView from its original position by adding x and y translation associated with locationInView received in the previous step
  4. After applying translations if touchedView intersects with trashView, remove the touchedView from the view hierarchy
@objc private func touched(_ gestureRecognizer: UIGestureRecognizer) {
    if let touchedView = gestureRecognizer.view {
        if gestureRecognizer.state == .changed {
            let locationInView = gestureRecognizer.location(in: touchedView)
            touchedView.frame.origin = CGPoint(x: touchedView.frame.origin.x + locationInView.x, y: touchedView.frame.origin.y + locationInView.y)
        }
    }
}

But as you can see, no matter where the touch starts inside the view, you always end up grabbing the top-left corner of the view.

In order to fix this problem, we have to save the initial location of touch inside view when the gestureRecognizer state is began. When the state changes to changed, we have to shift the moving view in x and y directions corresponding to the initial location of previously saved touch point.

Here is the updated code to get rid of previous bug and move the movable views in sync with the moving touch.

@objc private func touched(_ gestureRecognizer: UIGestureRecognizer) {
    if let touchedView = gestureRecognizer.view {
        if gestureRecognizer.state == .began {
            beginningPosition = gestureRecognizer.location(in: touchedView)
        } else if gestureRecognizer.state == .changed {
            let locationInView = gestureRecognizer.location(in: touchedView)
            touchedView.frame.origin = CGPoint(x: touchedView.frame.origin.x + locationInView.x - beginningPosition.x, y: touchedView.frame.origin.y + locationInView.y - beginningPosition.y)
        }
    }
}

So we have come this far to be able to move each view with touch. However, there is one more thing we need to add - An ability to make view jump to its original position if the view is released without ever intersecting the red square (Trash).

We can add this feature by saving the original position of square when the gestureRecognizer state is began and assigning the same value to view's origin when view is released without moving it to trash.

@objc private func touched(_ gestureRecognizer: UIGestureRecognizer) {
    if let touchedView = gestureRecognizer.view {
        if gestureRecognizer.state == .began {
            initialMovableViewPosition = touchedView.frame.origin
        } else if gestureRecognizer.state == .ended {
            touchedView.frame.origin = initialMovableViewPosition
        }
    }
}


And finally we will add the last feature that will allow us the detection of intersection of green square with red square and completely remove the green square from the view hierarchy if these squares ever overlap with each other.

This can be achieved by placing additional logic when the gesture recognizer state is changed. Every time the green square is moved, we will call the func intersects(_ rect2: CGRect) -> Bool method to check if the frames of green and red squares intersect or not. If they do intersect, we will remove the corresponding green square from view and set initialMovableViewPosition to .zero since that will no longer be applicable for non-existent view.

@objc private func touched(_ gestureRecognizer: UIGestureRecognizer) {
    if let touchedView = gestureRecognizer.view {
        ...
        ...
        ...
        ...
        
        else if gestureRecognizer.state == .changed {
            let locationInView = gestureRecognizer.location(in: touchedView)
            touchedView.frame.origin = CGPoint(x: touchedView.frame.origin.x + locationInView.x - beginningPosition.x, y: touchedView.frame.origin.y + locationInView.y - beginningPosition.y)

            if touchedView.frame.intersects(trashView!.frame) {
                touchedView.removeFromSuperview()
                initialMovableViewPosition = .zero
            }
        }
    }
}
And that's all you need to know to implement basic drag and drop functionality on iOS platform using Swift as a programming language. The full source code is available on Github at DragAndDrop repository