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