UIStackView - layout constraint issues when hiding stack views
Asked Answered
A

13

29

My app has 2 screens:

  1. TableViewVC (no stack views here)

  2. DetailVC (all the nested stack views here; please see link for picture: Nested StackViews Picture) -- Note, there are labels and images within these stack views.

When you press a cell in the tableview, it passes the information from the TableViewVC to the DetailVC. The problem is with hiding the specific UIStackViews in the DetailVC. I want only 2 stack views out of the various ones in the DetailVC to be hidden as soon as the view loads. So I write this code in the DetailVC to accomplish this:

override func viewDidLoad() {
    super.viewDidLoad()

    self.nameLabel.text = "John"

    self.summaryStackView.hidden = true
    self.combinedStackView.hidden = true
}

Everything looks great but Xcode give many warnings only at runtime. There are no warning in Storyboard when the app is not running. Please see link for picture of errors: Picture of Errors

Basically it's a lot of UISV-hiding, UISV-spacing, UISV-canvas-connection errors. These errors go away if I hide the same stack views in viewDidAppear but then there is a flash of the stuff that was supposed to be hidden and then it hides. The user sees the the view briefly and then it hides which is not good.

Sorry for not being able to actually post pictures instead of links, still can't do so.

Any suggestions on how to fix this? This is for an app I actually want to launch to the app store - it's my first so any help would be great!

Edit/ Update 1:

I found a small work around with this code which I put inside the second screen called DetailVC:

// Function I use to delay hiding of views
func delay(delay: Double, closure: ()->()) {
    dispatch_after(
        dispatch_time(
            DISPATCH_TIME_NOW,
            Int64(delay * Double(NSEC_PER_SEC))
        ),
        dispatch_get_main_queue(), closure)
}

// Hide the 2 stack views after 0.0001 seconds of screen loading
override func awakeFromNib() {
    delay(0.001) { () -> () in
        self.summaryStackView.hidden = true
        self.combinedStackView.hidden = true
    }
}

// Update view screen elements after 0.1 seconds in viewWillAppear
override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    delay(0.1) { () -> () in
        self.nameLabel.text = "John"
    }
}

This gets rid of the warnings about layout constraints completely from Xcode.

It's still not perfect because sometimes I see a glimpse of the views that are supposed to be hidden -- they flash really quick on the screen then disappear. This happens so quickly though.

Any suggestions as to why this gets rid of warnings? Also, any suggestions on how to improve this to work perfectly??? Thanks!

Artificer answered 11/11, 2015 at 1:41 Comment(9)
The problem is conflicting constraints. Try something like self.view.layoutIfNeeded() at the end of viewDidLoad?Applied
@Applied Hey, thanks for replying. Just tried your suggestion and the warnings still come. By the way, I updated my question with what else I do in viewDidLoad -- I update a label and some other things but I just put one label there for simplicity. Do you think I should call layoutIfNeeded anywhere else?Artificer
Well it depends on your layout in storyboard. You need to play around with the auto-constraints there, or possibly set constraints programmatically after the changes take place. There's lots of answers on here about how to do that. Another line that is sometimes helpful is self.view.translatesAutoresizingMaskIntoConstraints = false. You could try that before layoutIfNeeded. I'm just shooting out all the stuff I've tried, which is why I don't actually have an answer. Good luck!Applied
@Applied Just tried self.view.translatesAutoresizingMaskIntoConstraints = false -- when I click on a cell from the first screen to show the second screen, the second screen shows and starts off with a half black screen and then goes completely black except for the navigation bar. Thanks for the suggestions anyways!Artificer
Why don't you hide the views in the storyboard initially instead of hiding them in viewDidLoad? By the way, if you use a view controller, you do not normally need awakeFromNib, use viewDidLoad instead.Scandian
@Davyd Interesting, I just tried that now and as soon as I hide the 2 stack views in Storyboard, I get a long list of red warnings from Xcode. I took a picture of some of them: i.sstatic.net/pSUIV.png ---- Could this be what's happening at runtime? With the delay function it works, but without it could this be what's going on?Artificer
@Larry, yep this is exaxctly what you need to fix in design-time now. See my updated answer. I have a feeling the problem is with the scroll view constraints. I would suggest to invest some time learning how to apply auto layout constraints to scroll view.Scandian
@JEL, Can you reconsider choosing the accepted answer. There might be better ones.Writhe
Possible duplicate of Nested UIStackViews Broken ConstraintsEphemeron
F
-4

