135

I want to alter the anchorPoint, but keep the view in the same place. I've tried NSLog-ing self.layer.position and self.center and they both stay the same regardless of changes to the anchorPoint. Yet my view moves!

Any tips on how to do this?

self.layer.anchorPoint = CGPointMake(0.5, 0.5);
NSLog(@"center point: %f %f", self.layer.position.x, self.layer.position.y);
self.layer.anchorPoint = CGPointMake(1, 1);
NSLog(@"center point: %f %f", self.layer.position.x, self.layer.position.y);

The output is:

2009-12-27 20:43:24.161 Type[11289:207] center point: 272.500000 242.500000
2009-12-27 20:43:24.162 Type[11289:207] center point: 272.500000 242.500000
rounak
  • 9,217
  • 3
  • 42
  • 59
Kenny Winker
  • 11,919
  • 7
  • 56
  • 78

12 Answers12

210

I had the same problem. Brad Larson's solution worked great even when the view is rotated. Here is his solution translated into code.

-(void)setAnchorPoint:(CGPoint)anchorPoint forView:(UIView *)view
{
    CGPoint newPoint = CGPointMake(view.bounds.size.width * anchorPoint.x, 
                                   view.bounds.size.height * anchorPoint.y);
    CGPoint oldPoint = CGPointMake(view.bounds.size.width * view.layer.anchorPoint.x, 
                                   view.bounds.size.height * view.layer.anchorPoint.y);

    newPoint = CGPointApplyAffineTransform(newPoint, view.transform);
    oldPoint = CGPointApplyAffineTransform(oldPoint, view.transform);

    CGPoint position = view.layer.position;

    position.x -= oldPoint.x;
    position.x += newPoint.x;

    position.y -= oldPoint.y;
    position.y += newPoint.y;

    view.layer.position = position;
    view.layer.anchorPoint = anchorPoint;
}

And the swift equivalent:

func setAnchorPoint(anchorPoint: CGPoint, forView view: UIView) {
    var newPoint = CGPointMake(view.bounds.size.width * anchorPoint.x, view.bounds.size.height * anchorPoint.y)
    var oldPoint = CGPointMake(view.bounds.size.width * view.layer.anchorPoint.x, view.bounds.size.height * view.layer.anchorPoint.y)

    newPoint = CGPointApplyAffineTransform(newPoint, view.transform)
    oldPoint = CGPointApplyAffineTransform(oldPoint, view.transform)

    var position = view.layer.position
    position.x -= oldPoint.x
    position.x += newPoint.x

    position.y -= oldPoint.y
    position.y += newPoint.y

    view.layer.position = position
    view.layer.anchorPoint = anchorPoint
}

SWIFT 4.x

func setAnchorPoint(anchorPoint: CGPoint, forView view: UIView) {
    var newPoint = CGPoint(x: view.bounds.size.width * anchorPoint.x,
                           y: view.bounds.size.height * anchorPoint.y)


    var oldPoint = CGPoint(x: view.bounds.size.width * view.layer.anchorPoint.x,
                           y: view.bounds.size.height * view.layer.anchorPoint.y)

    newPoint = newPoint.applying(view.transform)
    oldPoint = oldPoint.applying(view.transform)

    var position = view.layer.position
    position.x -= oldPoint.x
    position.x += newPoint.x

    position.y -= oldPoint.y
    position.y += newPoint.y

    view.layer.position = position
    view.layer.anchorPoint = anchorPoint
}
dahiya_boy
  • 9,298
  • 1
  • 30
  • 51
