Why Does Autoreversing the Added Animation "Mess Up" the Final Result? - animation

I have an application that animates a small, red subview using added animations. The first animation moves the view down by 100 points. The second animation kicks in at the half way point and moves the view to the right by 100 points. The down and the right animations are additive resulting in a diagonal movement toward the lower right. Additionally, both animations are autoreversed so that the view is restored to its original position over the top of the small, green "marker" subview.
From the above gif one can see that the view animation behaves as expected until the autoreversing has completed whereupon the view jumps to the left (apparently by 100 points). Typically such a jump would be the result of not having set the state of the view to match the final frame of the animation. However, the view state is indeed being correctly(?) set by the animator's completion method - this seems to be borne out by the information provided by the print statements.
What is the cause of the view's final, leftward jump?
See the relevant code below. A complete project can be downloaded from GitHub
I have a hunch that the calls to UIView.setAnimationRepeatAutoreverses in each of the two animation closures might be the culprit. It is not clear to me that what I have done in that regard is in alignment with the framework's expectations.
class ViewController: UIViewController {
private var markerView: UIView!
private var animationView: UIView!
override func viewDidLoad() {
view.backgroundColor = .black
view.addGestureRecognizer(UITapGestureRecognizer.init(target: self, action: #selector(tapGestureHandler)))
let frame = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 10, height: 10)
markerView = UIView(frame: frame)
markerView.backgroundColor = .green
view.addSubview(markerView)
animationView = UIView(frame: frame)
animationView.backgroundColor = .red
view.addSubview(animationView)
}
#objc private func tapGestureHandler(_ sender: UITapGestureRecognizer) {
animate()
}
// Move down; half way through begin moving right while still moving down.
// Autoreverse it.
private func animate() {
let distance: CGFloat = 100
let down = {
UIView.setAnimationRepeatAutoreverses(true)
self.animationView.center.y += distance
}
let right = {
UIView.setAnimationRepeatAutoreverses(true)
self.animationView.center.x += distance
}
print("\nCenter: \(self.animationView.center)")
let animator = UIViewPropertyAnimator(duration: 2, curve: .linear, animations: down)
animator.addAnimations(right, delayFactor: 0.5)
animator.addCompletion { _ in
print("Center: \(self.animationView.center)")
self.animationView.center.x -= distance
self.animationView.center.y -= distance
print("Center: \(self.animationView.center)")
}
animator.startAnimation()
}
}
Update 1
I have added two other versions of the animate method which are informative. Based upon what I learned from these two versions, I altered the title of this question.
animate2: The calls to UIView.setAnimationRepeatAutoreverses have been commented out in each of the down and right animation functions. This modified code functions as expected. Of course we do not get the smooth autoreversing effect.
extension ViewController {
private func animate2() {
let distance: CGFloat = 100
let down = {
//UIView.setAnimationRepeatAutoreverses(true)
self.animationView.center.y += distance
}
let right = {
//UIView.setAnimationRepeatAutoreverses(true)
self.animationView.center.x += distance
}
print("\nCenter: \(self.animationView.center)")
let animator = UIViewPropertyAnimator(duration: 2, curve: .linear, animations: down)
animator.addAnimations(right, delayFactor: 0.5)
animator.addCompletion { _ in
print("Center: \(self.animationView.center)")
self.animationView.center.x -= distance
self.animationView.center.y -= distance
print("Center: \(self.animationView.center)")
}
animator.startAnimation()
}
}
animate3: The addition of the second (i.e. right) animation is commented out. This modified code functions as expected. Of course we do not get the movement to the right.
extension ViewController {
private func animate3() {
let distance: CGFloat = 100
let down = {
UIView.setAnimationRepeatAutoreverses(true)
self.animationView.center.y += distance
}
let right = {
UIView.setAnimationRepeatAutoreverses(true)
self.animationView.center.x += distance
}
print("\nCenter: \(self.animationView.center)")
let animator = UIViewPropertyAnimator(duration: 2, curve: .linear, animations: down)
//animator.addAnimations(right, delayFactor: 0.5)
animator.addCompletion { _ in
print("Center: \(self.animationView.center)")
self.animationView.center.y -= distance
//self.animationView.center.x -= distance
print("Center: \(self.animationView.center)")
}
animator.startAnimation()
}
}
It appears that calling UIView.setAnimationRepeatAutoreverses in both of the animation functions is "messing things up". It is not clear to me if this can be rightly be regarded as an iOS bug.

Just comment the line under your animate() method ::
let animator = UIViewPropertyAnimator(duration: 2, curve: .linear, animations: down)
animator.addAnimations(right, delayFactor: 0.5)
animator.addCompletion { _ in
print("Center: \(self.animationView.center)")
//self.animationView.center.x -= distance //<--- Comment or remove this line.
self.animationView.center.y -= distance
print("Center: \(self.animationView.center)")
}

