Native layout system is animating - ios

I recently faced an issue where all layouts animated. I can't find out what causes this. Any idea about how can I detect the issue source would be welcome.
Controller hierarchy:
UITabBarController
UINavigationControler
UIViewController
UIPageViewController
UICollectionView
GoogleMap
Visible animated stuff: (without being inside animation block)
layout:
labels and views moving across the cell to the destination position (self-sizing cell)
horizontal collection view items comes from corner to their position
layer:
corner radius from 0 to height/2 animated.
view:
isHidden not works sometimes. It is inside UIStackView and when is hide it, it's just push it out from the stack view (visible because of the bug, strange but it's true)
setting title on buttons animated (setTitle:forState: method. And not the flashing animation, some kind of morphing animation)
Where are the layout and styling codes?
first after viewDidLoad and inside datasource didSet observer
What about threads?
I double checked all UI works dispatched in Main queue and Main Thread Checker is on.
- Show me The code !!!
Unfortunately this is an epic production project and changes that caused this issue is UNKNOWN. I wasn't able to reproduce the issue in some demo app to post it. Sorry

I found the issue after about 3 days of investigating and the answer in just a couple of minutes:
In some point of the code there was a animation block:
UIView.animate(withDuration: 1, animations: {
self.view.layoutIfNeeded()
})
I don't know why but this caused all layout animated for the rest of the controller's life (even after animation is done)
So for get around this I tried this:
UIView.animate(withDuration: 1, animations: {
DispatchQueue.main.async {
self.view.layoutIfNeeded()
}
})
Problem solved BUT!!! new issue appeared:
The original animation is not working at all!
So for the final workaround I changed the code a bit:
DispatchQueue.main.async {
UIView.animate(withDuration: 1, animations: {
self.view.layoutIfNeeded()
})
}
And fortunately it works! I knew:
all UI works should be done on main thread
and also animation blocks doesn't capture self, and perform their job on current thread. but strangely done.

Related

Trouble restraining subview's animation within an animation block

Why restrain a subview from animation? Why not simply carry out its layout changes after the animation?
That's logical but not feasible considering my view hierarchy and this particular use-case.
My view hierarchy:
MyViewController: UIViewController -> MyCustomView: UIView -> MyCustomScrollView: UIScrollView, UIScrollViewDelegate, UICollectionViewDelegate, UICollectionViewDataSource
Why is it not possible?:
1) I do this in MyViewController after various constraint changes:
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
})
2) Since MyCustomView is a subview which contains MyCustomScrollView (which in turn contains a UICollectionView as its subview), the layout update triggers CV's willDisplay delegate method under which I'm adding a bunch of labels to MyCustomView, to be precise.
Here's the function in MyCustomView that I'm calling:
func addLabel(forIndexPath indexPath: IndexPath) {
var label: UILabel!
label.frame = Util.labelFrame(forIndex: indexPath, fillWidth: false) // The frame for the label is generated here!
//Will assign text and font to the label which are unnecessary to this context
self.anotherSubView.addSubview(label) //Add the label to MyCustomView's subview
}
3) Since these changes get caught up within the animation block from point 1, I get some unnecessary, undesired animations happening. And so, MyCustomView's layout change is bound with this animation block, forcing me to look for a way to restrain this from happening
Things tried so far:
1) Tried the wrap the addSubView() from addLabel(forIndexPath:) inside a UIView.performWithoutAnimation {} block. - No luck
2) Tried the wrap the addSubView() from addLabel(forIndexPath:) inside another animation block with 0.0 seconds time as to see if this overrides the parent animation block - No luck
3) Explored UIView.setAnimationsEnabled(enabled:) but it seems like this won't cancel/pause the existing animators, and will completely disable all the animations if true (which is not what I want)
To sum this all up, my problem is:
I need to restrain the animations on MyCustomView but I need all my other desired layout changes to take place. Is this even possible? Would really appreciate a hint or a solution, TYIA!
Thanks to this answer, removing all the animations from anotherSubview's layer(inside the addLabel(forIndexPath:)) after adding the label:
self.anotherSubview.addSubview(label)
self.anotherSubview.layer.removeAllAnimations() //Doing this removes the animations queued to animate the label's frame into the view
does exactly what I want!

Hide one UIView and show another

When I click on an a segment of my UISegmentedControl, I want one of two UIViews to be displayed. But how do I arrange it, that I only show the new view. Currently I am calling thisview.removeFromSuperview() on the old one, and then setup the new all from scratch. I also tried setting all the HeightConstants of the views subviews to zero and then set the heightConstants of the view itself to zero but I'd rather avoid that constraints-surgery..
What better approaches are there?
Agree with #rmaddy about using UIView's hidden property, a nice simple way to cause a view to not be drawn but still occupy its place in the view hierarchy and constraint system.
You can achieve a simple animation to make it a bit less jarring as follows:
UIView.animate(withDuration:0.4, animations: {
myView.alpha = 0
}) { (result: Bool) in
myView.isHidden = true
}
This will fade the alpha on the view "myView", then upon completion set it to hidden.
The same animation concept can be used also if you've views need to re-arrange themselves, animating layout changes will be a nice touch.
Based on #rmaddy and #CSmiths answer, I built the following function:
func changeView(newView: UIView, oldView: UIView) {
newView.isHidden = false
newView.alpha = 0
UIView.animate(withDuration:0.4, animations: {
oldView.alpha = 0
newView.alpha = 1
}) { (result: Bool) in
oldView.isHidden = true
}
}
I feel dumb now for all the hours I spent on that constraint-surgery. :|