Magnus
  • 2,127
  • 1
  • 12
  • 3
  • 2
    I use this code in my awakeFromNib/initWithFrame methods, but it still doesn't work, do I need to update the display? edit: setNeedsDisplay doesn't work – Adam Carter Dec 16 '12 at 14:52
  • 2
    Why are you using view.bounds? Shouldn't you be using layer.bounds? – zakdances Apr 11 '13 at 10:20
  • 1
    in my case setting value to: view.layer.position does not change anything – AndrewK Jul 09 '15 at 21:38
  • @magnus Please can you tell how can I solve this anchor point jumping who's UIView is set using auto layout, means with auto layout we are not suppose to get or set frames and bounds properties. – S.J Aug 27 '15 at 09:32
  • 5
    FWIW, I had to wrap the setAnchor method in a dispatch_async block for it to work. Not sure why as I'm calling it inside viewDidLoad. Is viewDidLoad no longer on the main thread queue? – John Fowler Sep 23 '15 at 08:56
  • The reason it did not work for some people, or had to be wrapped in dispatch_async, could be because layout is not complete in `awakeFromNib()`. – Yuval Tal Nov 18 '16 at 04:03
  • 1
    @Magnus You are a genius. This works 100% perfectly. The only thing I changed was the UIView argument for CALayer argument, and all the references thereafter. This allows your solution to work for sub layers. And I had to remove the CGPointApplyAffineTransform lines. I have up voted this and why hasn't this been accepted as the official answer? And you have provided Swift/objective c solutions. Brilliant work. Thanks... – Charles Robertson Nov 23 '16 at 17:53
  • `translatesAutoresizingMaskIntoConstraints = true` is missing, then it will always work. Should be preferred over an `DispatchQueue.main.async` block – appsunited Jul 22 '20 at 18:11
  • Can anybody confirm if the answer is still valid for Swift 5 and iOS 13+? Adding a ```print``` just after setting newly calculated position shows correct results, but the view doesn't seem to refresh. Applying and animating subsequent transforms are indeed relative to the new anchor point, but position is still not correct. ```DispatchQueue.main.async``` works in this case, but I get a lot of warnings of manipulating UI in a background thread. I'm calling this method in ```viewDidLoad``` and I have a few constraints making my view appear in top-left safe area corner. – Rosen Dimov Sep 29 '20 at 11:24
142

The Layer Geometry and Transforms section of the Core Animation Programming Guide explains the relationship between a CALayer's position and anchorPoint properties. Basically, the position of a layer is specified in terms of the location of the layer's anchorPoint. By default, a layer's anchorPoint is (0.5, 0.5), which lies at the center of the layer. When you set the position of the layer, you are then setting the location of the center of the layer in its superlayer's coordinate system.

Because the position is relative to the anchorPoint of the layer, changing that anchorPoint while maintaining the same position moves the layer. In order to prevent this movement, you would need to adjust the layer's position to account for the new anchorPoint. One way I've done this is to grab the layer's bounds, multiply the bounds' width and height by the old and new anchorPoint's normalized values, take the difference of the two anchorPoints, and apply that difference to the position of the layer.

You might even be able to account for rotation this way by using CGPointApplyAffineTransform() with your UIView's CGAffineTransform.

