Setting boundaries on pan gestures

Hello,
I have created 5 UIViews, each with UIPanGestureRecognizers, which I then add to another UIView. All this works and I can drag each around, but I don’t want them to be dragged outside the boundaries of their parent UIView. I have read that I can implement UIGestureRecognizerDelegate with the gestureRecognizerShouldBegin function to achieve this, but I can’t find enough documentation on exactly how to achieve exactly what I need. Apple’s documentation on this, as usual, is quite vague. My code is:

    let foreheadTopViewWidth = foreheadTopView.frame.width
            let foreheadTopViewHeight = foreheadTopView.frame.height
            let touchPointOffset = touchPointSize / 2
            let touchPointYPos = (foreheadTopViewHeight / 2) - touchPointOffset

            let touchPointForehead1 = TouchPoint(touchPointSize: touchPointSize, xPosition: 0, yPosition: touchPointYPos, treatmentArea: .upperForehead)
            let touchPointForehead2 = TouchPoint(touchPointSize: touchPointSize, xPosition: ((foreheadTopViewWidth * 0.50) - touchPointOffset) / 2, yPosition: touchPointYPos, treatmentArea: .upperForehead)
            let touchPointForehead3 = TouchPoint(touchPointSize: touchPointSize, xPosition: (foreheadTopViewWidth * 0.50) - touchPointOffset, yPosition: touchPointYPos, treatmentArea: .upperForehead)
            let touchPointForehead4 = TouchPoint(touchPointSize: touchPointSize, xPosition: (foreheadTopViewWidth * 0.75) - (touchPointOffset / 0.75), yPosition: touchPointYPos, treatmentArea: .upperForehead)
            let touchPointForehead5 = TouchPoint(touchPointSize: touchPointSize, xPosition: foreheadTopViewWidth - touchPointSize, yPosition: touchPointYPos, treatmentArea: .upperForehead)

            foreheadTopView.addSubview(touchPointForehead1)
            foreheadTopView.addSubview(touchPointForehead2)
            foreheadTopView.addSubview(touchPointForehead3)
            foreheadTopView.addSubview(touchPointForehead4)
            foreheadTopView.addSubview(touchPointForehead5)

            let foreheadGesture1 = UIPanGestureRecognizer(target: self, action: #selector(didPan(gesture:)))
            let foreheadGesture2 = UIPanGestureRecognizer(target: self, action: #selector(didPan(gesture:)))
            let foreheadGesture3 = UIPanGestureRecognizer(target: self, action: #selector(didPan(gesture:)))
            let foreheadGesture4 = UIPanGestureRecognizer(target: self, action: #selector(didPan(gesture:)))
            let foreheadGesture5 = UIPanGestureRecognizer(target: self, action: #selector(didPan(gesture:)))
            foreheadGesture1.delegate = self
            foreheadGesture2.delegate = self
            foreheadGesture3.delegate = self
            foreheadGesture4.delegate = self
            foreheadGesture5.delegate = self

            touchPointForehead1.addGestureRecognizer(foreheadGesture1)
            touchPointForehead2.addGestureRecognizer(foreheadGesture2)
            touchPointForehead3.addGestureRecognizer(foreheadGesture3)
            touchPointForehead4.addGestureRecognizer(foreheadGesture4)
            touchPointForehead5.addGestureRecognizer(foreheadGesture5)

            foreheadTopView.layer.addSublayer(touchPointForehead1.lineTo(touchpoint: touchPointForehead2))
            foreheadTopView.layer.addSublayer(touchPointForehead2.lineTo(touchpoint: touchPointForehead3))
            foreheadTopView.layer.addSublayer(touchPointForehead3.lineTo(touchpoint: touchPointForehead4))
            foreheadTopView.layer.addSublayer(touchPointForehead4.lineTo(touchpoint: touchPointForehead5))

@objc func didPan(gesture: UIPanGestureRecognizer) {

        guard let touchpoint = gesture.view as? TouchPoint else {
            return
        }
        if (gesture.state == .began) {
            touchpoint.center = gesture.location(in: foreheadTopView)
        }

        let newCenter: CGPoint = gesture.location(in: foreheadTopView)
        let dX = newCenter.x - touchpoint.center.x
        let dY = newCenter.y - touchpoint.center.y
        touchpoint.center = CGPoint(x: touchpoint.center.x + dX, y: touchpoint.center.y + dY)

        if let outGoingTouchPoint = touchpoint.outGoingTouchPoint, let line = touchpoint.outGoingLine, let path = touchpoint.outGoingLine?.path {
            let newPath = UIBezierPath(cgPath: path)
            newPath.removeAllPoints()
            newPath.move(to: touchpoint.center)
            newPath.addLine(to: outGoingTouchPoint.center)
            line.path = newPath.cgPath
        }

        if let inComingTouchPoint = touchpoint.inComingTouchPoint, let line = touchpoint.inComingLine, let path = touchpoint.inComingLine?.path {
            let newPath = UIBezierPath(cgPath: path)
            newPath.removeAllPoints()
            newPath.move(to: inComingTouchPoint.center)
            newPath.addLine(to: touchpoint.center)
            line.path = newPath.cgPath
        }
    }

The above code works fine in terms of what I need it to do in terms of panning/dragging. I have also extended my ViewController to conform to UIGestureRecognizerDelegate

extension ViewController: UIGestureRecognizerDelegate {
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {

    }
}

But I am unsure where to go from here in how to restrict the draggable views within the boundaries of their parent view. Can anybody help?

@deniznasif Thanks very much for your question!

I found some sample code that might be of some help:

What you want to do is use the translation of the gesture recogniser, not the location otherwise you will get a jump if the user doesn’t “grab” the view right in the middle:

var startPosition: CGPoint?
func userDragged(gesture: UIPanGestureRecognizer){
    if gesture.state == .began {
        startPosition = bottomTextField.center
    } else if gesture.state == .changed {
        let translation = gesture.translationInView(self.view)
        guard let start = self.startPosition else { return }
        let newCenter = CGPoint(x: start.x + translation.x, y: x.y + translation.y)
        bottomTextField.center = newCenter
    } else {
        startPosition = nil
    }
} 

After that, you’re going to want to look closer at:

let newCenter = CGPoint(x: start.x + translation.x, y: x.y + translation.y)

to apply some limits to where you can drag the view.

Suppose you don’t want the view to be draggable more than 50pts (or 10% of the superview size) in any direction, you can simply use:

let newCenter = CGPoint(x: start.x + min(translation.x, 50), y: x.y + min(translation.y, 50))

Which simply checks which is smaller, the translation or the value 50, and uses the smaller of the two. [Note: this gives you a square limit around the start point, not a radial limit]

And then you want to look at making this a bit more complex, making the altered distance a function of the translation, so that you can taper the movement as it approaches the limit:

let normX = max(min(translation.x / 150, 1), -1)
let deltaX = 50 * sin(normX) // add custom function here
let normY = max(min(translation.y / 150, 1), -1)
let deltaY = 50 * sin(normY) // add custom function here
let newCenter = CGPoint(x: start.x + min(deltaX, 50), y: x.y + min(deltaY, 50))

This is not the transform you will want to use, but indicates how you can achieve an effect of altering the translation to not just linearly move the view, you will want to sub in your own function of translation to produce the deltas based on whatever elastic curve you desire…

(the provided function will normalised the translation between 1 and -1 over a drag distance of 150pts, and then map it over a sine curve to taper off the delta used to move the view, capping the max delta to 50pts.)


I hope this helps!

All the best!

This topic was automatically closed after 166 days. New replies are no longer allowed.