I have seen here very smooth animation of hiding/showing subview in UIStackView.
I try to reproduce it in my own application but I have meet a problem.
View that is hidden during animation proces does not resize. It just wait until animation finish and then disappear. Opposite to clear button from above linked example.
My code:
UIView.animate(withDuration: 0.5,
delay: 0.0,
usingSpringWithDamping: 0.9,
initialSpringVelocity: 1,
options: [],
animations: {
self.acceptDeclineBar.isHidden = !newState
self.view.layoutIfNeeded()
},
completion: nil)
Question
Is UIStackView give resizing animation on hide/show for free or do I need implement it for myself using height constraint for example?
Your view (self.acceptDeclineBar) will hide when the animation completes. try to hide before the animation.
self.acceptDeclineBar.isHidden = !newState
UIView.animate(withDuration: 0.3){ [weak self]
self?.view.layoutIfNeeded()
}
or instead of hiding you can use Height Constraint
acceptDeclineBarHeightConstraint.constant = newState ? 60 (whatever Visbale size) : 0 (Hide)
UIView.animate(withDuration: 0.3){ [weak self]
self?.view.layoutIfNeeded()
}
Related
I've decided to switch to constraints and face up with animation problems. In autoresizing mask world everything works fine. UIView is attached to right top. Content inside use autoresizing mask.
Animate code:
UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseInOut]) {
self.constraints.first(where: {$0.firstAttribute == .height})?.constant = dstSize.height
self.constraints.first(where: {$0.firstAttribute == .width})?.constant = dstSize.width
self.setNeedsLayout()
self.layoutIfNeeded()
}
You need to move constraint's change outside the animation block
self.constraints.first(where: {$0.firstAttribute == .height})?.constant = dstSize.height
self.constraints.first(where: {$0.firstAttribute == .width})?.constant = dstSize.width
UIView.animate(withDuration: 0.5, delay: 0, options: [.curveEaseInOut]) {
self.superView!.layoutIfNeeded()
}
Update
In your Github attached code you need to re-layout the main view not the container
UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut) {
self.view.layoutIfNeeded()
}
A few things:
It's good practice to make properties for the constraints you want to update later;
Only the layoutIfNeeded call have to be inside an animation block;
It looks like setNeedsLayout is unnecessary. Have you tried to remove it?
I'm struggling to understand why the following key-framed animation doesn't perform as I expect. Here minuteClock is a subclassed UIView that is also a child view of the main view. minuteClock's horizontal position is set via an exposed constraint. minuteClock's own layer has a CAShapeLayer sublayer called borderLayer.
I'm trying to simultaneously animate the location of minuteClock and the fillcolor of that grandchild borderLayer. What I'm finding is that the keyframes work for the positioning but are ignored for the CAShapeLayer. That is, while the minuteClock moves to 100 half-way through the overall duration, I don't see borderLayer's fillcolor turn red. Instead it seems to just animates very quickly (like a quarter-second) to green.
UIView.animateKeyframes(withDuration: 2.0,
delay: 0,
options: .calculationModeLinear,
animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0,
relativeDuration: 0.5,
animations: {
self.minuteClockPositionConstraint.constant = 100
self.minuteClock.borderLayer.fillColor = UIColor.red.cgColor
self.view.layoutIfNeeded()
})
UIView.addKeyframe(withRelativeStartTime: 0.5,
relativeDuration: 0.5,
animations: {
self.minuteClockPositionConstraint.constant = 50
self.minuteClock.borderLayer.fillColor = UIColor.green.cgColor
self.view.layoutIfNeeded()
})
}, completion: nil)
I think I'm probably going about this wrong, mixing up keyframed animation at the view level with implicit animation triggered by changing a property on a CAShapeLayer.
Any suggestions as to how I should be doing this?
I have a weird problem that seems to be fairly easy to solve. I think I could use some workaround to be able to have the behavior that I want but I want to know if there is a better way to do it.
I have a view called contentView (blue in the image) that it will expand its height using a UIView.animation, this is no problem at all and works as expected.
The problem here is that this view has a child component (a button) that has an autolayout constraint to its bottom equal to 22 like this:
This is the autolayout constraint:
If I do the height resize without the animation it works fine, the view height change and the button are always 22 points of the bottom of the contentView. But when I use an animation to make this change more smoothy and user-friendly the button moves to the end position before the animation even start.
I want to know how I could achieve a smooth animation here but with the button moving along its parent view
The part of the code that handles the animation is pretty straightforward but I'll post it in here:
#IBAction func openDetail(_ sender: ExpandCourseDetail) {
let rotation = sender.getOpen() ? CGAffineTransform.identity : CGAffineTransform(rotationAngle: CGFloat.pi)
UIView.animate(withDuration: 0.5, delay: 0.1, options: [.curveEaseInOut], animations: {
sender.transform = rotation
}, completion: {
success in
sender.setOpen(!sender.getOpen())
})
UIView.animate(withDuration: 1.0, delay: 0.5, options: [.curveEaseInOut], animations: {
self.contentView.frame.size.height = sender.getOpen() ? self.contentView.frame.height - 300 : self.contentView.frame.height + 300
}, completion: nil)
}
As a side note, the button itself has an animation that rotates the button 180 degrees to show the user that the view is expanding.
Thank you so much for your help.
It's super easy with constraints, just create a superView height constraint IBOutlet and change its constant value.
#IBAction func btnPressed(_ sender: UIButton) {
self.toggleButton.isSelected = !sender.isSelected
//Animation starts here
self.view.layoutIfNeeded()
UIView.animate(withDuration: 0.7) {
if self.toggleButton.isSelected {
//transform button
self.toggleButton.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi))
//change your ParentView height to desired one
self.constContentViewHeight.constant = self.view.frame.size.height - 220
self.view.layoutIfNeeded()
} else {
self.toggleButton.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi*2))
// Set height constraint to original value
self.constContentViewHeight.constant = 250
self.view.layoutIfNeeded()
}
}
}
I have created a demo, check it out.
The issue you are facing is due to two animation blocks. So I have changed some lines and put both button transformation and height animation into one animation block.
func openDetail(_ sender: ExpandCourseDetail) {
let isOpen = sender.getOpen() ? CGAffineTransform.identity : CGAffineTransform(rotationAngle: CGFloat.pi)
UIView.animate(withDuration: 1.0, delay: 0.5, options: [.curveEaseInOut], animations: {
sender.transform = rotation
self.contentView.frame.size.height = self.contentView.frame.height + (isOpen ? 300 : -300)
}, completion: { (success) in
sender.setOpen(!isOpen)
})
}
To animate a bar opening...
#IBOutlet var barHeight: NSLayoutConstraint!
barHeight.constant = barShut?30:100
self.view.layoutIfNeeded()
t = !barShut?30:100
UIView.animate(withDuration: 0.15,
delay: 0,
options: UIViewAnimationOptions.curveEaseOut,
animations: { () -> Void in
self.barHeight.constant = t
self.view.layoutIfNeeded()
},
completion: {_ in
Screen.barShut = !Screen.barShut
}
)
That's great ...
But how would you make it boing like this?
(The only way I'd know to do this is, use CADisplayLink, with a few lines of code for a spring decaying.) Is this available in UIKit?
You can use the spring animation method that is built in to UIView:
func toggleBar() -> Void {
self.view.layoutIfNeeded()
let newHeight:CGFloat = !barShut ? 30:100
barShut = !barShut
barHeightConstraint.constant = newHeight
UIView.animate(withDuration: 1.5, delay: 0, usingSpringWithDamping: 0.2, initialSpringVelocity: 3, options: [], animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
You will want a longer animation duration than 0.15 of a second in order for the bounce to seem realistic; I think the values I have look pretty good, but you can play with them to get the exact effect you are after.
Since the animation duration is longer, I found that I could tap the button the triggered the open/shut while the previous animation was still running. Setting barShut in the completion block meant that the bar didn't react to all taps. I moved the toggle outside of the animation to address this.
When I animate a change to a view's transform, then reset that change in another animation before the first animation finishes, everything's great (shown here with a rotation). The animation smoothly switches to the new target:
But when I do this with a scale, the animation overshoots magnificently:
Here's the breaking code:
UIView.animateWithDuration(1) {
self.someView.layer.transform = CATransform3DMakeScale(0.001, 0.001, 1)
}
UIView.animateWithDuration(1,
delay: 0.5,
options: nil,
animations: {
self.someView.layer.transform = CATransform3DIdentity
}, completion: nil
)
Has anyone else seen this? Am I doing something wrong?
EDIT: And is there a good workaround?
EDIT 2: I believe this is a duplicate of this question and am voting to close.
This blog post provides the answer: in iOS 8, UIView animations are additive, and this has an unfortunate result with scale animations.
Basically, the second animation happens together with the first animation. The only solution is to explicitly remove the original animation before starting a new one:
view.layer.transform = view.layer.presentationLayer().transform
view.layer.removeAllAnimations()
Hi I'm not quite sure what your looking for but if you want the view to go back to it's original scale you'd add the .Autoreverse flag.
UIView.animateWithDuration(1, delay: 0, options: .Autoreverse | .Repeat, animations: {
myView.layer.transform = CATransform3DMakeScale(0.001, 0.001, 1)
}, completion: nil)
While if you wanted to string animations together I'd do it within UIView.animateKeyframesWithDuration()
UIView.animateKeyframesWithDuration(2, delay: 0.0, options: nil, animations: {
UIView.addKeyframeWithRelativeStartTime(0.0, relativeDuration: 0.5, animations: {
// Animation 1
})
UIView.addKeyframeWithRelativeStartTime(1, relativeDuration: 0.5, animations: {
// Animation 2
})
}, completion: nil)