0

I am trying to implement a shaking NSView in my macOS app.

I tried following the example/tutorial from here, but I am not having much success.

I am attempting to not only shake the view left to right, but also up and down, and with a 2 degree positive and negative rotation.

I thought I could nest animations through the completion handler within a loop, but that is not working the way I expected so I am trying to use an if statement to implement the shake, which also is not working.

The intent here is to simulate a shake from an explosion and as the explosions get closer the intensity increases which would cause the shaking to increase in the number of shakes as well as the duration of the view shaking.

Here's example code I am working with to master this task:

import Cocoa

class ViewController: NSViewController {

    @IBOutlet weak var shakeButton1: NSButton!
    @IBOutlet weak var shakeButton2: NSButton!
    @IBOutlet weak var shakeButton3: NSButton!
    @IBOutlet weak var shakeButton4: NSButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    override var representedObject: Any? {
        didSet {
        // Update the view, if already loaded.
        }
    }

    @IBAction func shakeButtonClicked(button: NSButton) {
        switch button.identifier {
        case shakeButton1.identifier:
            shakeView(on: self.view, shakes: 2)
        case shakeButton2.identifier:
            shakeView(on: self.view, shakes: 4)
        case shakeButton3.identifier:
            shakeView(on: self.view, shakes: 6)
        case shakeButton4.identifier:
            shakeView(on: self.view, shakes: 8)
        default: break
        }
    }

    /**
        Method simulates a shaking of a view
     
     intensity value is an integer for the number of times the animation is repeated.
     */
    func shakeView(on theView: NSView, shakes: Int) {
        let originalOrigin = self.view.frame.origin
        let rotation: CGFloat = 2
        let moveValue: CGFloat = 5
        let contextDuration: Double = 0.05
        for shake in 0..<shakes {
            if shake.isMultiple(of: 2) {
                print("Even")
                // Perform the first animation
                NSAnimationContext.runAnimationGroup({ firstAnimation in
                    firstAnimation.duration = contextDuration
                    // Animate to the new origin and angle
                    theView.animator().frame.origin.x = moveValue // positive origin moves right
                    theView.animator().frame.origin.y = moveValue // positive origin moves up
                    theView.animator().rotate(byDegrees: -rotation) // apply a negative rotation
                    print("First rotation \(shake) of \(shakes) = \(self.view.animator().frameRotation)")
                }) { // First completion handler
                    theView.frame.origin = originalOrigin
                    theView.rotate(byDegrees: rotation) // apply a positive rotation to rotate the view back to the original position
                    print("Second rotation \(shake) of \(shakes) = \(self.view.frameRotation)")
                }
            } else {
                print("Odd")
                // Perform the opposite action
                NSAnimationContext.runAnimationGroup({ secondAnimation in
                    secondAnimation.duration = contextDuration
                    theView.animator().frame.origin.x = -moveValue // negative origin moves left
                    theView.animator().frame.origin.y = -moveValue // negative origin moves down
                    theView.animator().rotate(byDegrees: rotation) // apply positive rotation
                    print("Third rotation \(shake) of \(shakes) = \(self.view.frameRotation)")
                }) { // Second completion handler
                    theView.frame.origin = originalOrigin
                    theView.rotate(byDegrees: -rotation) // apply a negative rotation to rotate the view back to the original position
                    print("Fourth rotation \(shake) of \(shakes) = \(self.view.frameRotation)")
                }
            }
            
        }
    }
}

I set up four buttons in storyboard and linked them all to the same IBAction method for simplicity. Button identifiers in storyboard are "shake1Button", "shake2Button", "shake3Button", and "shake4Button".

Thank you for any help.

EDIT

I think I nearly have it. The only issue I am now having is when the rotation occurs, the center of rotation does not appear to be centered on the view. I am now using NotificationCenter to handle the animation. It looks pretty good the way it is, but I would really like to get the center of rotation set on the center of the view.

import Cocoa

class ViewController: NSViewController {

    @IBOutlet weak var shake1Button: NSButton!
    @IBOutlet weak var shake2Button: NSButton!
    @IBOutlet weak var shake3Button: NSButton!
    @IBOutlet weak var shake4Button: NSButton!
    
