3

I am currently trying to implement some custom NSViews that will be animate by certain events in a macOS app. The controls are along the lines of slider you would find in an AudioUnit Plugin.

The question of animation has been answered in this question.

What I am uncertain of is how to alter a view's CALayer position and anchor point for the rotation and updating its frame for mouse events.

As a basic example, I wish to draw a square by creating an NSView and setting it's background colour. I then want to animate the rotation of the view, a la the previous link, on a mouseDown event.

With following setup, the view is moved to the centre of window, its anchor point is altered so that the view rotates around it and not around the origin of the window.contentView!. However, moving the view.layer.position and setting the view.layer!.anchorPoint = CGPoint(x: 0.5,y: 0.5) does not also move the frame that will detect events.

@IBOutlet weak var window: NSWindow!

func applicationDidFinishLaunching(_ aNotification: Notification)
{
    let view = customView(frame: NSRect(x: 0, y: 0, width: 50, height: 50))
    window.contentView!.wantsLayer = true
    view.wantsLayer = true
    view.layer?.backgroundColor = NSColor.blue.cgColor
    window.contentView?.addSubview(view)

    let containerFrame = window.contentView!.frame
    let center = CGPoint(x: containerFrame.midX, y: containerFrame.midY)
    view.layer?.position = center
    view.layer?.anchorPoint = CGPoint(x: 0.5,y: 0.5)
}

The customView in this case is simply an NSView with the rotation animation from the previous link executed on mouseDown

override func mouseDown(with event: NSEvent)
    {
        let timeToRotate = 1
        let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
        rotateAnimation.fromValue = 0.0
        rotateAnimation.toValue = angle
        rotateAnimation.duration = timeToRotate
        rotateAnimation.repeatCount = .infinity
        self.layer?.add(rotateAnimation, forKey: nil)
    }

Moving the frame by setting view.frame = view.layer.frame results in the view rotating around one of its corners. So, instead I have altered the view.setFrameOrigin()

@IBOutlet weak var window: NSWindow!

func applicationDidFinishLaunching(_ aNotification: Notification)
{
    let view = customView(frame: NSRect(x: 0, y: 0, width: 50, height: 50))
    window.contentView!.wantsLayer = true
    view.wantsLayer = true
    view.layer?.backgroundColor = NSColor.blue.cgColor
    window.contentView?.addSubview(view)

    let containerFrame = window.contentView!.frame
    let center = CGPoint(x: containerFrame.midX, y: containerFrame.midY)
    let offsetFrameOrigin = CGPoint(x: center.x - view.bounds.width/2,
                                    y: center.y - view.bounds.height/2)
    view.setFrameOrigin(offsetFrameOrigin)
    view.layer?.position = center
    view.layer?.anchorPoint = CGPoint(x: 0.5,y: 0.5)
}

Setting the view.frame origin to an offset of the centre achieves what I want, but I cannot help but feel this is a little 'hacky' and that I may be approaching this the wrong way. Especially since any further change to view.layer or view.frame will result in either the animation being incorrect or events being detected outside what is drawn.

How can I alter NSView.layer so that it rotates around its centre at the same time as setting the NSView.frame so that mouse events are detected in the correct area?

Also, is altering NSView.layer.anchorPoint a correct way to set it up for rotation around its centre?

Sarreph
  • 1,986
  • 2
  • 19
  • 41
fdcpp
  • 1,677
  • 1
  • 14
  • 25
  • Do you think this could have anything to do with Xcode 10 / Mojave? I have been through about 10 tutorials, finally getting layer rotation to work without messing up an anchorPoint, only to discover the same as you, that my 'event area' is translated off... Further translations to counter this lead nowhere as the anchorPoint resets itself... – Sarreph Jun 17 '18 at 00:39

1 Answers1

2

I think I'm in much the same position as you. Fortunately, I stumbled across a gist that extends NSView with a setAnchorPoint function that does the job for me (keeping the layer's anchor in sync with the main frame).

There's a fork for that gist for Swift 4. Here's the code itself:

extension NSView
{
    func setAnchorPoint (anchorPoint:CGPoint)
    {
        if let layer = self.layer
        {
            var newPoint = CGPoint(x: self.bounds.size.width * anchorPoint.x, y: self.bounds.size.height * anchorPoint.y)
            var oldPoint = CGPoint(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.position = position
            layer.anchorPoint = anchorPoint
        }
    }
}

You'd then use it like this on the NSView directly (i.e. not its layer):

func applicationDidFinishLaunching(_ aNotification: Notification)
{
    // ... //
    let view = customView(frame: NSRect(x: 0, y: 0, width: 50, height: 50))
    view.wantsLayer = true
    view.setAnchorPoint(anchorPoint: CGPoint(x: 0.5, y: 0.5))
    // ... //
}
Sarreph
  • 1,986
  • 2
  • 19
  • 41