Hello I'm trying to understand view update cycle
I could understand almost things, but there are some mysterious things
I learn that animation blocks are also triggered at update cycle
To try to understand layoutIfNeeded in Animation block, I wrote some sample codes like below
class MyView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
print("Layout Subviews")
}
}
class ViewController: UIViewController {
#IBOutlet weak var blueHeight: NSLayoutConstraint!
#IBAction func heightPressed(_ sender: AnyObject) {
if self.blueHeight.constant == 25.0 {
self.blueHeight.constant = self.view.bounds.height - 100.0
} else {
self.blueHeight.constant = 25.0
}
self.view.setNeedsLayout()
UIView.animate(withDuration: 2.0, animations: {
print("animation block")
self.view.layoutIfNeeded() // ----- **
}) { (_) in
print("animation ended")
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
The output was like this
"Layout Subviews"
"Layout Subviews" ---- because of initial settings
"Animation block"
"Layout Subviews"
"Animation ended"
but when i changed layoutIfNeeded to setNeedsLayout the output is changed
"Layout Subviews"
"Layout Subviews" ---- because of initial settings
"Animation block"
"Animation ended"
"Layout Subviews"
so i understand that animation block executed first and view updates, so animation block has higher priority than view's update in update cycle
is that right? If i was wrong, i want to understand relationship between view's update cycles and animation block
What is inside the animation block, if it is animatable, does not run until animation time at the end of the transaction.
layoutIfNeeded counts as animatable. What it does in an animation block is: it causes the changes in frame etc. performed in code or by autolayout in layoutSubviews to be animated rather than immediate.
Basically, this is a way of wrapping the next layout moment in an animation.
Related
I notice that, if I perform add/ expand animation within an UIScrollView, it will cause unwanted scrolling behavior, when the UIScrollView fill with enough content to become scroll-able.
As you can see in the following animation, initially, the add/ expand animation works just fine.
When we have added enough item till the UIScrollView scrollable, whenever a new item is added, and UIScrollView will first perform scroll down, and then scroll up again!
My expectation is that, the UIScrollView should remain static, when add/ expand animation is performed.
Here's the code which performs add/ expand animation.
Add/ expand animation
#IBAction func add(_ sender: Any) {
let customView = CustomView.instanceFromNib()
customView.hide()
stackView.addArrangedSubview(customView)
// Clear off horizontal swipe in animation caused by addArrangedSubview
stackView.superview?.layoutIfNeeded()
customView.show()
// Perform expand animation.
UIView.animate(withDuration: 1) {
self.stackView.superview?.layoutIfNeeded()
}
}
Here's the constraint setup of the UIScrollView & added custom view item
Constraint setup
Custom view
class CustomView: UIView {
private var zeroHeightConstraint: NSLayoutConstraint!
#IBOutlet weak var borderView: UIView!
#IBOutlet weak var stackView: UIStackView!
override func awakeFromNib() {
super.awakeFromNib()
borderView.layer.cornerRadius = stackView.frame.height / 2
borderView.layer.masksToBounds = true
borderView.layer.borderWidth = 1
zeroHeightConstraint = self.safeAreaLayoutGuide.heightAnchor.constraint(equalToConstant: 0)
zeroHeightConstraint.isActive = false
}
func hide() {
zeroHeightConstraint.isActive = true
}
func show() {
zeroHeightConstraint.isActive = false
}
}
Here's the complete source code
https://github.com/yccheok/add-expand-animation-in-scroll-view
Do you have any idea why such problem occur, and we can fix such? Thanks.
Because of the way stack views arrange their subviews, animation can be problematic.
One approach that you may find works better is to embed the stack view in a "container" view.
That way, you can use the .isHidden property when adding an arranged subview, and allow the animation to update the "container" view:
The "add view" function now becomes (I added a Bool so we can skip the animation on the initial add in viewDidLoad()):
func addCustomView(_ animated: Bool) {
let customView = CustomView.instanceFromNib()
stackView.addArrangedSubview(customView)
customView.isHidden = true
if animated {
DispatchQueue.main.async {
UIView.animate(withDuration: 1) {
customView.isHidden = false
}
}
} else {
customView.isHidden = false
}
}
And we can get rid of all of the hide() / show() and zeroHeightConstraint in the custom view class:
class CustomView: UIView {
#IBOutlet weak var borderView: UIView!
#IBOutlet weak var stackView: UIStackView!
override func awakeFromNib() {
super.awakeFromNib()
borderView.layer.masksToBounds = true
borderView.layer.borderWidth = 1
}
override func layoutSubviews() {
super.layoutSubviews()
borderView.layer.cornerRadius = borderView.bounds.height * 0.5
}
}
Since it's a bit difficult to clearly show everything here, I forked your project with the changes: https://github.com/DonMag/add-expand-animation-in-scroll-view
Edit
Another "quirk" of animating a stack view shows up when adding the first arranged subview (also, when removing the last one).
One way to get around that is to add an empty view as the first subview.
So, for this example, in viewDidLoad() before adding an instance of CustomView:
let v = UIView()
stackView.addArrangedSubview(v)
This will make the first arranged subview a zero-height view (so it won't be visible).
Then, if you're implementing removing custom views, just make sure you don't remove that first, empty view.
If your stack view has .spacing = 0 noting else is needed.
If your stack view has a non-zero spacing, add another line:
let v = UIView()
stackView.addArrangedSubview(v)
stackView.setCustomSpacing(0, after: v)
I did a little research on this and the consensus was to update the isHidden and alpha properties when inserting a view with animations.
In CustomView:
func hide() {
alpha = 0.0
isHidden = true
zeroHeightConstraint.isActive = true
}
func show() {
alpha = 1.0
isHidden = false
zeroHeightConstraint.isActive = false
}
In your view controller:
#IBAction func add(_ sender: Any) {
let customView = CustomView.instanceFromNib()
customView.hide()
stackView.addArrangedSubview(customView)
self.stackView.layoutIfNeeded()
UIView.animate(withDuration: 00.5) {
customView.show()
self.stackView.layoutIfNeeded()
}
}
Also, the constraints in your storyboard aren't totally correct. You are seeing a red constraint error because autolayout doesn't know the height of your stackView. You can give it a fake height and make sure that "Remove at build time" is checked.
Also, get rid of your scrollView contentView height constraint defined as View.height >= Frame Layout Guide.height. Autolayout doesn't need to know the height, it just needs to know how subviews inside of the contentView stack up to define its vertical content size.
Everything else looks pretty good.
I'm using UIStackView and it contains 3 UIView instances, which has fixed height
I'm trying to hide these subviews by clicking button
first and second view show/hide well with proper animation
but last view doesn't animate
class ViewController: UIViewController {
private var flag: Bool = true
#IBOutlet weak var targetView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func buttonDidTapped(_ sender: Any) {
flag = !flag
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
self.targetView.isHidden = !self.flag
}
}
}
The issue is the way stack views change their frames when hiding an arranged subview.
Easiest way to see what's happening:
set your Green view to Alpha: 0.5
toggle .isHidden on the Blue view
You'll see that the 50% translucent Green view "slides up over" the Blue view... the Blue view does not "shrink in height" during the animation.
To solve your specific issue, set Clips To Bounds to true on your stack view. Now, when you toggle .isHidden on your Green view, the animation will look correct.
That will not change the "slide over" appearance if you have translucent views, but that's a different issue.
As a side note, you can simplify your code and get rid of the flag like this:
UIView.animate(withDuration: 0.5) {
// not needed
//self.view.layoutIfNeeded()
self.targetView.isHidden.toggle()
}
Try change your code from:
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
self.targetView.isHidden = !self.flag
}
to:
self.targetView.isHidden = !self.flag
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
}
Looks like you animate before change.
I'm trying to have a simple slide-down-from-top animation using auto layout (with the help of SnapKit).
Below is the code I use to generate a view, set its constraints and force a reload (thus display) of said view. In the same code cope I change the top constraint to a different value and call layoutIfNeeded in an animation block.
What happens instead though is that the view is displayed at the intended position without any animations whatsoever.
Here is the code called in a method after a short delay (for other purposes):
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(4), execute: {
self.view.addSubview(view)
view.backgroundColor = .black
view.snp.makeConstraints { (make) -> Void in
make.left.right.equalToSuperview()
make.height.equalTo(200)
make.bottom.equalTo(self.view.snp.top) // Pin view to top to prepare slide-down animation
}
view.setNeedsLayout()
view.layoutIfNeeded()
view.snp.updateConstraints { (make) -> Void in
make.bottom.equalTo(self.view.snp.top).offset(200)
}
view.setNeedsLayout()
UIView.animate(withDuration: 5, animations: {
view.layoutIfNeeded()
})
})
Thanks to In Swift, how do I animate a UIView with auto-layout like a page sliding in? I found the culprit.
Although I don't 100% understand why this is, but layoutIfNeeded needs to be called from the superview, not the view itself.
So calling self.view.layoutIfNeeded() is the correct way. Note that view is the currently being added UIView, whereas self.view is the superview, where view is being added to.
I'm facing a problem : I have a very simple custom UIView which groups one UITextField and one UILabel.
The UILabel is on the UITextField, and I want that the UILabel go up to 40px when user begins to edit the UITextField.
I'm using constraints to set the textfield's frame and the label's frame.
So I just set the delegate in my custom view with UITextFieldDelegate protocol, and implement the delegate method : textFieldDidBeginEditing, as bellow :
I'm just animating the constraint (center Y) of my label in the delegate method.
func textFieldDidBeginEditing(textField: UITextField) {
self.centerVerticallyLabelConstraint.constant = -40
UIView.animateWithDuration(0.5) {
self.textFieldLabel.layoutIfNeeded()
}
}
What happens is that the UILabel goes up correctly, but it goes up instantly. It ignores the duration of 0.5 in my animation. I don't know why :
I'm not animating something else at the same time, I only have this animation.
I haven't disable animations for UIView.
The delegate is set to my UITextfield.
Any idea?
Try calling layoutIfNeeded on your view instead of textview.
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
}
Hope this helps.
need to call self.view.layoutIfNeeded() not self.textFieldLabel.layoutIfNeeded()
Thanks
Try Below code to execute:
func textFieldDidBeginEditing(textField: UITextField) {
UIView.animate(withDuration: 0.5) {
self.textFieldLabel.frame.origin.y -= 40
}
}
For me, the issue seems to be because I was already on the main thread & I called the animation block inside a GCD main.async call.
ie I had
DispatchQueue.main.async {
<# update the constraint s#>
self.animatedViewsParent.setNeedsLayout()
animatedView.setNeedsLayout()
UIView.animate(withDuration: 2.0) {
self.animatedViewsParent.layoutIfNeeded()
}
}
The view animates moves but does not animate gradually over 2.0 seconds. Removing the outer DispatchQueue.main.async {} solves the problem.
I have watched the WWDC 2012 presentations on Auto Layout and read the documentation on the view appearance calls.
So I thought I needed to perhaps wait a frame or a second after viewDidAppear just to be safe, but still didn't work. Here was my code:
override func viewDidAppear(animated: Bool)
{
super.viewDidAppear(animated)
view.layoutIfNeeded()
view.autoresizesSubviews = false
_textButton.frame = CGRectMake(0, 0, 0, 0)
println(_textButton.frame)
let delay:Double = 4*Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue())
{
self._textButton.frame = CGRectMake(0, 0, 0, 0)
println(self._textButton.frame)
}
}
This actually prints out (144.0,6.5,32.0,32.0) twice in a row. Which means even AFTER setting the frame to 0, it was set back to its constraint defaults.
Why is this?
I have another ViewController that looks almost the same as this one, with buttons having the same constraints. But when I close the view, I animate the buttons to slide out to the left with the following code:
#IBAction func takePhotoTap(sender: AnyObject)
{
UIView.animateWithDuration(0.5, animations: animations)
_camera.captureImage(receivePhoto)
}
func animations()
{
var height = CGFloat(_distanceBetweenTopAndMiddleBar)/2
_lowerLens.frame = CGRectMake(_lowerLens.frame.origin.x, _lowerLens.frame.origin.y, _lowerLens.frame.width,-height)
_upperLens.frame = CGRectMake(_upperLens.frame.origin.x, _upperLens.frame.origin.y, _upperLens.frame.width,height)
view.autoresizesSubviews = false
slideOffScreenLeft(_gridLinesButton)
slideOffScreenLeft(_swapCameraButton)
slideOffScreenLeft(_flashButton)
}
func slideOffScreenLeft(obj:UIView)
{
obj.frame = CGRectMake((-obj.frame.width), obj.frame.origin.y, obj.frame.width, obj.frame.height)
}
This works JUST FINE! When they hit the button, these buttons slide off the screen. However when I load the next view I want the buttons to slide in from the right. But, as you can see above, even waiting 4 seconds before trying to set the frames of the buttons has no effect.
Can you suggest what I can do to animate some buttons to slide in the from the screen when the view loads? Why are the constraints overriding my changes in the first case but when I animated a View Controller before closing it with UIView.animateWithDuration the constraints were overriden?
When using AutoLayout, it is best practice to create outlets for your constraints and then modify the constraints, rather than the frames of your objects. I was having a similar problem. This also gets rid of the need for the dispatch_after call.
For example:
#IBOutlet weak var buttonHeight: NSLayoutConstraint!
#IBOutlet weak var buttonWidth: NSLayoutConstraint!
override func viewDidAppear(animated: Bool)
{
super.viewDidAppear(animated)
// Modify the constraint rather than the frame
self.buttonHeight.constant = 0
self.buttonWidth.constant = 0
}
This will make the button 0x0. You can then do the same for the x,y position.