Animate Auto Layout Constraints and Alpha on NSView - nsview

Swift 3, iOS 10, macOS 10.12.4
I am building an app that runs on both iOS and Mac. On the iOS side, I have successfully animated a UIView. When the user taps on something, a popup appears and animates into position. Here's my code inside the tap event:
self.popupConstraintX.constant = x
self.popupConstraintY.constant = y
UIView.animate(withDuration: 0.25 , delay: 0.0, options: .curveLinear, animations: {
self.graphPopup.alpha = 1.0
self.layoutIfNeeded()
}, completion:nil)
In this case, self refers to a UITableViewCell that holds the graphPopup.
I have built the same thing on the Mac side, but I'm trying to animate graphPopup which is now an NSView. Here's what I have so far inside my click event:
self.popupConstraintX.constant = x
self.popupConstraintY.constant = y
self.view.layoutSubtreeIfNeeded()
NSAnimationContext.runAnimationGroup({_ in
self.graphPopup.alphaValue = 1.0
//Indicate the duration of the animation
NSAnimationContext.current().duration = 0.25
NSAnimationContext.current().allowsImplicitAnimation = true
self.view.updateConstraints()
self.view.layoutSubtreeIfNeeded()
}, completionHandler:nil)
Here self refers to the containing NSViewController. Nothing animates--not the position or the alpha of graphPopup. It just appears and disappears like it's on an Atari in 1985.
Any idea what I'm doing wrong with my NSView animation?
Update
For posterity's sake, here is the working code as suggested by BJ (with a slight tweak to use the implicit animation context):
self.popupConstraintX.constant = x
self.popupConstraintY.constant = y
NSAnimationContext.runAnimationGroup({context in
context.duration = 0.25
context.allowsImplicitAnimation = true
self.graphPopup.alphaValue = 1.0
self.view.layoutSubtreeIfNeeded()
}, completionHandler:nil)

The modification of alphaValue is happening before you have turned on implicit animation, so the alpha will not be animated. It's unclear to me whether that was intentional or not.
The view is not animating to the position given by the popupConstraints, because you're not actually doing anything inside the animation block that would cause the view's frame to change. In order to trigger an implicit animation, you must not only update constraints; you must also ensure that the view's frame changes within the animation block. If you're using AutoLayout, this is usually done by calling layoutSubtreeIfNeeded.
However, because you updated the constraints and called layoutSubtreeIfNeeded() prior to the animation block, there are no additional frame changes left to make inside the block (unless there's something happening down in updateConstraints() which you haven't shown us.)
You should remove the first call to layoutSubtreeIfNeeded(), or if it's still necessary, put it above the popupConstraint modifications. Then, when you call layoutSubtreeIfNeeded() within the animation block, a new frame will be set based on those changed constraints, and you should see animation happening.

Swift 5
animator() is the key.
You can simply do like this:
NSAnimationContext.runAnimationGroup({ context in
//Indicate the duration of the animation
context.duration = 0.25
self.popupConstraintX.animator().constant = x
self.popupConstraintY.animator().constant = y
self.graphPopup.animator().alphaValue = 1.0
}, completionHandler:nil)

Related

Setting the image on UIImageView changes the animation

I have a UIImageView that I wish to animate at the click of a button. But before the animation, I set its image. However, this image-setting messes up the animation. When I comment out the image-set, the animation works as expected.
var busFrame = self.newBusView.frame
let sourceFrame = self.buttonA.superview?.convert(self.buttonA.frame.origin, to: nil)
busFrame.origin.y = sourceFrame!.y
busFrame.origin.x = sourceFrame!.x
self.newBusView.frame = busFrame
//the following line messes up the animation
// self.newBusView.image = UIImage(named: "back_blue")
UIView.animate(withDuration: 2.0, delay: 0.3, options: .curveEaseOut, animations: {
busFrame = self.newBusView.frame
let desstinationFrame = self.buttonD.superview?.convert(self.buttonD.frame.origin, to: nil)
busFrame.origin.y = desstinationFrame!.y
busFrame.origin.x = desstinationFrame!.x
self.newBusView.frame = busFrame
}, completion: { finished in
print("Done!")
//same thing happening here
//self.newBusView.image = UIImage(named: "Background 1")
})
Also note that the buttons are in separate Stack views (hence the need for superview?.convert)
UPDATE:
So it was noticed that whenever I have a button.setImage() the positions go wrong (Even if the button has nothing to do with the animation). But instead of setImage, if I use button.imageView.image = something, then it has no side effects on animations. What is happening?
Expected:
Actual:
As we do not know the "expected" animation, I could not repeat the problem that you have. When I set same image again in the commented line, I get the same animation.
It may be about the area that the image covers. New image's ratio may be different than the default, therefore you see different animation although view's animation is same. You actually animate the view. Hence, you should focus on frames.
I believe that as #musakokcen mentioned your image have different size.
Try to use:
yourImageView.layer.masksToBounds = yes

Stopping and Resetting a CGAffineTransform