Brad Larson
  • 170,088
  • 45
  • 397
  • 571
  • 4
    See the example function from Magnus to see how Brad's answer can be implemented in Objective-C. – Jim Jeffers Feb 09 '12 at 05:30
  • Thanks but I don't understand how the `anchorPoint` and `position` are related together? – dumbfingers Feb 06 '13 at 12:11
  • 7
    @ss1271 - As I describe above, the anchorPoint of a layer is the basis of its coordinate system. If it is set to (0.5, 0.5), then when you set the position for a layer, you are setting where its center will lie in the coordinate system of its superlayer. If you set the anchorPoint to (0.0, 0.5), setting the position will set where the center of its left edge will be. Again, Apple has some nice images in the above-linked documentation (which I've updated the reference to). – Brad Larson Feb 06 '13 at 15:27
  • I used the method provided by Magnus. When I NSLog position and frame they look correct but my view is still moved. Any idea why? It's like the new position isn't taken into account. – Alexis Jun 19 '14 at 13:29
  • In my case I want to add an animation on the layer using the AVVideoCompositionCoreAnimationTool class's videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer method. What happens is, half of my layer keeps getting dissapeared while I am rotating it around y - axis. – nr5 Oct 11 '14 at 08:13
  • @BradLarson Please can you tell how can I solve this anchor point jumping who's UIView is set using auto layout, means with auto layout we are not suppose to get or set frames and bounds properties. – S.J Aug 27 '15 at 09:31
  • Even a disguised RTFM comment means a down point. Too bad, otherwise it's a good note. – johnrubythecat Nov 01 '17 at 22:07
  • @BradLarson I got your point too. The key to understanding this is "The position is relative to the anchorPoint". – songgeb Nov 03 '19 at 11:09
  • I wrote it under @Magnus' answer, but it seems he hasn't logged in for years, so I'd ask here too - can anyone confirm if this approach is still valid for Swift 5 and iOS 13+? Transform animation is done according to the new anchor point, but the position is still not visually updated in the first place. – Rosen Dimov Sep 29 '20 at 15:32
47

The key to solving this was to use the frame property, which is weirdly the only thing that changes.

Swift 2

let oldFrame = self.frame
self.layer.anchorPoint = CGPointMake(1, 1)
self.frame = oldFrame

Swift 3

let oldFrame = self.frame
self.layer.anchorPoint = CGPoint(x: 1, y: 1)
self.frame = oldFrame

Then I do my resize, where it scales from the anchorPoint. Then I have to restore the old anchorPoint;

Swift 2

let oldFrame = self.frame
self.layer.anchorPoint = CGPointMake(0.5,0.5)
self.frame = oldFrame

Swift 3

let oldFrame = self.frame
self.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.frame = oldFrame

EDIT: this flakes out if the view is rotated, as the frame property is undefined if a CGAffineTransform has been applied.

luk2302
  • 55,258
  • 23
  • 97
  • 137
Kenny Winker
  • 11,919
  • 7
  • 56
  • 78
  • 10
    Just wanted to add why this works and seems to be the easiest way: according to the docs, the frame property is a "composed" property: *"The value of frame is derived from the bounds, anchorPoint and position properties."* That's also why the "frame" property isn't animatable. – DarkDust Nov 24 '10 at 09:36
  • 2
    This is honestly my preferred approach. If you aren't trying to manipulate the bounds and position independently or animate either of them, just capturing the frame before you change the anchor point is usually sufficient. No math necessary. – CIFilter Oct 06 '16 at 19:33
28

For me understanding position and anchorPoint was easiest when I started comparing it with my understanding of frame.origin in UIView. A UIView with frame.origin = (20,30) means that the UIView is 20 points from left and 30 points from top of its parent view. This distance is calculated from which point of a UIView? Its calculated from top-left corner of a UIView.

In layer anchorPoint marks the point (in normalized form i.e. 0 to 1) from where this distance is calculated so e.g. layer.position = (20, 30) means that the layer anchorPoint is 20 points from left and 30 points from top of its parent layer. By default a layer anchorPoint is (0.5, 0.5) so the distance calculation point is right in the center of the layer. The following figure will help clarify my point:

enter image description here

anchorPoint also happens to be the point around which rotation will happen in case you apply a transform to the layer.

2cupsOfTech
  • 5,953
  • 4
  • 34
  • 57
  • 1
    Please can you tell how can I solve this anchor point jumping who's UIView is set using auto layout, means with auto layout we are not suppose to get or set frames and bounds properties. – S.J Aug 27 '15 at 09:35
18

There is such a simple solution. This is based on Kenny's answer. But instead of applying the old frame, use it's origin and the new one to calculate the translation, then apply that translation to the center. It works with rotated view too! Here's the code, a lot simpler than other solutions:

func setAnchorPoint(anchorPoint: CGPoint, view: UIView) {
   let oldOrigin = view.frame.origin
   view.layer.anchorPoint = anchorPoint
   let newOrigin = view.frame.origin

   let translation = CGPoint(x: newOrigin.x - oldOrigin.x, y: newOrigin.y - oldOrigin.y)

   view.center = CGPoint(x: view.center.x - translation.x, y: view.center.y - translation.y)
}
Fried Rice
  • 3,295
  • 3
  • 18
  • 27
  • 2
    Nice little method... works ok with a couple minor fixes: Line 3: capital P in anchorPoint. Line 8: TRANS.Y = NEW - OLD – bob Dec 06 '13 at 08:26
  • 2
    still i am in confusion how can I use it. I am facing same problem right now for rotating my view with uigesturerotation. I am in strange that where should I call this method. If I am calling in gesture recognizer handler. It makes view disappear. – JAck Aug 16 '16 at 09:30
  • 3
    This answer is so sweet I now have diabetes. – GeneCode Apr 23 '17 at 04:37
17

For those who need it, here is Magnus's solution in Swift:

func setAnchorPoint(anchorPoint: CGPoint, view: UIView) {
    var newPoint: CGPoint = CGPointMake(view.bounds.size.width * anchorPoint.x, view.bounds.size.height * anchorPoint.y)
    var oldPoint: CGPoint = CGPointMake(view.bounds.size.width * view.layer.anchorPoint.x, view.bounds.size.height * view.layer.anchorPoint.y)

    newPoint = CGPointApplyAffineTransform(newPoint, view.transform)
    oldPoint = CGPointApplyAffineTransform(oldPoint, view.transform)

    var position: CGPoint = view.layer.position

    position.x -= oldPoint.x
    position.x += newPoint.x

    position.y -= oldPoint.y
    position.y += newPoint.y

    view.setTranslatesAutoresizingMaskIntoConstraints(true)     // Added to deal with auto layout constraints
    view.layer.anchorPoint = anchorPoint
    view.layer.position = position
}
Corey Davis
  • 185
  • 3
  • 8
  • 2
    upvote for `view.setTranslatesAutoresizingMaskIntoConstraints(true)`. thanks man – Oscar Yuandinata Mar 22 '18 at 03:09
  • 2
    `view.setTranslatesAutoresizingMaskIntoConstraints(true)` is necessary when use auto layout constraints. – SamirChen Jun 08 '18 at 09:56
  • 2
    Thank you so much for this! The `translatesAutoresizingMaskIntoConstraints` is just what I was missing, could've learned by now – zbx Aug 22 '18 at 07:43
6

Here is user945711's answer adjusted for NSView on OS X. Besides NSView not having a .center property, the NSView's frame doesn't change (probably because NSViews do not come with a CALayer by default) but the CALayer frame origin changes when the anchorPoint is changed.

func setAnchorPoint(anchorPoint: NSPoint, view: NSView) {
    guard let layer = view.layer else { return }

    let oldOrigin = layer.frame.origin
    layer.anchorPoint = anchorPoint
    let newOrigin = layer.frame.origin

    let transition = NSMakePoint(newOrigin.x - oldOrigin.x, newOrigin.y - oldOrigin.y)
    layer.frame.origin = NSMakePoint(layer.frame.origin.x - transition.x, layer.frame.origin.y - transition.y)
}
Shmidt
  • 16,436
  • 18
  • 88
  • 136
iMaddin
  • 982
  • 1
  • 14
  • 23
4

If you change anchorPoint, its position will change too, UNLESS you origin is zero point CGPointZero.

position.x == origin.x + anchorPoint.x;
position.y == origin.y + anchorPoint.y;
Mahendra
  • 8,448
  • 3
  • 33
  • 56
4

Edit and See UIView's Anchor Point Right on Storyboard (Swift 3)

This is an alternate solution which allows you to change the anchor point through the Attributes Inspector and has another property to view the anchor point for confirmation.

Create new file to include in your project

import UIKit

@IBDesignable
class UIViewAnchorPoint: UIView {

    @IBInspectable var showAnchorPoint: Bool = false
    @IBInspectable var anchorPoint: CGPoint = CGPoint(x: 0.5, y: 0.5) {
        didSet {
            setAnchorPoint(anchorPoint: anchorPoint)
        }
    }

    override func draw(_ rect: CGRect) {
        if showAnchorPoint {
            let anchorPointlayer = CALayer()
            anchorPointlayer.backgroundColor = UIColor.red.cgColor
            anchorPointlayer.bounds = CGRect(x: 0, y: 0, width: 6, height: 6)
            anchorPointlayer.cornerRadius = 3

            let anchor = layer.anchorPoint
            let size = layer.bounds.size

            anchorPointlayer.position = CGPoint(x: anchor.x * size.width, y: anchor.y * size.height)
            layer.addSublayer(anchorPointlayer)
        }
    }

    func setAnchorPoint(anchorPoint: CGPoint) {
        var newPoint = CGPoint(x: bounds.size.width * anchorPoint.x, y: bounds.size.height * anchorPoint.y)
        var oldPoint = CGPoint(x: bounds.size.width * layer.anchorPoint.x, y: bounds.size.height * layer.anchorPoint.y)

        newPoint = newPoint.applying(transform)
        oldPoint = oldPoint.applying(transform)

        var position = layer.position
        position.x -= oldPoint.x
        position.x += newPoint.x

        position.y -= oldPoint.y
        position.y += newPoint.y

        layer.position = position
        layer.anchorPoint = anchorPoint
    }
}

Add View to Storyboard and set the Custom Class

Custom Class

Now set the New Anchor Point for the UIView

Demonstration

Turning on the Show Anchor Point will show a red dot so you can better see where the anchor point will be visually. You can always turn it off later.

This really helped me when planning transforms on UIViews.

Mark Moeykens
  • 15,915
  • 6
  • 63
  • 62
  • No one dot on UIView, i used Objective-C with Swift, attached UIView custom class from your code and switched on Show anch...but no one dot – Genevios Apr 20 '18 at 07:31
1

For Swift 3:

func setAnchorPoint(_ anchorPoint: CGPoint, forView view: UIView) {
    var newPoint = CGPoint(x: view.bounds.size.width * anchorPoint.x, y: view.bounds.size.height * anchorPoint.y)
    var oldPoint = CGPoint(x: view.bounds.size.width * view.layer.anchorPoint.x, y: view.bounds.size.height * view.layer.anchorPoint.y)

    newPoint = newPoint.applying(view.transform)
    oldPoint = oldPoint.applying(view.transform)

    var position = view.layer.position
    position.x -= oldPoint.x
    position.x += newPoint.x

    position.y -= oldPoint.y
    position.y += newPoint.y

    view.layer.position = position
    view.layer.anchorPoint = anchorPoint
}
AJ9
  • 1,256
  • 1
  • 17
  • 28
  • 1
    Thank you for this :) I just pasted it into my project as a static func and works like a charm :D – Kjell Dec 12 '16 at 18:12