Related

Player / CameraNode Position Issue

I have a camerNode position issue. I have included the code below + an attempt of resolving this with no progress. Essentially in my GameScene I have the camera locked perfectly onto the player when moving through the scene, I am actually trying to amend this so that the camera is slightly ahead of the player, this meaning my player is actually positioned on the left side of the screen, almost like an offset (+ 200) :) if this existed.
The Code:
class GameScene: SKScene, SKPhysicsContactDelegate {
// Create a constant cam as a SKCameraNode:
let cam = SKCameraNode()
override func didMove(to view: SKView) {
// vertical center of the screen:
screenCenterY = self.size.height / 2
// Assign the camera to the scene
self.camera = cam
//Add the camera itself to the scene's node tree"
self.addChild(self.camera!)
// Position the camera node above the game elements:
self.camera!.zPosition = 50
}
override func didSimulatePhysics() {
// Keep the camera locked at mid screen by default:
var cameraYPos = screenCenterY
cam.yScale = 1
cam.xScale = 1
// Follow the player:
if (player.position.y > screenCenterY) {
cameraYPos = player.position.y
cam.yScale = newScale
cam.xScale = newScale
}
// Camera Adjustment:
self.camera!.position = CGPoint(x: player.position.x, y: cameraYPos)
I initially thought that I could overcome this by changing the player to another SKSpriteNode.. i'e in my HUD class I could add a node and apply this code around? I could then refer back to my player class in which I already defined the position above my override func didMove.
let initialPlayerPosition = CGPoint(x: 50, y: 350)
I did try this and the GameScene started playing up, is there a better method for achieving this result? I assume the code could be changed to accommodate but I am still in the learning grounds of Xcode.
Never mind - solved it!
Camera adjustment statement - player.position.x + 200) :)
This does the trick!

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

Clock minute-hand disappears when attempting to rotate it

