Search code examples
iosswiftuiviewuigesturerecognizercashapelayer

Swift - Creating a connection between two labels or button using blue drag line like in Xcode


I have managed to create the blue line like in Xcode but how would you recognise when the end point of the line is dragged and released above the other labels or buttons so a connection can be made?

enter image description here

Code

class ViewController: UIViewController {

@IBOutlet var label: UILabel!

@IBOutlet var label2: UILabel!


private lazy var lineShape: CAShapeLayer = {
    var color = hexStringToUIColor(hex: "#5DBCD2")
    let lineShape = CAShapeLayer()
    lineShape.strokeColor = UIColor.blue.cgColor
    lineShape.fillColor = color.cgColor
    lineShape.lineWidth = 2.0

    return lineShape
}()
private var panGestureStartPoint: CGPoint = .zero
private lazy var panRecognizer: UIPanGestureRecognizer = {
    return UIPanGestureRecognizer(target: self, action: #selector(panGestureCalled(_:)))
}()

override func viewDidLoad() {
    super.viewDidLoad()

    self.label.addGestureRecognizer(panRecognizer)
}

@objc func panGestureCalled(_: UIPanGestureRecognizer) {
    let currentPanPoint = panRecognizer.location(in: self.view)
    switch panRecognizer.state {
    case .began:
        panGestureStartPoint = currentPanPoint
        self.view.layer.addSublayer(lineShape)

    case .changed:
        let linePath = UIBezierPath()
        linePath.move(to: panGestureStartPoint)
        linePath.addLine(to: currentPanPoint)

        lineShape.path = linePath.cgPath
        lineShape.path = CGPath.barbell(from: panGestureStartPoint, to: currentPanPoint, barThickness: 2.0, bellRadius: 6.0)

    case .ended:
        lineShape.path = nil
        lineShape.removeFromSuperlayer()
    default: break
    }
}

func hexStringToUIColor (hex:String) -> UIColor {
    var cString:String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()

    if (cString.hasPrefix("#")) {
        cString.remove(at: cString.startIndex)
    }

    if ((cString.count) != 6) {
        return UIColor.gray
    }

    var rgbValue:UInt32 = 0
    Scanner(string: cString).scanHexInt32(&rgbValue)

    return UIColor(
        red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
        green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
        blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
        alpha: CGFloat(1.0)
    )
}
}

extension CGPath {
    class func barbell(from start: CGPoint, to end: CGPoint, barThickness proposedBarThickness: CGFloat, bellRadius proposedBellRadius: CGFloat) -> CGPath {
        let barThickness = max(0, proposedBarThickness)
        let bellRadius = max(barThickness / 2, proposedBellRadius)

        let vector = CGPoint(x: end.x - start.x, y: end.y - start.y)
        let length = hypot(vector.x, vector.y)

        if length == 0 {
            return CGPath(ellipseIn: CGRect(origin: start, size: .zero).insetBy(dx: -bellRadius, dy: -bellRadius), transform: nil)
        }

        var yOffset = barThickness / 2
        var xOffset = sqrt(bellRadius * bellRadius - yOffset * yOffset)
        let halfLength = length / 2
        if xOffset > halfLength {
            xOffset = halfLength
            yOffset = sqrt(bellRadius * bellRadius - xOffset * xOffset)
        }

        let jointRadians = asin(yOffset / bellRadius)
        let path = CGMutablePath()
        path.addArc(center: .zero, radius: bellRadius, startAngle: jointRadians, endAngle: -jointRadians, clockwise: false)
        path.addArc(center: CGPoint(x: length, y: 0), radius: bellRadius, startAngle: .pi + jointRadians, endAngle: .pi - jointRadians, clockwise: false)
        path.closeSubpath()

        let unitVector = CGPoint(x: vector.x / length, y: vector.y / length)
        var transform = CGAffineTransform(a: unitVector.x, b: unitVector.y, c: -unitVector.y, d: unitVector.x, tx: start.x, ty: start.y)
        return path.copy(using: &transform)!
    }
}

The code above replicates the gif animation. I used the questions below to create it if that helps.

Draw a line that can stretch like the Xcode assistant editor in Swift

How to reproduce this Xcode blue drag line

I think the answer is in the above question but I can't get my head around it as it's for MacOS and not iOS.

Any help is much appreciated

UPDATE

From the really good answers given, I added the code below in AddedPanGesture(). This isn't the finish version of the code.

 case .changed:
        let linePath = UIBezierPath()
        linePath.move(to: panGestureStartPoint)
        linePath.addLine(to: currentPanPoint)

        lineShape.path = linePath.cgPath
        lineShape.path = CGPath.barbell(from: panGestureStartPoint, to: currentPanPoint, barThickness: 2.0, bellRadius: 6.0)

        let labels = [label2, label3]
        for labelPoint in labels {
            let point = panRecognizer.location(in: labelPoint)

            if labelPoint!.layer.contains(point){
                labelPoint!.layer.borderWidth =  10
                labelPoint!.layer.borderColor =  UIColor.green.cgColor
            } else {
                labelPoint!.layer.borderWidth =  0
                labelPoint!.layer.borderColor =  UIColor.clear.cgColor

            }
        }

    case .ended:
        let labels = [label2, label3]
        for labelPoint in labels {
            let point = panRecognizer.location(in: labelPoint)

            if labelPoint!.layer.contains(point){
                labelPoint!.layer.borderWidth =  2
                labelPoint!.layer.borderColor =  UIColor.green.cgColor
            } else {
                lineShape.path = nil
                lineShape.removeFromSuperlayer()
                labelPoint!.layer.borderWidth =  0
                labelPoint!.layer.borderColor =  UIColor.clear.cgColor

            }
        }

enter image description here

If there is a better way I would love the input. Thanks!


Solution

  • I just update the code for your convenience.

     @objc func panGestureCalled(_: UIPanGestureRecognizer) {
        let currentPanPoint = panRecognizer.location(in: self.view)
        switch panRecognizer.state {
        case .began:
            panGestureStartPoint = currentPanPoint
            self.view.layer.addSublayer(lineShape)
    
        case .changed:
            let linePath = UIBezierPath()
            linePath.move(to: panGestureStartPoint)
            linePath.addLine(to: currentPanPoint)
    
            lineShape.path = linePath.cgPath
            lineShape.path = CGPath.barbell(from: panGestureStartPoint, to: currentPanPoint, barThickness: 2.0, bellRadius: 6.0)
    
    
            label2.layer.borderWidth =  hasDestine(panRecognizer) ? 10 :   0
            label2.layer.borderColor =  hasDestine(panRecognizer) ? UIColor.green.cgColor :   UIColor.clear.cgColor
    
        case .ended:
            if  (!hasDestine(panRecognizer)){
            lineShape.path = nil
            lineShape.removeFromSuperlayer()}
            label2.layer.borderWidth =   0
        default: break
        }
    }
    
    func hasDestine(_ panRecognizer: UIPanGestureRecognizer)-> Bool {
        let point = panRecognizer.location(in: label2)
        return   label2.layer.contains(point)
    }