Have you tried this? Calling super after your changes?

override func viewWillAppear() {
    self.nameLabel.text = "John"
    self.summaryStackView.hidden = true
    self.combinedStackView.hidden = true
    super.viewWillAppear()
}
Fervency answered 22/3, 2017 at 23:37 Comment(2)
It is looks like workaround, not a fix. The second question is real fixScabble
"The second question is real fix" - Probably you mean "answer", not a "question". Anyway, let's share the link to the mostly upvoted answer here so the new-joiners use it (https://mcmap.net/q/477902/-uistackview-layout-constraint-issues-when-hiding-stack-views) instead of workarounds :)Weekend
W
88

I had the same problem and I fixed it by giving the height constraints of my initially hidden views a priority of 999.

enter image description here

The problem is that your stackview applies a height constraint of 0 on your hidden view which conflicts with your other height constraint. This was the error message:

Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) 
(
    "<NSLayoutConstraint:0x7fa3a5004310 V:[App.DummyView:0x7fa3a5003fd0(40)]>",
    "<NSLayoutConstraint:0x7fa3a3e44190 'UISV-hiding' V:[App.DummyView:0x7fa3a5003fd0(0)]>"
)

Giving your height constraint a lower priority solves this problem.

Writhe answered 10/12, 2015 at 9:28 Comment(7)
Agree with @tsp, great answer solved my issue, thank you.Belomancy
Really appreciate answers that explain the issue and why the fix worksCaldron
Oddly enough, putting the hiding code inside a UIView animation block also fixes it for me. No clue why. I'll do both just to be safe. :-)Byrne
I was running into the same problem and I feel this is the correct answer. Thanks!Chapen
Thanks for the answer. I lower the priority for all the stackviews that may be hidden.Buckler
The only problem with this is when you have non-zero spacing in your stack view. The spacing constraints still conflict with the hiding constraint.Flavourful
@MaciejTrybiło How to overcome that issue when having non-zero spacing in stack view?Cath
E
35

This is a known problem with hiding nested stack views.

There are essentially 3 solutions to this problem:

  1. Change the spacing to 0, but then you'll need to remember the previous spacing value.
  2. Call innerStackView.removeFromSuperview(), but then you'll need to remember where to insert the stack view.
  3. Wrap the stack view in a UIView with at least one 999 constraint. E.g. Top, Leading, Trailing @ 1000, Bottom@999.

The 3rd option is the best in my opinion. For more information about this problem, why it happens, the different solutions, and how to implement solution 3, see my answer to a similar question.

Ephemeron answered 7/7, 2016 at 5:1 Comment(4)
The 3rd option works great and provides a very clean solution. Thank you for sharing!Boothman
The 3rd option is the best solution indeed. Thank you!Corsetti
You made my day! Thank you. Making priority to 999 solved my question.Nigrify
@Ephemeron Thank you. Option 3 helped me solve out of memory issue. here is the post. https://mcmap.net/q/501119/-app-crashes-with-low-memory-only-on-iphone-8-ios-14-4-1/430690. I faced this on iOS 14.4 but only on iPhone 8. It didn't give problem on iPhone 11 and 8 Plus devices.Jahdai
A
8

You can use the removeArrangedSubview and removeFromSuperview property of UIStackView.

In Objective-C :

 [self.topStackView removeArrangedSubview:self.summaryStackView];
 [self.summaryStackView removeFromSuperview];

 [self.topStackView removeArrangedSubview:self.combinedStackView];
 [self.combinedStackView removeFromSuperview];