I'm trying to write code where when a trigger happens an instance of UIImageView creates a slow "growing" effect with a CGAffineTransform running for 20 seconds.
The issue happens when the trigger happens again before the first transform has completed. Instead of the image resetting back to it's original size, it shrinks exponentially, depending on when the trigger happened during the first transformation.
Here is my current code:
func changeCategoryImage() {
self.categoryPanoramaImageView.transform = CGAffineTransform.identity
UIView.transition(with: self.categoryPanoramaImageView, duration: 0.4, options: .transitionCrossDissolve, animations: {
self.categoryPanoramaImageView.image = self.newPanoramaImage
}) { (done) in
UIView.animate(withDuration: 10, animations: {
self.categoryPanoramaImageView.transform = CGAffineTransform(scaleX: 5.0, y: 5.0)
})
}
}
I was under the impression that CGAffineTransform.identity would reset the image back to it's original size. This is seems to not be the case.
How can I halt and reset the current running transformation in order to start a new one?
I was under the impression that CGAffineTransform.identity would reset the image back to it's original size. This is seems to not be the case.
Correct. It isn't the case. That's not how animations work. You are making a direct change to the underlying view (i.e. the model layer), but the animation is still present (i.e. the presentation layer).
Thus, you are setting the view's transform to the identity, but you are not removing the existing animation. And animations are additive by default. Hence the result you are observing.
Try inserting
self.categoryPanoramaImageView.layer.removeAllAnimations()
as the first line of your function. That way, you will remove the animation and start over from the identity.

iOS UIButton in Navigation bar disappears after performing transform animation

I'm trying to animate transform from '+' button to 'X' button by rotating it on click.
Everything works great when I'm using standard button on UIView. I'm using following code:
UIView.animateWithDuration(2.0, delay: 0.0, options: .CurveEaseOut, animations: {
let transform = CGAffineTransformMakeRotation(CGFloat(M_PI_4))
self.navigationPlusButton.transform = transform
}, completion: nil)
The problem occurs when I try to achieve the same with button in navigation bar.
Looks like button height is resized to 0 before the rotation and I don't know how to fix navigation button size since there are no constraints definitions. Note storyboard
and for constraints
Note: Second approach using CABasicAnimation rotated button nicely but it returned it to initial state immediately after animation ends. I would like to stay 'X' until I click again.
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(M_PI_4)
rotateAnimation.duration = 2.0
self.navigationPlusButton.layer.addAnimation(rotateAnimation, forKey: nil)
How it works so easily outside the navigation bar and it is so hard to do it on navigation bar? I can't find the solution. Please help
try following lines to your code, it will solve your problem.
rotateAnimation.fillMode = kCAFillModeForwards
rotateAnimation.removedOnCompletion = false
Try this line on the CABasicAnimation:
rotateAnimation.removedOnCompletion = false
It may solve your problem.

Implementing basic animations with tvOS

I'm hoping someone may be able to help me understand why my tvOS animations are not running.
In my code I have a function like this:
func testAnimation() {
clockLabel.alpha = 0.0
UIView.animateWithDuration(1.0, animations: {
self.clockLabel.alpha = 1.0
})
}
I am calling testAnimation() from within my viewDidLoad(), but no animation ever seems to happen.
I've tested with a few different types of animations, from things like position to opacity, but it seems that no animation ever actually runs in the Simulator.
At this time, my app does not have a focus. All I'm trying to do is load a blank UIView with a label in the middle that fades in.
You're trying to animate your UILabel before it has been displayed. Move your animation from viewDidLoad() to viewDidAppear().
I can confirm this behaviour as this happens for me as well. What I did was setting a delay of 0.1 (afterDelay:), and it is "good enough"
On the other hand, what DID actually work was setting a transform for my views. E.g. (objc though):
CGAffineTransform scaleTransform = CGAffineTransformMakeScale(0.5,0.5);
[UIView animateWithDuration: 0.5 animations:^{
_myView.transform = scaleTransform;
} completion: nil]
Maybe it's a bug in tvOS
Try using UIView Animation, like this (Swift):
clockLabel.alpha = 0
UIView.animateWithDuration(0.5, delay: 0.0, options: .Linear, animations: {
self.clockLabel.alpha = 1.0
}, completion: { finished in
// you may add some code here to make another action right after the animation end, or just delete this comment :)
})
This works on our side.

Animating a label to increase and decrease in size

I have been trying to implement an animation that brings the users attention to a change in value in a label. I want to do this by quickly increasing and reducing the size of the label (can't think of a better way to describe it) and I've made some progress towards this. The problem is that while the animation increases in size as I want it to; the way it deceases in size isn't smooth. Also, once the animation is complete, the size of the font does not return to the original.
Here is what I have:
func bloat() {
UIView.beginAnimations(nil, context: nil)
UIView.setAnimationDelegate(self)
UIView.setAnimationDelay(0.6)
UIView.setAnimationDuration(0.3)
UIView.setAnimationRepeatCount(4)
UIView.setAnimationCurve(UIViewAnimationCurve.EaseInOut)
currentBudgetDisplay.transform = CGAffineTransformMakeScale(0.9, 0.9)
UIView.commitAnimations()
}
What changes can I make to get it to work as I intend it to?
The requirement is simple using core animation, try this
func bloat() {
var animation = CABasicAnimation(keyPath: "transform.scale")
animation.toValue = NSNumber(float: 0.9)
animation.duration = 0.3
animation.repeatCount = 4.0
animation.autoreverses = true
currentBudgetDisplay.layer.addAnimation(animation, forKey: nil)
}
In iOS 6 and 7, transform UIView animations applied to UIViews under auto layout do not work (because the auto layout stops them). This is fixed in iOS 8, however.
If you insist on supporting iOS 7, there are many workarounds, including using CABasicAnimation (layer animation) instead of UIView animation. See also my essay here: How do I adjust the anchor point of a CALayer, when Auto Layout is being used?