    let observatory = NotificationCenter.default
    var shaken: CGFloat = 0
    var intensity: CGFloat = 0

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        setupObservatory()
    }

    override var representedObject: Any? {
        didSet {
        // Update the view, if already loaded.
        }
    }

    @IBAction func shakeButtonClicked(button: NSButton) {
        switch button.identifier {
        case shake1Button.identifier:
            intensity = 1
        case shake2Button.identifier:
            intensity = 2
        case shake3Button.identifier:
            intensity = 3
        case shake4Button.identifier:
            intensity = 4
        default: break
        }
        observatory.post(name: .shakeView, object: nil)
    }

    func setupObservatory() {
        observatory.addObserver(forName: .shakeView, object: nil, queue: nil, using: shakeView)
        observatory.addObserver(forName: .moveDownAnimation, object: nil, queue: nil, using: moveDownAnimation)
        observatory.addObserver(forName: .moveLeftAnimation, object: nil, queue: nil, using: moveLeftAnimation)
        observatory.addObserver(forName: .moveRightAnimation, object: nil, queue: nil, using: moveRightAnimation)
        observatory.addObserver(forName: .moveUpAnimation, object: nil, queue: nil, using: moveUpAnimation)
        observatory.addObserver(forName: .rotateLeftAnimation, object: nil, queue: nil, using: rotateLeftAnimation)
        observatory.addObserver(forName: .rotateRightAnimation, object: nil, queue: nil, using: rotateRightAnimation)
    }
    
    func shakeView(notification: Notification) {
        if shaken != intensity {
            shaken += 1
            observatory.post(name: .moveDownAnimation, object: nil)
        } else {
            shaken = 0
        }
    }
    
    func moveDownAnimation(notification: Notification) {
        //guard let theView = notification.object as? NSView else { return }
        let originalOrigin = self.view.frame.origin
        NSAnimationContext.runAnimationGroup({ verticalAnimation in
            verticalAnimation.duration = 0.05
            // Animate to the new angle
            self.view.animator().frame.origin.y -= intensity // subtracting value moves the view down
        }) { // Completion handler
            self.view.frame.origin = originalOrigin
            self.observatory.post(name: .moveLeftAnimation, object: nil)
        }
    }
    
    func moveLeftAnimation(notification: Notification) {
        //guard let theView = notification.object as? NSView else { return }
        let originalOrigin = self.view.frame.origin
        // Perform the animation
        NSAnimationContext.runAnimationGroup({ firstAnimation in
            firstAnimation.duration = 0.05
            // Animate to the new origin
            self.view.animator().frame.origin.x -= intensity // subtracting value moves the view left
        }) { // Completion handler
            self.view.frame.origin = originalOrigin
            self.observatory.post(name: .rotateLeftAnimation, object: nil)
        }
    }

    func moveRightAnimation(notification: Notification) {
        //guard let theView = notification.object as? NSView else { return }
        let originalOrigin = self.view.frame.origin
        // Perform the animation
        NSAnimationContext.runAnimationGroup({ horizontalAnimation in
            horizontalAnimation.duration = 0.05
            // Animate to the new origin
            self.view.animator().frame.origin.x += intensity // adding value moves the view right
        }) { // Completion handler
            self.view.frame.origin = originalOrigin
            self.observatory.post(name: .moveUpAnimation, object: nil)
        }
    }
    
    func moveUpAnimation(notification: Notification) {
        //guard let theView = notification.object as? NSView else { return }
        let originalOrigin = self.view.frame.origin
        NSAnimationContext.runAnimationGroup({ verticalAnimation in
            verticalAnimation.duration = 0.05
            // Animate to the new angle
            self.view.animator().frame.origin.y += intensity // adding value moves the view up
        }) { // Completion handler
            self.view.frame.origin = originalOrigin
            self.observatory.post(name: .rotateRightAnimation, object: nil)
        }
    }
    
    func rotateLeftAnimation(notification: Notification) {
        // Prepare the anchor point to rotate around the center
        let newAnchorPoint = CGPoint(x: 0.5, y: 0.5)
        view.layer?.anchorPoint = newAnchorPoint
                
        // Prevent the anchor point from modifying views location on screen
        guard var position = view.layer?.position else { return }
        position.x += view.bounds.maxX * newAnchorPoint.x
        position.y += view.bounds.maxY * newAnchorPoint.y
        view.layer?.position = position

        // Configure the animation
        let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
        rotateAnimation.byValue = intensity / 180 * CGFloat.pi
        rotateAnimation.duration = 0.05;

        let unRotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
        unRotateAnimation.byValue = -(intensity / 180 * CGFloat.pi)
        unRotateAnimation.duration = 0.05;

        // Trigger the animation
        CATransaction.begin()
        view.layer?.add(rotateAnimation, forKey: "rotate")
        CATransaction.setCompletionBlock {
            // Handle animation completion
            self.view.layer?.add(unRotateAnimation, forKey: "rotate")
            self.observatory.post(name: .moveRightAnimation, object: nil)
        }
        CATransaction.commit()
    }
    
    func rotateRightAnimation(notification: Notification) {
        // Prepare the anchor point to rotate around the center
        let newAnchorPoint = CGPoint(x: 0.5, y: 0.5)
        view.layer?.anchorPoint = newAnchorPoint
                
        // Prevent the anchor point from modifying views location on screen
        guard var position = view.layer?.position else { return }
        position.x += view.bounds.maxX * newAnchorPoint.x
        position.y += view.bounds.maxY * newAnchorPoint.y
        view.layer?.position = position

        // Configure the animation
        let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
        rotateAnimation.byValue = -(intensity / 180 * CGFloat.pi)
        rotateAnimation.duration = 0.05;

        let unRotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
        unRotateAnimation.byValue = intensity / 180 * CGFloat.pi
        unRotateAnimation.duration = 0.05;

        // Trigger the animation
        CATransaction.begin()
        view.layer?.add(rotateAnimation, forKey: "rotate")
        CATransaction.setCompletionBlock {
            // Handle animation completion
            self.view.layer?.add(unRotateAnimation, forKey: "rotate")
            self.observatory.post(name: .shakeView, object: nil)
        }
        CATransaction.commit()
    }

}

