5
votes

I am currently converting an MKPolyline to a BezierPath then a CAShapeLayer then adding the layer as a sublayer to a UIView. Currently struggling to ensure the path is not drawn outside the bounds of the UIView. I do not want to mask and have part of the path dissapear, but rather ensure that every point is resized and positioned in the center of the UIView.

func addPathToView() {
    guard let path = createPath(onView: polylineView) else { return }
    path.fit(into: polylineView.bounds).moveCenter(to: polylineView.center).fill()
    path.lineWidth     = 3.0
    path.lineJoinStyle = .round

    guard let layer  = createCAShapeLayer(fromBezierPath: path) else { return }
    layer.path       = getScaledPath(fromPath: path, layer: layer)
    layer.frame      = polylineView.bounds
    layer.position.x = polylineView.bounds.minX
    layer.position.y = polylineView.bounds.minY

    polylineView.layer.addSublayer(layer)
}

func createCAShapeLayer( fromBezierPath path: UIBezierPath? ) -> CAShapeLayer? {
    guard let path = path else { print("No Path"); return nil }
    let pathLayer = CAShapeLayer(path: path, lineColor: UIColor.red, fillColor: UIColor.clear)
    return pathLayer
}

func createPath( onView view: UIView? ) -> UIBezierPath? {
    guard let polyline = Polyline().createPolyline(forLocations: locations) else { print("No Polyline"); return nil }
    guard let points   = convertMapPointsToCGPoints(fromPolyline: polyline) else { print("No CGPoints"); return nil }

    let path = UIBezierPath(points: points)

    return path
}

func convertMapPointsToCGPoints( fromPolyline polyline: MKPolyline? ) -> [CGPoint]? {
    guard let polyline = polyline else { print( "No Polyline"); return nil }

    let mapPoints = polyline.points()

    var points = [CGPoint]()

    for point in 0..<polyline.pointCount {
        let coordinate = MKCoordinateForMapPoint(mapPoints[point])
        points.append(mapView.convert(coordinate, toPointTo: view))
    }

    return points
}

func getScaledPath( fromPath path: UIBezierPath, layer: CAShapeLayer ) -> CGPath? {
    let boundingBox = path.cgPath.boundingBoxOfPath

    let boundingBoxAspectRatio = boundingBox.width / boundingBox.height
    let viewAspectRatio = polylineView.bounds.size.width / polylineView.bounds.size.height

    let scaleFactor: CGFloat
    if (boundingBoxAspectRatio > viewAspectRatio) {
        // Width is limiting factor
        scaleFactor = polylineView.bounds.size.width / boundingBox.width
    } else {
        // Height is limiting factor
        scaleFactor = polylineView.bounds.size.height/boundingBox.height
    }

    var affineTransorm = CGAffineTransform(scaleX: scaleFactor, y: scaleFactor)
    let transformedPath = path.cgPath.copy(using: &affineTransorm)

    guard let tPath = transformedPath else { print ("nope"); return nil }

    return tPath
}

extension UIBezierPath
{
    func moveCenter(to:CGPoint) -> Self{
        let bound  = self.cgPath.boundingBox
        let center = bounds.center

        let zeroedTo = CGPoint(x: to.x-bound.origin.x, y: to.y-bound.origin.y)
        let vector = center.vector(to: zeroedTo)

        offset(to: CGSize(width: vector.dx, height: vector.dy))
        return self
    }

    func offset(to offset:CGSize) -> Self{
        let t = CGAffineTransform(translationX: offset.width, y: offset.height)
        applyCentered(transform: t)
        return self
    }

    func fit(into:CGRect) -> Self{
        let bounds = self.cgPath.boundingBox

        let sw     = into.size.width/bounds.width
        let sh     = into.size.height/bounds.height
        let factor = min(sw, max(sh, 0.0))

        return scale(x: factor, y: factor)
    }

