Search code examples
swiftuibezierpath

Dashed UIBezierPath subpath junctions


I create dashed UIBezierPath using some passed points.

let pathLayer = CAShapeLayer()
pathLayer.strokeColor = UIColor.blue.cgColor
pathLayer.lineWidth = 3.0
pathLayer.fillColor = UIColor.clear.cgColor
pathLayer.lineDashPattern = [5.0, 2.0]
pathLayer.lineDashPhase = 0

let path = PathCreator.createPath(resultPoints)

pathLayer.path = path.cgPath

I've simplified PathCreator, but totally it looks something like this

PathCreator {
    func createPath(resultPoints: [CGPoint]) -> UIBezierPath {
        let path = UIBezierPath()
        for i in 0..<(resultPoints.count-1) {
            if resultPoints[i] is CircleCenterPoint {
                continue
            }
            if resultPoints[i] is ArcEndPoint {
                path.move(to: resultPoints[i].cg())
                path.addLine(to: resultPoints[i+1].cg())
            }
            if let arcStartPoint = resultPoints[i] as? ArcStartPoint,
               let circleCenterPoint = resultPoints[i+1] as? CircleCenterPoint,
               let arcEndPoint = resultPoints[i+2] as? ArcEndPoint {
                path.addArc(withCenter: circleCenterPoint.cg(),
                radius: circleCenterPoint.radius,
                startAngle: arcStartPoint.arcStartAngle,
                endAngle: arcEndPoint.arcEndAngle,
                clockwise: circleCenterPoint.clockwise)
            } else {
                path.move(to: resultPoints[i].cg())
                path.addLine(to: resultPoints[i+1].cg())
            }
        }
        return path
    }
}

But sometimes i have problems at the junction of subpaths:

path example

What am i missing?


Solution

  • I didn't follow this question for a long time. But now, when I came back, I found that I actually changed my code and it seems to work quite good. Here is the code that can be posted into playground and tested. Also this code doesn't expect any special types for points and other helper things. Check it out :)

    import UIKit
    import PlaygroundSupport
    
    class PathCreator {
        func createPath(by points: [CGPoint]) -> CGPath {
            let path = CGMutablePath()
            if points.count >= 3 {
                path.move(to: points.first!)
                for i in 0..<(points.count - 2) {
                    let startPoint = points[i]
                    let middlePoint = points[i+1]
                    let endPoint = points[i+2]
    
                    let isOnOneLin = isOnOneLine(point1: startPoint, point2: middlePoint, point3: endPoint)
                    if isOnOneLin {
                        path.addLine(to: middlePoint)
                        if i == points.count - 3 {
                            path.addLine(to: endPoint)
                            break
                        }
    
                    } else {
                        let currentAngleValue = angleBetween(vector1: (startPoint, middlePoint), vector2: (middlePoint, endPoint))
                        print(currentAngleValue)
                        if currentAngleValue < 45 {
                            path.addArc(tangent1End: middlePoint, tangent2End: endPoint, radius: 1)
                        } else {
                            path.addArc(tangent1End: middlePoint, tangent2End: endPoint, radius: 10)
                        }
                        if i == points.count - 3 {
                            path.addLine(to: endPoint)
                            break
                        }
                    }
                }
            } else if points.count == 2 {
                path.move(to: points.first!)
                path.addLine(to: points[1])
            }
    
            return path
        }
    
        private func angleBetween(vector1: (CGPoint, CGPoint), vector2: (CGPoint, CGPoint)) -> CGFloat {
    
            //  vector1.0
            //     ^
            //     \
            //      \
            //       \
            //        \
            //         ---------->vector2.1
            //     vector1.1
            //     vector2.0
    
            let dx1 = vector1.0.x - vector1.1.x
            let dy1 = vector1.0.y - vector1.1.y
            let vector1 = CGVector(dx: dx1, dy: dy1)
    
            let dx2 = vector2.1.x - vector2.0.x
            let dy2 = vector2.1.y - vector2.0.y
            let vector2 = CGVector(dx: dx2, dy: dy2)
    
            let scalarProduct = vector1.dx * vector2.dx + vector1.dy * vector2.dy
    
            let vector1Module = sqrt(vector1.dx * vector1.dx + vector1.dy * vector1.dy)
            let vector2Module = sqrt(vector2.dx * vector2.dx + vector2.dy * vector2.dy)
    
            let result = acos(scalarProduct / (vector1Module * vector2Module))
            return result * 180 / .pi
        }
    
        private func isOnOneLine(point1: CGPoint, point2: CGPoint, point3: CGPoint) -> Bool {
            let x1 = point1.x
            let x2 = point2.x
            let x3 = point3.x
            let y1 = point1.y
            let y2 = point2.y
            let y3 = point3.y
            let a = (x1 - x3) * (y2 - y3)
            let b = (x2 - x3) * (y1 - y3)
            let area = 1 / 2 * (a - b)
            if area != 0 {
                return false
            } else {
                return true
            }
        }
    }
    
    
    
    
    let str = "hello"
    
    let points = [CGPoint(x: 150, y: 150),
                  CGPoint(x: 350, y: 150),
                  CGPoint(x: 350, y: 350),
                  CGPoint(x: 550, y: 350),
                  CGPoint(x: 550, y: 450),
                  CGPoint(x: 205, y: 455),
                  CGPoint(x: 450, y: 650),
                  CGPoint(x: 185, y: 750)]
    
    let view = UIView(frame: CGRect(x: 0, y: 0, width: 1000, height: 1000))
    view.backgroundColor = .white
    PlaygroundPage.current.liveView = view
    
    let pathLayer = CAShapeLayer()
    pathLayer.strokeColor = UIColor.blue.cgColor
    pathLayer.lineWidth = 4.0
    pathLayer.fillColor = UIColor.clear.cgColor
    pathLayer.lineDashPattern = [5.0, 2.0]
    pathLayer.lineDashPhase = 0
    
    view.layer.addSublayer(pathLayer)
    
    let pathCreator = PathCreator()
    let path = pathCreator.createPath(by: points)
    pathLayer.path = path
    

    And the result is on the image:

    enter image description here

    Although the solution is probably not perfect. I think it can be helpful for someone.