Clifton Labrum
Clifton Labrum

Reputation: 14168

Rotate NSImageView at its Center to Make it Spin

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?

Upvotes: 4

Views: 2340

Answers (3)

Max_B
Max_B

Reputation: 1027

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);
}

Upvotes: 2

brianLikeApple
brianLikeApple

Reputation: 4371

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

Upvotes: 11

Lucas Derraugh
Lucas Derraugh

Reputation: 7049

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.

Upvotes: 1

Related Questions