4

Swift 4, macOS 10.13

I have read a variety of answers on SO and still can't get an NSImageView to spin at its center instead of one of its corners.

Right now, the image looks like this (video): http://d.pr/v/kwiuwS

Here is my code:

//`loader` is an NSImageView on my storyboard positioned with auto layout

loader.wantsLayer = true

let oldFrame = loader.layer?.frame
loader.layer?.anchorPoint = CGPoint(x: 0.5, y: 0.5)
loader.layer?.position = CGPoint(x: 0.5, y: 0.5)
loader.layer?.frame = oldFrame!

let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(-1 * .pi * 2.0)
rotateAnimation.duration = 2
rotateAnimation.repeatCount = .infinity

loader.layer?.add(rotateAnimation, forKey: nil)

Any ideas what I am still missing?

Clifton Labrum
  • 13,053
  • 9
  • 65
  • 128

3 Answers3

11

I just created a simple demo which contains the handy setAnchorPoint extension for all views.

The main reason you see your rotation from a corner is that your anchor point is somehow reset to 0,0.

import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

@IBOutlet weak var window: NSWindow!
var imageView: NSImageView!

func applicationDidFinishLaunching(_ aNotification: Notification) {
    // Insert code here to initialize your application

    // Create red NSImageView
    imageView = NSImageView(frame: NSRect(x: 100, y: 100, width: 100, height: 100))
    imageView.wantsLayer = true
    imageView.layer?.backgroundColor = NSColor.red.cgColor

    window.contentView?.addSubview(imageView)
}

func applicationWillTerminate(_ aNotification: Notification) {
    // Insert code here to tear down your application
}

func applicationDidBecomeActive(_ notification: Notification) {
    // Before animate, reset the anchor point
    imageView.setAnchorPoint(anchorPoint: CGPoint(x: 0.5, y: 0.5))
    // Start animation
    if imageView.layer?.animationKeys()?.count == 0 || imageView.layer?.animationKeys() == nil {
        let rotate = CABasicAnimation(keyPath: "transform.rotation")
        rotate.fromValue = 0
        rotate.toValue = CGFloat(-1 * .pi * 2.0)
        rotate.duration = 2
        rotate.repeatCount = Float.infinity

        imageView.layer?.add(rotate, forKey: "rotation")
    }
}
}

extension NSView {
    func setAnchorPoint(anchorPoint:CGPoint) {
        if let layer = self.layer {
            var newPoint = NSPoint(x: self.bounds.size.width * anchorPoint.x, y: self.bounds.size.height * anchorPoint.y)
            var oldPoint = NSPoint(x: self.bounds.size.width * layer.anchorPoint.x, y: self.bounds.size.height * layer.anchorPoint.y)

        newPoint = newPoint.applying(layer.affineTransform())
        oldPoint = oldPoint.applying(layer.affineTransform())

        var position = layer.position

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

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


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

enter image description here

brianLikeApple
  • 4,262
  • 1
  • 27
  • 46
  • your code worked,but is there a simple way to make this simple animation? – frank Jan 31 '18 at 08:14
  • @frank Is it not simple already? Set anchorPoint and position of layer which I have already given extension function and just do a simple CABasicAnimation. – brianLikeApple Jan 31 '18 at 09:12
  • yes,extension make this simple. I mean in iOS there is no need to make additional work to make this animation. I don't know is there a simple way like iOS do this animation. – frank Jan 31 '18 at 09:24
  • @frank No, macOS is not as same as iOS. In macOS you'd better read the documents before start if you have iOS background. Because it is really different. – brianLikeApple Jan 31 '18 at 09:56
  • For me it's still spins from the corners and not from the center. – Witterquick Oct 29 '18 at 09:27
  • 1
    @Roee84 Just create new project and copy my code, it should just work as seen from gif. – brianLikeApple Oct 30 '18 at 15:56
  • Thanks for the reply. I tried this code (without any change, just copy paste), and it didn't work. Even without the image - just tried to rotate the red layer, and again - the rotation starts from the corner. – Witterquick Oct 30 '18 at 21:16
  • 1
    @Roee84 I updated the code how to use the extension. Now it should work. I don't know what happened here, it was working before...But anyway, the extension is correct and used in my software without any problem. Thanks – brianLikeApple Oct 31 '18 at 10:35
  • @brianLikeApple Thanks! I'm not sure regarding this - when I tried this, the result is (still) rotation from the corners. But then I tried it on a new project and the rotation was from the center. Don't know yet what are the differences.. – Witterquick Oct 31 '18 at 15:49
  • 2
    @Roee84 I am not sure...The newer version is just separating the image init code and rotation code, but still using the same extension. If you init your view, sometimes the layer's anchor point will reset to 0,0 even you set it to 0.5,0.5. So we need to use the extension code after the whole layout settle down. – brianLikeApple Oct 31 '18 at 16:23
  • 1
    Amazing! I guess your comment is true about the view layout settle down because when I moved it to another func it works. Thanks!!! – Witterquick Nov 01 '18 at 08:13
  • 1
    Thanks! This helped a lot. So set anchor point must be happened before animation. – Tualatrix Chou Mar 12 '19 at 10:36
  • if you use an existing image, be sure to set `.wantsLayer = true` to the image view before the `setAnchorPoint` – keyle May 03 '20 at 12:39
2

As I wondered many times myself on this question, here is my own simple method to rotate any NSView. I post it also as a self reminder. It can be defined in a category if needed.

This is a simple rotation, not a continuous animation. Should be applied to an NSView instance with wantsLayer = YES.

- (void)rotateByNumber:(NSNumber*)angle {
    self.layer.position = CGPointMake(NSMidX(self.frame), NSMidY(self.frame));
    self.layer.anchorPoint = CGPointMake(.5, .5);
    self.layer.affineTransform = CGAffineTransformMakeRotation(angle.floatValue);
}
Max_B
  • 887
  • 7
  • 16
1

This is the result of a layout pass resetting your view's layer to default properties. If you check your layer's anchorPoint for example, you'll find it's probably reset to 0, 0.

A simple solution is to continually set the desired layer properties in viewDidLayout() if you're in a view controller. Basically doing the frame, anchorPoint, and position dance that you do in your initial setup on every layout pass. If you subclassed NSImageView you could likely contain that logic within that view, which would be much better than putting that logic in a containing view controller.

There is likely a better solution with overriding the backing layer or rolling your own NSView subclass that uses updateLayer but I'd have to experiment there to give a definitive answer.

Lucas Derraugh
  • 6,929
  • 3
  • 27
  • 43