Manish
Manish

Reputation: 702

How to set boundaries of dragging control using UIPanGestureRecognizer?

I have a View, inside that I am adding a Label then I tried to set boundary for moving Label using PanGesture. Below is my code:

@objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
    if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
        let translation = gestureRecognizer.translation(in: self)
        if let view = gestureRecognizer.view {

            if (view.frame.origin.x + translation.x >= 0) && (view.frame.origin.y + translation.y >= 0) && (view.frame.origin.x + translation.x <= view.frame.width) && (view.frame.origin.y + translation.y <= view.frame.height)
            {
                view.center = CGPoint(x:view.center.x + translation.x,
                                       y:view.center.y + translation.y)
            }
        }
        gestureRecognizer.setTranslation(CGPoint.zero, in: self)
    }

}

I took the reference of this answer : https://stackoverflow.com/a/49008808/9970928

But it does not work, can anyone tell what I am missing in the condition?

Upvotes: 1

Views: 439

Answers (1)

Matic Oblak
Matic Oblak

Reputation: 16774

The code is not most intuitive but assuming everything else works the problem is that it only goes off the bounds on right and bottom. Look at your condition with:

(view.frame.origin.x + translation.x <= view.frame.width)
(view.frame.origin.y + translation.y <= view.frame.height)

So it says that the origin may not be greater than size of bounds what you want to do is check the maximum values of the inner view:

(view.frame.maxX + translation.x <= view.frame.width)
(view.frame.maxY + translation.y <= view.frame.height)

But this procedure in general may produce issues. Imagine that user swipes very quickly rightwards. And that the maximum center.x could be 100. Current center.x is 50 and user drags it in a single frame to 200. Your condition will fail and your label will stay at 50 instead of 100. I would go with clamping the frame to bounds.

Something like the following should do:

func clampFrame(_ frame: CGRect, inBounds bounds: CGRect) -> CGRect {
    let center: CGPoint = CGPoint(x: max(bounds.minX + frame.width*0.5, min(frame.midX, bounds.maxX - frame.width*0.5)),
                                  y: max(bounds.minY + frame.height*0.5, min(frame.midY, bounds.maxY - frame.height*0.5)))
    return CGRect(x: center.x-frame.width*0.5, y: center.y-frame.height*0.5, width: frame.width, height: frame.height)
}

func moveFrame(_ frame: CGRect, by translation: CGPoint, constrainedTo bounds: CGRect) -> CGRect {
    var newFrame = frame
    newFrame.origin.x += translation.x
    newFrame.origin.y += translation.y
    return clampFrame(newFrame, inBounds: bounds)
}

There may be other issues as well using "translation" procedure. I would go with finding a location in view. Please see the following working example:

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let myView = MyView(frame: CGRect(x: 100.0, y: 100.0, width: 200.0, height: 200.0))
        myView.backgroundColor = UIColor.green
        view.addSubview(myView)
        let label = UILabel(frame: .zero)
        label.backgroundColor = UIColor.blue.withAlphaComponent(0.2)
        label.font = UIFont.systemFont(ofSize: 50.0)
        label.text = "Hi!"
        label.sizeToFit()
        myView.addSubview(label)
        label.addGestureRecognizer(UIPanGestureRecognizer(target: myView, action: #selector(MyView.handlePan)))
        label.isUserInteractionEnabled = true
    }


}

class MyView: UIView {

    func clampFrame(_ frame: CGRect, inBounds bounds: CGRect) -> CGRect {
        let center: CGPoint = CGPoint(x: max(bounds.minX + frame.width*0.5, min(frame.midX, bounds.maxX - frame.width*0.5)),
                                      y: max(bounds.minY + frame.height*0.5, min(frame.midY, bounds.maxY - frame.height*0.5)))
        return CGRect(x: center.x-frame.width*0.5, y: center.y-frame.height*0.5, width: frame.width, height: frame.height)
    }

    func moveFrame(_ frame: CGRect, by translation: CGPoint, constrainedTo bounds: CGRect) -> CGRect {
        var newFrame = frame
        newFrame.origin.x += translation.x
        newFrame.origin.y += translation.y
        return clampFrame(newFrame, inBounds: bounds)
    }

    private var startLocation: CGPoint = .zero
    private var startFrame: CGRect = .zero

    @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
        guard let label = gestureRecognizer.view else { return }

        if gestureRecognizer.state == .began {
            startLocation = gestureRecognizer.location(in: self)
            startFrame = label.frame
        } else if gestureRecognizer.state == .changed {
            let newLocation = gestureRecognizer.location(in: self)
            let translation = CGPoint(x: newLocation.x-startLocation.x, y: newLocation.y-startLocation.y)

            label.frame = moveFrame(startFrame, by: translation, constrainedTo: self.bounds)
        }

    }

}

Upvotes: 2

Related Questions