From Apple UIStackView Documentation:(https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIStackView_Class_Reference/#//apple_ref/occ/instm/UIStackView/removeArrangedSubview:)

The stack view automatically updates its layout whenever views are added, removed or inserted into the arrangedSubviews array.

  • removeArrangedSubview: This method removes the provided view from the stack’s arrangedSubviews array. The view’s position and size will no longer be managed by the stack view. However, this method does not remove the provided view from the stack’s subviews array; therefore, the view is still displayed as part of the view hierarchy.

To prevent the view from appearing on screen after calling the stack’s removeArrangedSubview: method, explicitly remove the view from the subviews array by calling the view’s removeFromSuperview method, or set the view’s hidden property to YES.

Andrade answered 6/2, 2016 at 11:26 Comment(0)
M
3

When the UIViewStack is hidden, the constraints automatically generated by the UIStackView will throw lots of UISV-hiding, UISV-spacing, UISV-canvas-connection warnings, if the UIStackView's spacing property has any value other than zero.

This doesn't make much sense, it's almost certainly a framework bug. The workaround I use is to set the spacing to zero when hiding the component.

if hideStackView {
    myStackView.hidden = true
    myStackView.spacing = CGFloat(0)
} else {
    myStackView.hidden = false
    myStackView.spacing = CGFloat(8)
}
Megaspore answered 3/5, 2016 at 9:4 Comment(1)
This solution worked perfectly for my stack views which had dynamic heights (height constraints could not be set as in other answers)Radiator
S
3

I've found that nested UIStackViews show this behavior if you set the hidden property in ✨Interface Builder✨. My solution was to set everything to not hidden in ✨Interface Builder✨, and hide things in viewWillAppear selectively.

Stallard answered 20/7, 2017 at 1:43 Comment(1)
Perfect! Now in 2020 it is still actual crash, if you support iOS12. Just set stack views to visible in storyboard.Marquisette
S
1

This error is not about hiding, but about ambiguous constraints. You must not have any ambiguous constraints in your view. If you add them programmatically you should exactly understand what constraints you add and how they work together. If you do not add them programmatically, but use storyboard or xib, which is a good place to start, make sure there are no constraint errors or warnings.

UPD: You have a pretty complex structure of views there. Without seeing the constraints is hard to say what exactly is wrong. However, I would suggest to build you view hierarchy gradually adding views one by one and making sure there are no design-time/runtime warnings. Scroll view may add another level of complexity if you do not handle it correctly. Find out how to use constraints with a scroll view. All other timing hacks is not a solution anyway.

Scandian answered 11/11, 2015 at 3:0 Comment(3)
I am using storyboards and there are no warning or errors. The warnings only come when I am running the app and go to the second screen. So that confuses me about what I need to solve with constraints since theres no warnings. I'm still learning about all this and trying my best to solve it. Please see my updated Question with a small work around which solves the warning but still isn't perfect. Please check it out --- any suggestions to make this work better?Artificer
I just deleted 4 UIViews that had fixed Height constraints and everything went away in design-time. Then at run-time the warning come back all over again. This is crazy. I mean the screen works perfectly and theres no crashes --- do you think I could launch to the app store with warnings like this though?Artificer
@Larry I do not know if Apple would reject it or not.Scandian
D
1

I moved all UIStackView.hidden code from viewDidLoad to viewDidAppear and broken constraints problem went away. In my case all conflicting constraints were auto generated, so no way to adjust priorities.

I also used this code to make it prettier:

UIView.animateWithDuration(0.5) {
    self.deliveryGroup.hidden = self.shipVia != "1"
}

EDIT:

Also needed the following code to stop it from happening again when device is rotated:

override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)

    self.deliveryGroup.hidden = false

    coordinator.animateAlongsideTransition(nil) {
    context in
        self.deliveryGroup.hidden = self.shipVia != "1"
    }
}
Disarrange answered 30/12, 2015 at 0:38 Comment(3)
As I said in my question above, viewDidAppear does fix my error messages just like it did for you. But then the user briefly sees what was supposed to be hidden. My view is like an expanding/collapsing table made of stack views. If it's hidden in viewDidAppear, the user will see the stack collapse together which is not intended. Does this make sense? Do you have any ideas on fixing this? Thanks.Artificer
no I just accept this and use animateWithDuration so it looks intentional. I believe the underlying problem is just a bug with UIStackView, perhaps related to nested stack views. One other thing I did not try was iterating over all of the stackview's children, grandchildren, etc and setting the hidden flag.Disarrange
Ok I think I might do the same thing and animate it hiding in viewDidAppear. Do you see others having a similar issue? Is that why you think it's a bug?Artificer
S
1