extension Notification.Name {
    static var shakeView: Notification.Name {
        .init(rawValue: "ViewController.shakeView")
    }
    static var moveDownAnimation: Notification.Name {
        .init(rawValue: "ViewController.moveDownAnimation")
    }
    static var moveLeftAnimation: Notification.Name {
        .init(rawValue: "ViewController.moveLeftAnimation")
    }
    static var moveRightAnimation: Notification.Name {
        .init(rawValue: "ViewController.moveRightAnimation")
    }
    static var moveUpAnimation: Notification.Name {
        .init(rawValue: "ViewController.moveUpAnimation")
    }
    static var rotateLeftAnimation: Notification.Name {
        .init(rawValue: "ViewController.rotateLeftAnimation")
    }
    static var rotateRightAnimation: Notification.Name {
        .init(rawValue: "ViewController.rotateRightAnimation")
    }
}
SouthernYankee65
  • 1,129
  • 10
  • 22
  • Maybe it's just me, but a duration of 0.05 seems (5/100th of a second) seems like an awfully short time for the animation. At least set it larger for debug purposes? – NSGod Sep 22 '22 at 18:26
  • Yes, it is short, but if it worked, it would be visible. Even at 1 second, I am only getting a single animation. – SouthernYankee65 Sep 22 '22 at 21:49
  • After increasing the animation time on the new version I can see the rotation is centered on the view, but for some reason the rotation is only happening counter clockwise. The clockwise rotation is not occuring. Debugging continues... – SouthernYankee65 Sep 23 '22 at 01:34

1 Answers1

0

Coming back around to this to close it out. I found a solution to my problem. Here's the code if someone else is looking for similar.

For now I am using notifications in my completion handlers.

import Cocoa

class ViewController: NSViewController {

    @IBOutlet weak var shake1Button: NSButton!
    @IBOutlet weak var shake2Button: NSButton!
    @IBOutlet weak var shake3Button: NSButton!
    @IBOutlet weak var shake4Button: NSButton!
    
