user1391152
user1391152

Reputation: 1289

Draggable connected UIViews in Swift

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

enter image description here

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?

Upvotes: 4

Views: 1554

Answers (1)

Nordeast
Nordeast

Reputation: 1373

You want to use CAShapeLayers with UIBezier paths to draw the lines between the circles and then change the paths when the user moves the views.

Here is a playground showing an implementation. You can copy and paste this into a playground to see it in action.

//: A UIKit based Playground for presenting user interface

import UIKit
import PlaygroundSupport

class CircleView : UIView {

    var outGoingLine : CAShapeLayer?
    var inComingLine : CAShapeLayer?
    var inComingCircle : CircleView?
    var outGoingCircle : CircleView?

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.layer.cornerRadius = self.frame.size.width / 2
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func lineTo(circle: CircleView) -> CAShapeLayer {
        let path = UIBezierPath()
        path.move(to: self.center)
        path.addLine(to: circle.center)

        let line = CAShapeLayer()
        line.path = path.cgPath
        line.lineWidth = 5
        line.strokeColor = UIColor.red.cgColor
        circle.inComingLine = line
        outGoingLine = line
        outGoingCircle = circle
        circle.inComingCircle = self
        return line
    }
}

class MyViewController : UIViewController {

    let circle1 = CircleView(frame: CGRect(x: 100, y: 100, width: 50, height: 50))
    let circle2 = CircleView(frame: CGRect(x: 100, y: 200, width: 50, height: 50))
    let circle3 = CircleView(frame: CGRect(x: 100, y: 300, width: 50, height: 50))
    let circle4 = CircleView(frame: CGRect(x: 100, y: 400, width: 50, height: 50))

    override func loadView() {
        let view = UIView()
        view.backgroundColor = .white
        self.view = view

        circle1.backgroundColor = .red
        view.addSubview(circle1)

        circle2.backgroundColor = .red
        view.addSubview(circle2)

        circle3.backgroundColor = .red
        view.addSubview(circle3)

        circle4.backgroundColor = .red
        view.addSubview(circle4)

        circle1.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(didPan(gesture:))))

        circle2.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(didPan(gesture:))))

        circle3.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(didPan(gesture:))))

        circle4.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(didPan(gesture:))))

        view.layer.addSublayer(circle1.lineTo(circle: circle2))
        view.layer.addSublayer(circle2.lineTo(circle: circle3))
        view.layer.addSublayer(circle3.lineTo(circle: circle4))
    }

    @objc func didPan(gesture: UIPanGestureRecognizer) {
        guard let circle = gesture.view as? CircleView else {
            return
        }
        if (gesture.state == .began) {
            circle.center = gesture.location(in: self.view)
        }
        let newCenter: CGPoint = gesture.location(in: self.view)
        let dX = newCenter.x - circle.center.x
        let dY = newCenter.y - circle.center.y
        circle.center = CGPoint(x: circle.center.x + dX, y: circle.center.y + dY)


        if let outGoingCircle = circle.outGoingCircle, let line = circle.outGoingLine, let path = circle.outGoingLine?.path {

            let newPath = UIBezierPath(cgPath: path)
            newPath.removeAllPoints()
            newPath.move(to: circle.center)
            newPath.addLine(to: outGoingCircle.center)
            line.path = newPath.cgPath
        }

        if let inComingCircle = circle.inComingCircle, let line = circle.inComingLine, let path = circle.inComingLine?.path {

            let newPath = UIBezierPath(cgPath: path)
            newPath.removeAllPoints()
            newPath.move(to: inComingCircle.center)
            newPath.addLine(to: circle.center)
            line.path = newPath.cgPath
        }
    }
}

// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
PlaygroundPage.current.needsIndefiniteExecution = true

gif showing the code working

Upvotes: 8

Related Questions