0

Expanding on Magnus' great & thorough answer, I have created a version that works on sub layers:

-(void)setAnchorPoint:(CGPoint)anchorPoint forLayer:(CALayer *)layer
{
    CGPoint newPoint = CGPointMake(layer.bounds.size.width * anchorPoint.x, layer.bounds.size.height * anchorPoint.y);
    CGPoint oldPoint = CGPointMake(layer.bounds.size.width * layer.anchorPoint.x, layer.bounds.size.height * layer.anchorPoint.y);
    CGPoint position = layer.position;
    position.x -= oldPoint.x;
    position.x += newPoint.x;
    position.y -= oldPoint.y;
    position.y += newPoint.y;
    layer.position = position;
    layer.anchorPoint = anchorPoint;
}
Charles Robertson
  • 1,760
  • 16
  • 21
0

Let a be the center of the layer in terms of anchorPoint coords (0.5, 0.5). Think unit coordinates/or uv coords if you are familiar with Metal/Vulkan/OpenGL/DirectX. Let n be the new anchor position (x, y). Let d be the dimensions of the bounds of the layer, (width, height).

Then to move back the layer to its original position after a layer anchorPoint position change, compute the offset vector from a to n multiplied by d: v = d(n-a). So then, to get your layer back to it's original position, add offset vector to the layer.position: layer.position += v

Swift/iOS

let vx = (layer.anchorPoint.x - 0.5) * layer.bounds.width
let vy = (layer.anchorPoint.y - 0.5) * layer.bounds.height
layer.position.x += vx
layer.position.y += vy

Your view.center moves when the layer.position changes(and vice versa) if the layer is the view's backing layer. If it was a sublayer.position, then that would not move view.center. So if in the above code, the layer was the view's backing layer, I could have offset view.center instead of layer.position, both would have worked.

joqqy
  • 408
  • 4
  • 11