Convert UIBezierPath in similar way to CGRect, CGPoint, etc

115 views Asked by At

UIView has functions to convert points, rects, etc. between coordinate spaces. I need to do the same thing for UIBezierPaths. By way of illustration, consider two views, viewA and viewB. We have rectA : CGRect in the coordinate space of viewA. We can calculate:

let rectB = viewB.convert(rectA, from:viewA)

Now suppose we create a path from the rect:

let pathA = UIBezierPath(rect: rectA)

The question is, how to calculated rectB. We could simply:

let pathB = UIBezierPath(rect: rectB)

But that works only if we have rectB. What if we only have `pathA'?

I figure to do this we need a transform aTob. Then we can do:

var pathB = pathA
pathB.apply(aTob)

So the question boils down to how to calculate aTob. For simplicity, assume there is no rotation.

With the result, drawing pathA in viewA should draw a path in the same shape and location on the screen as drawing pathB in viewB.

2

There are 2 answers

0
HangarRash On

One solution would be to call apply() on the bezier path using a translation transform.

You can calculate the needed x and y of the transform by converting the origin of viewB to the coordinate space of viewA.

Here's is some sample code you can put in a Swift Playground:

// Some setup
let rectA = CGRect(x: 40, y: 60, width: 200, height: 100)
let rectB = CGRect(x: 10, y: 80, width: 200, height: 100)
let viewA = UIView(frame: rectA)
let viewB = UIView(frame: rectB)
// Create pathA
let pathA = UIBezierPath(rect: rectA)

// Calculate the offset needed to move from viewA to viewB
let offset = viewA.convert(CGPoint.zero, from: viewB)
let tranform = CGAffineTransform(translationX: offset.x, y: offset.y)
// Copy pathA
let pathB = UIBezierPath()
pathB.append(pathA)
// Shift pathB
pathB.apply(tranform)
// Check result
print(pathA)
print(pathB)

Output:

<UIBezierPath: 0x600002c31e80; <MoveTo {40, 60}>,
 <LineTo {140, 60}>,
 <LineTo {140, 160}>,
 <LineTo {40, 160}>,
 <Close>
<UIBezierPath: 0x600002c12280; <MoveTo {10, 80}>,
 <LineTo {110, 80}>,
 <LineTo {110, 180}>,
 <LineTo {10, 180}>,
 <Close>

I believe this is what you are looking for. The calculation of offset might be backwards. If the result of pathB is in the wrong direction, change the offset line to:

let offset = viewB.convert(CGPoint.zero, from: viewA)

Here's an extension to UIView for converting a UIBezierPath from one view to/from another:

extension UIView {
    private func adjust(_ path: UIBezierPath, by offset: CGPoint) -> UIBezierPath {
        let transform = CGAffineTransform(translationX: offset.x, y: offset.y)
        let result = UIBezierPath()
        result.append(path)
        result.apply(tranform)

        return result
    }

    func convert(_ path: UIBezierPath, from view: UIView) -> UIBezierPath {
        let offset = convert(CGPoint.zero, from: view)

        return adjust(path, by: offset)
    }

    func convert(_ path: UIBezierPath, to view: UIView) -> UIBezierPath {
        let offset = convert(CGPoint.zero, to: view)

        return adjust(path, by: offset)
    }
}

This answer assumes that neither view has a non-identity transform applied to it. Based on the OP's own answer, apparently one or both of the views may have a non-identity transform.

5
Victor Engel On

Using an extension like the one HangarRash posted, here is a solution that works when both views have the same superview.

func convert(_ path: UIBezierPath, from view: UIView) -> UIBezierPath {
    path.apply(view.transform)
    // we now have a path in view's superview's coordinates. Let's adjust by the origin.
    let viewTransform = CGAffineTransform(translationX: view.frame.origin.x, y: view.frame.origin.y)
    path.apply(viewTransform)
    
    // now the path is adjusted by view's origin. Next we need to subtract self's origin
    let selfTranslationTransform = CGAffineTransform(translationX: -self.frame.origin.x, y: -self.frame.origin.y)
    path.apply(selfTranslationTransform)
    
    // now adjust by self's transform
    path.apply(self.transform.inverted())
    return path
}