Not all content is animated inside a stack view when hiding it

I'm currently working on a iOS (swift 3) app. I have a simple vertical stack view containing 2 horizontal stack views. In some cases I want to hide the bottom one. I do so by using the following code
UIView.animate(withDuration: 3) {
self.bottomStackView.isHidden = true;
};
The animation shown below doesn't really do what I would expect:
While the border of the buttons is animated properly when hiding, the text inside each button doesn't seem to be affected until the very end. Any idea as to how I could fix this?
I kept doing some research on the subject, and it seems like most articles were suggesting that using stacks to perform animation would work fine. However I have also found that animations would only work with animatable properties, isHidden not being one of them.
In the end after some trial and errors I have found that isHidden can be animated with stack views, but you can expect children to misbehave. So far the only workaround I have found is like so:
let duration = 0.5;
let delay = 0;
UIView.animate(withDuration: duration, delay: delay, animations: {
self.bottomStack.isHidden = self.hideBottomStack;
})
UIView.animate(withDuration: duration/2, delay: delay, animations: {
self.bottomStack.alpha = 0;
})
You'll note here that I basically "turn" the alpha property down to 0 in half the time I take to hide the stack. This has the effect to hide the text before it overlaps with the upper stack. Also note that I could also have decided to do something like this:
UIView.animate(withDuration: duration, delay: delay, animations: {
self.bottomStack.alpha = 0;
}, completion: { (_) in
self.bottomStack.isHidden = true;
})
This would also hide the bottom stack, but you lose the hiding motion in favor of a fading motion and hide the stack once the fading is done.
I'm not sure about this, I think stackviews can cause weird behaviour sometimes. Have you tried adding "self.view.layoutIfNeeded()" inside the UIView.animate block? Like this:
UIView.animate(withDuration: 3) {
self.bottomStackView.isHidden = true
self.view.layoutIfNeeded()
}
I think it should also work if you put the "self.bottomStackView.isHidden = true" above the UIView.animate, not sure though, not an expert at it.
Also, I don't think you need to use ";" after your line of code in swift :)

Using animateWithDuration when a button is pressed but it has no effect (swift 2)

I'm using the following code to have a label slide onto the screen when a button is pressed, but it's having no effect.
override func viewDidLayoutSubviews() {
summaryLabel.alpha = 0
}
#IBAction func searchButton(sender: AnyObject) {
UIView.animateWithDuration(2) { () -> Void in
self.summaryLabel.center = CGPointMake(self.summaryLabel.center.x - 400, self.summaryLabel.center.y)
self.summaryLabel.alpha = 1
self.summaryLabel.center = CGPointMake(self.summaryLabel.center.x + 400, self.summaryLabel.center.y)
}
//button code continues...
I've tested what's going on by fixing the alpha at 1, but the label just stays where it is and does not move when the button is pressed. What am I missing?
A couple of things:
First of all, your two changes to the view's center cancel each other out. The animation applies the full set of changes that are inside the animation block all in one animation. If the end result is no change, then no change is applied. As Ramy says in his comment, you either need 2 animations, timed so the second one takes place after the first one has completed, or you nee to apply the first change before the animation begins. I would suggest starting with a single change, and a single animation, and get that working first.
Second problem: View controllers use auto layout by default. With auto layout, you can't animate the position of a view directly. It doesn't work reliably. Instead, you have to put a constraint on the view, connect it to an outlet, and the animate a change to the constraint's constant value by changing the constant and calling layoutIfNeeded() inside the animation block. The call to layoutIfNeeded() inside the animation block causes the view's position to be changed, and since it's inside the animation block, the change is applied with animation.

Swift: removeFromSubview happens before animation

I have a little setup where I have an MPMoviePlayerController setup as a video background. I'm trying to get it to have a smooth transition when it updates/changes. Here's my current code. BTW, newBackground.view.alpha is initially 0.
let oldBackground = currentBackground
self.view.insertSubview(newBackground.view, aboveSubview: oldBackground.view)
UIView.animateWithDuration(1.5 as NSTimeInterval, animations: {
newBackground.view.alpha = 1
}, completion: {
finished in
oldBackground.view.removeFromSuperview()
println("The subview should be removed now")
})
When this executes, the oldBackground.view is immediately removed before the newBackground starts to fade in. The println, however, happens after the animation is completed. I don't understand why the removeFromSuperview happens immediately, but the println happens when I expect it to. If I remove oldBackground.view.removeFromSuperview(), the animation fades in and looks fine (but the view obviously hasn't been removed, it's just sitting behind newBackground).
EDIT: Bizarrely enough, it seems to work as expected in the simulator. Running on my iPhone 6 Plus gives me the issue every time. I've uninstalled and re-run it from Xcode and the problem persists.
If anyone has any advice for me, I would be very happy. Thank you.
I guess when the newBackground.view.alpha = 1 is executed during animation it brings the background to visibility and overlays the oldBackground.
When you put the new subview above the old subview, old subview goes to the back. This happens before the animation code starts. Make sure your new subview's alpha is 0 before inserting it. And if you want a fade out animation you should set your oldBackground alpha to 0.
Like this :
let oldBackground = currentBackground
newBackground.view.alpha = 0
self.view.insertSubview(newBackground.view, aboveSubview: oldBackground.view)
UIView.animateWithDuration(1.5 as NSTimeInterval, animations: {
newBackground.view.alpha = 1
oldBackground.view.alpha = 0
}, completion: {
finished in
oldBackground.view.removeFromSuperview()
println("The subview should be removed now")
})

Resources