I fixed it by putting the hide commands in traitCollectionDidChange.

override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    self.item.hidden = true
}
Stilbite answered 7/1, 2016 at 7:5 Comment(2)
THIS WORKS! Thanks so much for helping out really! : ) I'm curious to know how you figured this out so I could become a better developer. I know some of the view life cycle but had no idea this existed. Can you teach me how this traitCollectionDidChange works and also how you figured out this was the correct answer to this problem?Artificer
This could cause unexpected bugs. traitCollectionDidChange will get called when the user rotates the phone, which in this case would hide the view. Setting the constraint priority, as in @Writhe 's answer below, is a more correct solution.Foreman
S
1

So, this may only help 0.000001% of users but maybe this is a clips to bounds issue.

I ran into this recently when working with UICollectionViewCell I forgot to check clips to bounds on the view I was treating as my content view. When you create a UITableViewCell in IB it sets up a content view with clips to bounds as the default.

Point is, depending on your situation you may be able to accomplish your intended effect using frames and clipping.

Scaife answered 29/11, 2018 at 20:41 Comment(0)
A
0

Put your hide commands in viewWillLayoutSubviews() {}

Applied answered 11/11, 2015 at 3:20 Comment(8)
Just tried again now. Hiding in viewWillAppear causes the same warnings, but hiding in 'viewWillAppear' with the delay function I added above (delay for 0.1 seconds) makes all the warnings go away. Weird, don't get why.Artificer
How about viewWillLayoutSubviews>Applied
I tried in both viewDidLoad and viewWillAppear but no good. The order was the same as above in my code, I just added self.viewWillLayoutSubviews() at the end.Artificer
you can do override func viewWillLayoutSubviews() {} as an override code, that's what I meant. I would try hiding in that.Applied
Oh ok, so I tried it out and the warnings went away!!! The only problem is that I can't unhide the stack views -- I'm going to keep working with this to see if I can put it together. I'll let you know what I come up with --- thanks!Artificer
No luck, I created a new project and nested two stack views and added a button to toggle the hide/unhide. If I initially hide it inside of override func viewWillLayoutSubviews() {} then I can't seem to unhide the stack view.Artificer
You need an if clause that checks for the reasons to hide or unhide.Applied
Please see this link for picture to a sample project I created just to test out what you said: i.sstatic.net/Iko3K.png --- In the picture, the TOGGLE button is used to unhide the second stack view but it does not work. How would you change this around?Artificer
F
0

I did this by storing all the hidden views of the nested UIStackView in an array and removing them from the superview and arranged subviews. When I wanted them to appear again I looped through the array and added them back again. This was the first step.

The second step is after you remove the views of the nested UIStackView from the superview the parent UIStackView still doesn't adjust it's height. You can fix this by removing the nested UIStackView and adding it again straight afterwards:

UIStackView *myStackView;
NSUInteger positionOfMyStackView = [parentStackView indexOfObject:myStackView];
[parentStackView removeArrangedSubview:myStackView];
[myStackView removeFromSuperview];
[parentStackView insertArrangedSubview:myStackView atIndex:positionOfMyStackView];
Fallacy answered 24/7, 2018 at 17:20 Comment(0)
M
0

If you're having issues animating HIDING AND SHOWING subviews at the same time, repeating the .isHidden instructions in the animation completion may help. See my answer here for more detail on that.

Mockery answered 7/8, 2020 at 22:9 Comment(0)
F
-4

Have you tried this? Calling super after your changes?

override func viewWillAppear() {
    self.nameLabel.text = "John"
    self.summaryStackView.hidden = true
    self.combinedStackView.hidden = true
    super.viewWillAppear()
}
Fervency answered 22/3, 2017 at 23:37 Comment(2)
It is looks like workaround, not a fix. The second question is real fixScabble
"The second question is real fix" - Probably you mean "answer", not a "question". Anyway, let's share the link to the mostly upvoted answer here so the new-joiners use it (https://mcmap.net/q/477902/-uistackview-layout-constraint-issues-when-hiding-stack-views) instead of workarounds :)Weekend

© 2022 - 2024 — McMap. All rights reserved.