Draggable connected UIViews in Swift

I am trying to create some draggable UIViews that are connected by lines. See image below:

drag

I can create the draggable circles by creating a class that is a subclass of UIView and overriding the draw function

override func draw(_ rect: CGRect) {
let path = UIBezierPath(ovalIn: rect)
let circleColor:UIColor

switch group {
case .forehead:
    circleColor = UIColor.red
case .crowsFeetRightEye:
    circleColor = UIColor.green
case .crowsFeetLeftEye:
    circleColor = UIColor.blue
}

circleColor.setFill()
path.fill()
}

and then add a pan gesture recognizer for the dragging

func initGestureRecognizers() {
    let panGR = UIPanGestureRecognizer(target: self, action: #selector(DragPoint.didPan(panGR:)))
    addGestureRecognizer(panGR)
}

@objc func didPan(panGR: UIPanGestureRecognizer) {

    if panGR.state == .changed {
        self.superview!.bringSubview(toFront: self)
        let translation = panGR.translation(in: self)

        self.center.x += translation.x
        self.center.y += translation.y

        panGR.setTranslation(CGPoint.zero, in: self)

    }

}

However, I am totally stuck on how to go about the connecting lines and parenting the start/end points to its corresponding circle when dragged. Is there anybody who can help or point me in the right direction please?

hi @deniznasif,
Can you create the circles and drag them around?
If you have an array that holds the circles, they could be either in a CGRect format or a CGPoint with a fixed radius.

If you iterate through each of the circle and get the mid points, you will get x1, y1 and x2, y2. Draw a line between x1,y1 and x2,y2

it is that simple, you have two options, either you redraw (draw) on every frame move or have the circles as layers and delete and add the connecting lines as sublayers.

cheers,

Jayant

@jayantvarma, thanks for setting me on the right path. In case anybody is looking for a similar solution, this is my code for the class:

import UIKit

enum TreatmentArea {
    case upperForehead
    case glabellar
    case crowsFeet
}

class TouchPoint: UIView {
    
    let touchPointSize: CGFloat
    let xPosition: CGFloat
    let yPosition: CGFloat
    let treatmentArea: TreatmentArea
    var outGoingLine: CAShapeLayer?
    var inComingLine: CAShapeLayer?
    var inComingTouchPoint: TouchPoint?
    var outGoingTouchPoint: TouchPoint?
    
    init(touchPointSize: CGFloat, xPosition: CGFloat, yPosition: CGFloat, treatmentArea: TreatmentArea) {
        self.touchPointSize = touchPointSize
        self.xPosition = xPosition
        self.yPosition = yPosition
        self.treatmentArea = treatmentArea
        super.init(frame: CGRect(x: xPosition, y: yPosition, width: touchPointSize, height: touchPointSize))
        
        self.backgroundColor = UIColor.clear
    }
    
    // We need to implement init(coder) to avoid compilation errors
    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    /*required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
    }*/
    
    override func draw(_ rect: CGRect) {
        let path = UIBezierPath(ovalIn: rect)
        let pointColor = setColor()
        
        pointColor.setFill()
        path.fill()
        
        //initGestureRecognizers()
    }
    
    func lineTo(touchpoint: TouchPoint) -> CAShapeLayer {
        let path = UIBezierPath()
        path.move(to: self.center)
        path.addLine(to: touchpoint.center)
        
        let line = CAShapeLayer()
        line.path = path.cgPath
        line.lineWidth = 3
        
        let lineColor = setColor()
        line.strokeColor = lineColor.cgColor
        touchpoint.inComingLine = line
        outGoingLine = line
        outGoingTouchPoint = touchpoint
        touchpoint.inComingTouchPoint = self
        
        return line
    }
    
    func setColor() -> UIColor {
        let groupColor: UIColor
        
        switch treatmentArea {
        case .upperForehead:
            groupColor = UIColor(red: 255/255, green: 128/255, blue: 40/255, alpha: 1)
        case .glabellar:
            groupColor = .black
        case .crowsFeet:
            groupColor = UIColor(red: 124/255, green: 65/255, blue: 153/255, alpha: 1)
        }
        
        return groupColor
    }

    
}

This is the code I used in my ViewController:

@IBOutlet weak var foreheadTopView: UIView!
    
    let touchPointSize: CGFloat = 30

func drawForeheadPoints() {
        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
        }
    }

@deniznasif,
That’s good :+1:t3:

Cheers,

Jayant

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