    let observatory = NotificationCenter.default
    var shaken: CGFloat = 0
    var intensity: CGFloat = 0

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        setupObservatory()
    }

    override var representedObject: Any? {
        didSet {
        // Update the view, if already loaded.
        }
    }

    @IBAction func shakeButtonClicked(button: NSButton) {
        switch button.identifier {
        case shake1Button.identifier:
            intensity = 2
        case shake2Button.identifier:
            intensity = 3
        case shake3Button.identifier:
            intensity = 5
        case shake4Button.identifier:
            intensity = 6
        default: break
        }
        observatory.post(name: .shakeView, object: nil)
    }

    func setupObservatory() {
        observatory.addObserver(forName: .shakeView, object: nil, queue: nil, using: shakeView)
        observatory.addObserver(forName: .moveDownAnimation, object: nil, queue: nil, using: moveDownAnimation)
        observatory.addObserver(forName: .moveLeftAnimation, object: nil, queue: nil, using: moveLeftAnimation)
        observatory.addObserver(forName: .moveRightAnimation, object: nil, queue: nil, using: moveRightAnimation)
        observatory.addObserver(forName: .moveUpAnimation, object: nil, queue: nil, using: moveUpAnimation)
        observatory.addObserver(forName: .rotateCounterClockwiseAnimation, object: nil, queue: nil, using: rotateCounterClockwiseAnimation)
        observatory.addObserver(forName: .rotateClockwiseAnimation, object: nil, queue: nil, using: rotateClockwiseAnimation)
    }
    
    func shakeView(notification: Notification) {
        if shaken != intensity {
            shaken += 1
            observatory.post(name: .moveDownAnimation, object: nil)
        } else {
            shaken = 0
        }
    }
    
    func moveDownAnimation(notification: Notification) {
        let originalOrigin = self.view.frame.origin
        NSAnimationContext.runAnimationGroup({ verticalAnimation in
            verticalAnimation.duration = 0.075
            // Animate to the new angle
            self.view.animator().frame.origin.y -= intensity // subtracting value moves the view down
        }) { // Completion handler
            self.view.frame.origin = originalOrigin
            self.observatory.post(name: .moveLeftAnimation, object: nil)
        }
    }
    
    func moveLeftAnimation(notification: Notification) {
        let originalOrigin = self.view.frame.origin
        // Perform the animation
        NSAnimationContext.runAnimationGroup({ horizontalAnimation in
            horizontalAnimation.duration = 0.075
            // Animate to the new origin
            self.view.animator().frame.origin.x -= intensity // subtracting value moves the view left
        }) { // Completion handler
            self.view.frame.origin = originalOrigin
            self.observatory.post(name: .moveRightAnimation, object: nil)
        }
    }

    func moveRightAnimation(notification: Notification) {
        let originalOrigin = self.view.frame.origin
        // Perform the animation
        NSAnimationContext.runAnimationGroup({ horizontalAnimation in
            horizontalAnimation.duration = 0.075
            // Animate to the new origin
            self.view.animator().frame.origin.x += intensity // adding value moves the view right
        }) { // Completion handler
            self.view.frame.origin = originalOrigin
            self.observatory.post(name: .moveUpAnimation, object: nil)
        }
    }
    
    func moveUpAnimation(notification: Notification) {
        let originalOrigin = self.view.frame.origin
        NSAnimationContext.runAnimationGroup({ verticalAnimation in
            verticalAnimation.duration = 0.075
            // Animate to the new angle
            self.view.animator().frame.origin.y += intensity // adding value moves the view up
        }) { // Completion handler
            self.view.frame.origin = originalOrigin
            self.observatory.post(name: .rotateClockwiseAnimation, object: nil)
        }
    }
    
    func rotateCounterClockwiseAnimation(notification: Notification) {
        // Configure the animation
        let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
        rotateAnimation.fromValue = 0
        rotateAnimation.toValue = -(intensity * CGFloat.pi / 540)
        rotateAnimation.duration = 0.075;

        let unRotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
        rotateAnimation.fromValue = -(intensity * CGFloat.pi / 540)
        rotateAnimation.toValue = 0
        unRotateAnimation.duration = 0.075;

        // Trigger the animation
        CATransaction.begin()
        view.layer?.add(rotateAnimation, forKey: "rotateLeft")
        CATransaction.setCompletionBlock {
            // Handle animation completion
            self.view.layer?.add(unRotateAnimation, forKey: "rotateLeft")
            self.observatory.post(name: .shakeView, object: nil)
        }
        CATransaction.commit()
    }
    
    func rotateClockwiseAnimation(notification: Notification) {
        // Configure the animation
        let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
        rotateAnimation.fromValue = 0
        rotateAnimation.toValue = -(intensity * CGFloat.pi / 540)
        //rotateAnimation.byValue = -(intensity * CGFloat.pi / 180)
        rotateAnimation.duration = 0.075;

        let unRotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
        unRotateAnimation.fromValue = intensity * CGFloat.pi / 540
        unRotateAnimation.toValue = 0
        //unRotateAnimation.byValue = intensity * CGFloat.pi / 180
        unRotateAnimation.duration = 0.075;

        // Trigger the animation
        CATransaction.begin()
        self.view.layer?.add(rotateAnimation, forKey: "rotateLeft")
        CATransaction.setCompletionBlock {
            // Handle animation completion
            self.view.layer?.add(unRotateAnimation, forKey: "rotateLeft")
            self.observatory.post(name: .shakeView, object: nil)
        }
        CATransaction.commit()
    }
    
}

extension Notification.Name {
    static var shakeView: Notification.Name {
        .init(rawValue: "ViewController.shakeView")
    }
    static var moveDownAnimation: Notification.Name {
        .init(rawValue: "ViewController.moveDownAnimation")
    }
    static var moveLeftAnimation: Notification.Name {
        .init(rawValue: "ViewController.moveLeftAnimation")
    }
    static var moveRightAnimation: Notification.Name {
        .init(rawValue: "ViewController.moveRightAnimation")
    }
    static var moveUpAnimation: Notification.Name {
        .init(rawValue: "ViewController.moveUpAnimation")
    }
    static var rotateCounterClockwiseAnimation: Notification.Name {
        .init(rawValue: "ViewController.rotateCounterClockwiseAnimation")
    }
    static var rotateClockwiseAnimation: Notification.Name {
        .init(rawValue: "ViewController.rotateClockwiseAnimation")
    }
}
SouthernYankee65
  • 1,129
  • 10
  • 22