Rotate CGPath without changing its position

A Swift 3 method to rotate a rectangle in place around its center:

func createRotatedCGRect(rect: CGRect, radians: CGFloat) -> CGPath {
    let center = CGPoint(x: rect.midX, y: rect.midY)
    let path = UIBezierPath(rect: rect)
    path.apply(CGAffineTransform(translationX: center.x, y: center.y).inverted())
    path.apply(CGAffineTransform(rotationAngle: radians))
    path.apply(CGAffineTransform(translationX: center.x, y: center.y))
    return path.cgPath
}

All of rob mayoff's code for applying this to a path and its bounding box would still apply, so I didn't see cause to repeat them.


A path doesn't have “a position”. A path is a set of points (defined by line and curve segments). Every point has its own position.

Perhaps you want to rotate the path around a particular point, instead of around the origin. The trick is to create a composite transform that combines three individual transforms:

  1. Translate the origin to the rotation point.
  2. Rotate.
  3. Invert the translation from step 1.

For example, here's a function that takes a path and returns a new path that is the original path rotated around the center of its bounding box:

static CGPathRef createPathRotatedAroundBoundingBoxCenter(CGPathRef path, CGFloat radians) {
    CGRect bounds = CGPathGetBoundingBox(path); // might want to use CGPathGetPathBoundingBox
    CGPoint center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
    CGAffineTransform transform = CGAffineTransformIdentity;
    transform = CGAffineTransformTranslate(transform, center.x, center.y);
    transform = CGAffineTransformRotate(transform, radians);
    transform = CGAffineTransformTranslate(transform, -center.x, -center.y);
    return CGPathCreateCopyByTransformingPath(path, &transform);
}

Note that this function returns a new path with a +1 retain count that you are responsible for releasing when you're done with it. For example, if you're trying to rotate the path of a shape layer:

- (IBAction)rotateButtonWasTapped:(id)sender {
    CGPathRef path = createPathRotatedAroundBoundingBoxCenter(shapeLayer_.path, M_PI / 8);
    shapeLayer_.path  = path;
    CGPathRelease(path);
}

UPDATE

Here's a demonstration using a Swift playground. We'll start with a helper function that displays a path and marks the origin with a crosshair:

import UIKit
import XCPlayground

func showPath(label: String, path: UIBezierPath) {
    let graph = UIBezierPath()
    let r = 40
    graph.moveToPoint(CGPoint(x:0,y:r))
    graph.addLineToPoint(CGPoint(x:0,y:-r))
    graph.moveToPoint(CGPoint(x:-r,y:0))
    graph.addLineToPoint(CGPoint(x:r,y:0))
    graph.appendPath(path)
    XCPCaptureValue(label, graph)
}

Next, here's our test path:

var path = UIBezierPath()
path.moveToPoint(CGPoint(x:1000,y:1000))
path.addLineToPoint(CGPoint(x:1000,y:1200))
path.addLineToPoint(CGPoint(x:1100,y:1200))
showPath("original", path)

original path

(Remember, the crosshair is the origin and is not part of the path we're transforming.)

We get the center and transform the path so it's centered at the origin:

let bounds = CGPathGetBoundingBox(path.CGPath)
let center = CGPoint(x:CGRectGetMidX(bounds), y:CGRectGetMidY(bounds))

let toOrigin = CGAffineTransformMakeTranslation(-center.x, -center.y)
path.applyTransform(toOrigin)
showPath("translated center to origin", path)

path translated to origin

Then we rotate it. All rotations happen around the origin:

let rotation = CGAffineTransformMakeRotation(CGFloat(M_PI / 3.0))
path.applyTransform(rotation)
showPath("rotated", path)

path rotated

Finally, we translate it back, exactly inverting the original translation:

let fromOrigin = CGAffineTransformMakeTranslation(center.x, center.y)
path.applyTransform(fromOrigin)
showPath("translated back to original center", path)

path translated from origin

Note that we must invert the original translation. We don't translate by the center of its new bounding box. Recall that (in this example), the original center is at (1050,1100). But after we've translated it to the origin and rotated it, the new bounding box's center is at (-25,0). Translating the path by (-25,0) will not put it anywhere close to its original position!


A Swift 5 version:

func rotate(path: UIBezierPath, degree: CGFloat) {
    let bounds: CGRect = path.cgPath.boundingBox
    let center = CGPoint(x: bounds.midX, y: bounds.midY)

    let radians = degree / 180.0 * .pi
    var transform: CGAffineTransform = .identity
    transform = transform.translatedBy(x: center.x, y: center.y)
    transform = transform.rotated(by: radians)
    transform = transform.translatedBy(x: -center.x, y: -center.y)
    path.apply(transform)
}