Animating UIStackView subviews causes layout issues - ios

I have a UIStackView with several arranged views inside, which can be shown and hidden using buttons.
I animate the changes by:
- initially setting the alpha to 0 and isHidden = true of some subviews
- in an animate block, switch which subview has alpha = 1 and isHidden = false
I created a playground to show the issue: https://gist.github.com/krummler/d0e8db8cb037ae7202f7d801d3114111
In short, this works fine for two views: switching between ANY two subviews works fine. When pressing a third, the view collapses and refuses to come back. After that, showing subviews becomes a mess. Interestly, it does not show this behaviour when animations are commented out.
My questions:
- Am I missing something or did I hit some bug in UIKit?
- How can I work around this or is there a better wat to achieve what I am trying to do?

It is hard to say how UIStackView is implemented, but it may be attempting to update its layout when isHidden is modified even though the value is not actually changing.
Perhaps this is a UIKit bug, but as a workaround you can modify your resetSubviews(to:) implementation so that it only sets isHidden when the state is actually changing.
private func resetSubviews(to view: UIView) {
view1.alpha = view == view1 ? 1 : 0
view2.alpha = view == view2 ? 1 : 0
view3.alpha = view == view3 ? 1 : 0
view4.alpha = view == view4 ? 1 : 0
let updateIsHiddenForView = { (viewToUpdate: UIView) in
let isHidden = view != viewToUpdate
if isHidden != viewToUpdate.isHidden {
viewToUpdate.isHidden = isHidden
}
}
updateIsHiddenForView(view1)
updateIsHiddenForView(view2)
updateIsHiddenForView(view3)
updateIsHiddenForView(view4)
}

Related

BUG: Hide / Show multiple views arranged in stack view at once

We wanted to hide and show single or multiple views arranged in stack view whenever it is required. So, all what we did is simply:
stackView.arrangedSubviews[0].isHidden = false
stackView.arrangedSubviews[1].isHidden = true
stackView.arrangedSubviews[2].isHidden = false
However, the result is not as expected where stack view or views arranged in the stack view ignores in some cases to be hidden or shown!!
We've found this workaround to fix hide / show multiple views arranged in stack view:
extension UIView {
var isHiddenInStackView: Bool {
get {
return isHidden
}
set {
if isHidden != newValue {
isHidden = newValue
}
}
}
}
Surprisingly, this workaround is working fine but we're wondering if anything else more reliable solution that we can do to avoid causing this bug again later in the future updates?

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. :|

Adding and removing UIStackViews messing up my UIScrollView