Modus Operandi:
1) Use an UIImageView of a base Clock Image.
2) Add MinuteHand & HourHand sublayers (containing their respective images) to the UIImageView layer.
Problem: both sublayers disappear when attempting to perform a rotation transformation.
Note: 1) I've removed the 'hour' code & ancillary radian calculations to simplify code.
2) The 'center' is the center of the clock. I had adjusted the coordinates to actually pin the hands to the clock's center.
3) The ViewDidLayoutSubviews() appear to be okay. I got the clock + hands.
class ClockViewController:UIViewController {
private let minuteLayer = CALayer()
#IBOutlet weak var clockBaseImageView: UIImageView!
#IBOutlet weak var datePicker: UIDatePicker!
override func viewDidLayoutSubviews() {
guard var minuteSize = UIImage(named: "MinuteHand")?.size,
var hourSize = UIImage(named: "HourHand")?.size
else {
return
}
var contentLayer:CALayer {
return self.view.layer
}
var center = clockBaseImageView.center
// Minute Hand:
minuteLayer.setValue("*** Minute Hand ***", forKey: "id")
minuteSize = CGSize(width: minuteSize.width/3, height: minuteSize.height/3)
minuteLayer.contents = UIImage(named: "MinuteHand")?.cgImage
center = CGPoint(x: 107.0, y: 40.0)
var handFrame = CGRect(origin: center, size: minuteSize)
minuteLayer.frame = handFrame
minuteLayer.contentsScale = clockBaseImageView.layer.contentsScale
minuteLayer.anchorPoint = center
clockBaseImageView.layer.addSublayer(minuteLayer)
}
Here's my problem: Attempting to rotate the minute hand via 0.01 radians:
func set(_ time:Date) {
minuteLayer.setAffineTransform(CGAffineTransform(rotationAngle: .01)) // random value for test.
}
Before rotation attempt:
After attempting to rotate minute hand:
The hand shifted laterally to the right vs rotate.
Why? Perhaps due to the pivot point?
I think this will solve your problem, Take a look and let me know.
import GLKit // Importing GLKit Framework
func set(_ time:Date) {
minuteLayer.transform = CGAffineTransformMakeRotation(CGFloat(GLKMathDegreesToRadians(0.01)))
}
Note: this solution doesn't solve the issue about rotating a CALayer. Instead, it bypasses the issue by replacing the layer with a subview and rotating the subview via:
func set(_ time:Date) {
minuteView.transform = CGAffineTransform(rotationAngle: 45 * CGFloat(M_PI)/180.0)
}
Here's the result:
Still, it would be nice to know how to rotate a CALayer.

How to animate view based on another view

The middle lock animates upwards faster than the top pane in my animation. How do I animate the middle lock so that it moves upwards alongside the top pane? To be more specific, I want the middle locks center to always align with the top panes bottom as it moves up and off the screen.
This is the code I currently have, lockBorder and lockKeyhole is what is referred to as "middle lock" which is moving upwards too fast compared to the topLock:
#IBOutlet var topLock: UIImageView!
#IBOutlet var bottomLock: UIImageView!
#IBOutlet var lockBorder: UIImageView!
#IBOutlet var lockKeyhole: UIImageView!
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
openLock()
}
func openLock() {
UIView.animateWithDuration(0.5, delay: 1.0, options: [], animations: {
self.lockKeyhole.transform = CGAffineTransformMakeRotation(CGFloat(3.14))
}, completion: { _ in
UIView.animateWithDuration(0.6, delay: 0.2, options: [], animations: {
var topFrame = self.topLock.frame
topFrame.origin.y -= topFrame.size.height
var bottomFrame = self.bottomLock.frame
bottomFrame.origin.y += bottomFrame.size.height
var lockBorderFrame = self.lockBorder.frame
lockBorderFrame.origin.y -= self.view.frame.height
var lockKeyholeFrame = self.lockKeyhole.frame
lockKeyholeFrame.origin.y -= self.view.frame.height
self.topLock.frame = topFrame
self.bottomLock.frame = bottomFrame
self.lockKeyhole.frame = lockKeyholeFrame
self.lockBorder.frame = lockBorderFrame
}, completion: { finished in
})
})
}
You are adjusting topFrame.origin.y by topFrame.size.height, but you are adjusting lockBorderFrame.origin.y and lockKeyholeFrame.origin.y by self.view.frame.height. We can deduce that these adjustments are different amounts. I assume topFrame.size.height is one half of self.view.size.height. Since you're moving the lock views by twice the distance you're moving topLock, and both adjustments happen over the same 0.6 second interval, the lock views move twice as fast.
I assume you want to animate the lock views all the way off the screen, so it would be insufficient to move them by by only topFrame.size.height. That would leave the bottom half of the lock visible. You need to move the lock views so that their bottom edges are at the top edge of self.view.
This means that you need to set lockBorder.frame.origin.y to the negative of lockBorder.frame.size.height. It will move a total distance of lockBorder.frame.size.height + lockBorder.frame.origin.y, which happens to be equal to lockBorder.frame.maxY.
I assume that lockBorder is at least as large as lockKeyhole, so if we move all the views by lockBorder.frame.maxY, they will all be off the screen. And since we move them all the same distance, all the views will move together.
Note also that if you use constraints in your storyboard, then these views will snap back to their original positions as soon as you do almost anything else to the view hierarchy. So you should probably remove them from their superviews as soon as the animation finishes, unless you're hiding them in some other way (like by removing their superview from the view hierarchy).
Incidentally, the standard library defines M_PI for you.
func openLock() {
UIView.animateWithDuration(0.5, delay: 1.0, options: [], animations: {
self.lockKeyhole.transform = CGAffineTransformMakeRotation(CGFloat(M_PI))
}, completion: { _ in
UIView.animateWithDuration(0.6, delay: 0.2, options: [], animations: {
let yDelta = self.lockBorder.frame.maxY
self.topLock.center.y -= yDelta
self.lockKeyhole.center.y -= yDelta
self.lockBorder.center.y -= yDelta
self.bottomLock.center.y += yDelta
}, completion: { _ in
self.topLock.removeFromSuperview()
self.lockKeyhole.removeFromSuperview()
self.lockBorder.removeFromSuperview()
self.bottomLock.removeFromSuperview()
})
})
}

SKSpriteNode that the correct size

I am trying to draw a SKSpriteNode that is 30 tall and has the width of the viewport. This is the code (inside SKScene):
func floor() -> SKSpriteNode{
let floor = SKSpriteNode(color: SKColor.greenColor(), size: CGSizeMake(self.size.width, 20))
floor.position = CGPointMake(0, 0)
floor.physicsBody = SKPhysicsBody(rectangleOfSize: floor.size)
floor.physicsBody.dynamic = false
return floor
}
The sprite is added to the scene like this:
override func didMoveToView(view: SKView!){
if (!contentCreated){
self.createContents()
contentCreated = true
}
}
func createContents() {
self.backgroundColor = SKColor.blackColor()
self.scaleMode = SKSceneScaleMode.AspectFill
self.addChild(self.floor())
}
The sprite is 30 tall (seemingly), but the length seems to be half of the viewport in width instead of the full width. The code that creates this scene is:
var mainScene = MainScene(size: self.view.frame.size)
spriteView.presentScene(mainScene)
This code is inside a ViewController.
Does anyone know what might be going on?
The default anchorPoint of a sprite node is { 0.5, 0.5 }, which could result in the code above positioning only half of your sprite on the screen. Try setting the anchorPoint to { 0.0, 0.0 } and see if that helps.

Resources