    func scale(x:CGFloat, y:CGFloat) -> Self{
        let scale = CGAffineTransform(scaleX: x, y: y)
        applyCentered(transform: scale)
        return self
    }

    func applyCentered(transform: @autoclosure () -> CGAffineTransform ) -> Self{
        let bound  = self.cgPath.boundingBox
        let center = CGPoint(x: bound.midX, y: bound.midY)
        var xform  = CGAffineTransform.identity

        xform = xform.concatenating(CGAffineTransform(translationX: -center.x, y: -center.y))
        xform = xform.concatenating(transform())
        xform = xform.concatenating( CGAffineTransform(translationX: center.x, y: center.y))
        apply(xform)

        return self
    }
}

extension UIBezierPath
{
    convenience init(points:[CGPoint])
    {
        self.init()

        //connect every points by line.
        //the first point is start point
        for (index,aPoint) in points.enumerated()
        {
            if index == 0 {
                self.move(to: aPoint)
            }
            else {
                self.addLine(to: aPoint)
            }
        }
    }
}

//2. To create layer use this extension

extension CAShapeLayer
{
    convenience init(path:UIBezierPath, lineColor:UIColor, fillColor:UIColor)
    {
        self.init()
        self.path = path.cgPath
        self.strokeColor = lineColor.cgColor
        self.fillColor = fillColor.cgColor
        self.lineWidth = path.lineWidth

        self.opacity = 1
        self.frame = path.bounds
    }
}

enter image description here

2
why do you offset in your moveCenter function?Ocunidee
@Ocunidee the UIBezierPathExtension was an attempt found online to solve my problem link Currently I feel like I am just putting together parts of different puzzles and barely making something work which is why I came to stack overflow for help! Can't pinpoint why it is that I cant simply resize the layer, set its frame to the bounds of the uiview and be done.lifewithelliott
have you tried commenting out the part about the offset?Ocunidee
@Ocunidee yes, I feel as if I can approach the bezierpath positioning in a completely different manner. Just not aware of the best approachlifewithelliott
If you want, I can post an exemple on how I use BezierPath and CAShapeLayer to draw to a given scale which I don't know in advance. However the exemple won't contain anything about MKPolylineOcunidee

2 Answers

3
votes

A UIBezierPath can be scaled just like a CGRect, CGPoint or 'CGSize' using an CGAffineTransform. 🐙

// calculate the scale
//
let scaleWidth  = toSize.width / fromSize.width
let scaleHeight = toSize.height / fromSize.height

// re-scale the path
//
path.apply(CGAffineTransform(scaleX: scaleWidth, y: scaleHeight))
1
votes

Here is an approach I use to scale a UIBezierPath: I will use original (your MKPolyline size, my original data) and final (the receiving view size, how it will be displayed).

1.Calculate the original amplitude (for me it was just the height but for you it will be the width as well)

2.Write a function to scale your original data to the new X and Y axis scales (for a point position it would look like this):

func scaleValueToYAxis(_ value: Double) -> CGFloat {
    return finalHeight - CGFloat(value) / originalYAmplitude) * finalHeight
}

func scaleValueToXAxis(_ value: Double) -> CGFloat {
     return finalWidth - CGFloat(value) / originalXAmplitude) * finalWidth

}

3.Start drawing

let path = UIBezierPath()
let path.move(to: CGPoint(x: yourOriginForDrawing, y: yourOriginForDrawing)) // final scale position

path.addLine(to: CGPoint(x: nextXPoint, y: nextYPoint)) // this is not relevant for you as you don't draw point by point
// what is important here is the fact that you take your original
//data X and Y and make them go though your scale functions 

let layer = CAShapeLayer()
let layer.path = path.cgPath
let layer.lineWidth = 1.0
let layer.strokeColor = UIColor.black

yourView.layer.addSublayer(layer)

As you can see the logic about drawing from MKPolyline remains to be done. What does matter is that when you "copy" the polyline you move(to: ) the right point to do it. This is why i'm thinking you don't have the right offset