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?
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
}
}
If there is a better way I would love the input. Thanks!
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)
}