Whenever I use the pickerview to switch views from Auto Rent to Schedule Rent it works perfectly. It is when I switch from Schedule Rent to Auto Rent that this black bar appears. I have attached the hierarchy of my content view. I thought it had to do with previous constraints added, so I remove a StackView whenever one view is chosen. For example, if Auto Rent is chosen, then I remove the StackView where the Schedule View is in:
//Holds Temp Stackviews
var stackViewHolder1: UIView?
var stackViewHolder2: UIView?
override func viewDidLoad() {
super.viewDidLoad()
stackViewHolder1 = stackViewMain.arrangedSubviews[0]
stackViewHolder2 = stackViewMain.arrangedSubviews[1]
}
if txtRentType.text == "Auto Rent" {
let tempView = stackViewHolder1
let tempView1 = stackViewHolder2
tempView!.isHidden = true
stackViewMain.removeArrangedSubview(tempView!)
if(tempView1!.isHidden == true){
tempView1!.isHidden = false
stackViewMain.addArrangedSubview(tempView1!)
}
else{
let tempView = stackViewHolder1
let tempView1 = stackViewHolder2
tempView1!.isHidden = true
stackViewMain.removeArrangedSubview(tempView1!)
if(tempView!.isHidden == true){
tempView!.isHidden = false
stackViewMain.addArrangedSubview(tempView!)
}
}
I have tried deleting one view and toggling only one view has being hidden and that removes the black bar issue. There is no constraint with the stackViews and Content View.
EDIT:
The screen highlighted is the scrollView. The one after is the contentView. UIWindow goes black in the back.
My Title Bar at the top ends up in the middle somehow.
You can try to modify your stack distribution property
stack.distribution = .equalCentering
After you won't need to use this:
.removeArrangedSubview()
.addArrangedSubview()
When you hide some view, the other view take all space of your stack, you don't need to update your constraints. You can try it on interface builder to see how it works.
are you pinning your scrollview and the content view to the bottom with constraints?
If the content view is a stack view you can pin it to the bottom as well with layout constraints and play with the content distribution.
You don't need to use remove/Add arranged subviews.
when hiding a view in a stackView its automatically removed.
so i think you can just hide or show the stackViewMain.subviews[0] o stackViewMain.subviews[1]
i'm with objc maybe i do a mistake but it would be something like this :
if txtRentType.text == "Auto Rent" {
stackViewMain.arrangedSubviews[0].isHidden = true;
stackViewMain.arrangedSubviews[1].isHidden = false;
}else{
stackViewMain.arrangedSubviews[1].isHidden = true;
stackViewMain.arrangedSubviews[0].isHidden = false;
}

UIButton frame doesn't change if its panned

Here is an image of a simple APP I am trying to make
The App has a pan gesture recogniser for the centre purple button, the button when moved if it intersects with any of the 4 orange buttons, the purple button animates and moves inside the bounds of button it intersects otherwise it animates back to the initial starting position i.e centre.
For the first time when I pan the purple button the frame of the button updates, but If I try for the second time the frame of button remains the same (the value when it's at the centre of the view).
I am guessing this is something related to Auto Layout that I am missing, because if I remove the constrains on the centre purple button the frame updates every time correctly if I pan.
Can anybody explain what I have to keep in mind when animating with constraints
Here is my code for handling the Pan gesture:
#objc func handleCenterRectPan(_ pannedView: UIPanGestureRecognizer) {
let fallBackAnimation = UIViewPropertyAnimator(duration: 0.3, dampingRatio: 0.5) {
if self.button1.frame.intersects(self.centerRect.frame) {
self.centerRect.center = self.button1.center
} else if self.button2.frame.contains(self.centerRect.frame) {
self.centerRect.center = self.button2.center
} else if self.button3.frame.contains(self.centerRect.frame) {
self.centerRect.center = self.button3.center
} else if self.button4.frame.contains(self.centerRect.frame) {
self.centerRect.center = self.button4.center
} else {
// no intersection move to original position
self.centerRect.frame = OGValues.oGCenter
}
}
switch pannedView.state {
case .began:
self.centerRect.center = pannedView.location(in: self.view)
case .changed:
print("Changed")
self.centerRect.center = pannedView.location(in: self.view)
case .ended, .cancelled, .failed :
print("Ended")
fallBackAnimation.startAnimation();
//snapTheRectangle()
default:
break
}
}
First of all, you shouldn't be giving constraints if your aim is to move your views smoothly. And If you can't resist from constraints than you have to keep updating constraints and not the frames (centre),
In your code, I assume you have given your purple button centre constraints and then you are changing button's centre as user drag using the pan gesture. The problem is constraints you have given are still active and its try to set it back as soon as layout needs to update.
So what you can do is
Don't give constraints as its need to move freely when user drag
OR
Create IBoutlet for constraints and set .isActive to false when user dragging and make it true if it's not inside any of the other buttons and update UpdateLayoutIfNeeded, so it will come back to original position. (good approach)
#IBOutlet var horizontalConstraints: NSLayoutConstraint!
#IBOutlet var verticalConstraints: NSLayoutConstraint!
//make active false as the user is dragging the view
horizontalConstraints.isActive = false
verticalConstraints.isActive = false
self.view.layoutIfNeeded()
//make it true again if its not inside any of the other views
horizontalConstraints.isActive = true
verticalConstraints.isActive = true
self.view.layoutIfNeeded()
OR
Update constraints.constants for the horizontal and vertical constraints when user is moving the view (Not a good approach) and make it zero again if it's not inside of any other view
//make it true again if its not inside any of the other views
horizontalConstraints.constant = change in horizontal direction
verticalConstraints..constant = change in vertical direction
self.view.layoutIfNeeded()
Edit As you said constraints is nil after setting isActive = false because
what isActive actually do is add and remove those constraints as Apple doc says
Activating or deactivating the constraint calls addConstraint(:) and removeConstraint(:) on the view that is the closest common ancestor of the items managed by this constraint. Use this property instead of calling addConstraint(:) or removeConstraint(:) directly.
So making constraints strong reference does solve the issue but I think setting the strong reference to IBOutelet is not a good idea and adding and removing constraints programmatically is better. check out this question deallocating constraints

Swift: removefromSuperview removes constraints

Currently working on some swift program and I've a call to action where I remove a blurview from the superview and at the same time I'm animating 2 buttons.
Everything works like it should but there is one small problem. When I remove the blurview from my superview the constraints on my 2 buttons are being set to 0 on the bottom and animating from that position.
I don't want them to shift to 0. If I don't remove the blurview my animation is working perfectly. I've checked if my button constraints are related to the blurview, but that isn't the case. Because I assumed that it could only reset my constraints when they are relative to the blurview.
My storyboard looks the following:
view
|-> camera_view
|-> blur_view
|-> record_label
|-> record_button
The code that I'm executing is the following:
#IBAction func recordButton(sender: AnyObject) {
self.blurView?.removeFromSuperview()
UIButton.animateWithDuration(0.3, delay: 0.2, options: .CurveEaseOut, animations: {
var recordButtonFrame = self.recordButton.frame
var recordLabelFrame = self.recordLabel.frame
recordButtonFrame.origin.y -= recordButtonFrame.size.height
recordLabelFrame.origin.y -= recordLabelFrame.size.height
self.recordButton.frame = recordButtonFrame
self.recordLabel.frame = recordLabelFrame
}, completion: { finished in
print("Button moved")
})
}
What am I doing wrong?
Kind regards,
Wouter
Instead of removing blurView from superview you could hide it.
Replace
self.blurView?.removeFromSuperview()
with
self.blurView?.hidden = true
The problem is that you're animating frames while using constraints. You should be animating constrain changes / constraint constant value changes.
When you don't remove the view the layout isn't recalculated so your frame animation 'works'. It isn't correct and will get reorganised at some point in the future.
When you remove the view the layout is recalculated and everything moves around before your animation starts.
You don't give details of your constraints but it seems likely that you should be animating constraints before removing the view, then removing and ensuring the constraints are all sane on completion.

Resources