Swift: Disappearing views from a stackView
Asked Answered
L

5

21

I have a fairly simple set up in my main storyboard:

  • A stack view which includes three views
  • The first view has a fixed height and contains a segment controller
  • The other two views have no restrictions, the idea being that only one will be active at a time and thus fill the space available

I have code that will deal with the changing view active views as follows:

import Foundation
import UIKit
class ViewController : UIViewController {
    @IBOutlet weak var stackView: UIStackView!
    @IBOutlet weak var segmentController: UISegmentedControl!
    @IBAction func SegmentClicked(_ sender: AnyObject) {
        updateView(segment: sender.titleForSegment(at: sender.selectedSegmentIndex)!)
    }
    override func viewDidLoad() {
        updateView(segment: "First")
    }
    func updateView(segment: String) {
        UIView.animate(withDuration: 1) {
            if(segment == "First") {
                self.stackView.arrangedSubviews[1].isHidden = false
                self.stackView.arrangedSubviews[2].isHidden = true
            } else {
                self.stackView.arrangedSubviews[1].isHidden = true
                self.stackView.arrangedSubviews[2].isHidden = false

            }
            print("Updating views")
            print("View 1 is \(self.stackView.arrangedSubviews[1].isHidden ? "hidden" : "visible")")
            print("View 2 is \(self.stackView.arrangedSubviews[2].isHidden ? "hidden" : "visible")")
        }
    }
}

As you can see, when the tab called 'First' is selected, the subview at index 1 should show, whilst 2 is hidden, and when anything else is selected, the subview at index 2 should show, whilst 1 is hidden.

This appears to work at first, if I go slowly changing views, but if I go a bit quicker, the view at index 1 seems to remain permanently hidden after a few clicks, resulting in the view at index 0 covering the whole screen. I've placed an animation showing the issue and a screenshot of the storyboard below. The output shows that when the problem happens, both views remain hidden when clicking on the first segment.

Can anybody tell me why this is happening? Is this a bug, or am I not doing something I should be?

Many thanks in advance!

Animation showing issue

Image of stack view in storyboard

Update: I seem to be able to reliably reproduce the issue by going to the First > Second > Third > Second > First segments in that order.

Lepage answered 12/10, 2016 at 14:37 Comment(5)
You said your two others view don't have "restrictions" You should set them up with constraints. Then when you want them to not be shown in the view tell them to be hidden. The cool thing with stack views is when you tell something to hide in it the stack view will re adjust to fit the remaining views correctly.Sestet
The text "First" is actually visible at the bottom in your animation, so it is not hidden. Are you sure you have set a fixed height constraint on the segment control?Campground
@phix23 the fixed height is on the first child view of the stack view, that contains the segment control. It's set to height = 32. I've also tried that out of the stack view anyway, so I don't believe it's that. It's also reporting as hidden from the print, so it definitely thinks it is hidden (unless I maybe need to wait for the animation to finish before checking?).Lepage
@Sestet My understanding that was that views should automatically fill the stack views if they didn't have constraints? Either way, even with 0/0/0/0 constraints the problem still occurs. I believe it's because the code isn't able to unhide the first view for some reason, but I can't work out why.Lepage
i think so, StackView handle automatically when you show and hide view.Virendra
L
5

In the end, after trying all the suggestions here I still couldn't work out why it was behaving like this so I got in touch with Apple who asked me to file a bug report. I did however find a work around, by unhiding both views first, which solved my problem:

func updateView(segment: String) {
    UIView.animate(withDuration: 1) {
        self.stackView.arrangedSubviews[1].isHidden = false
        self.stackView.arrangedSubviews[2].isHidden = false
        if(segment == "First") {
            self.stackView.arrangedSubviews[2].isHidden = true
        } else {
            self.stackView.arrangedSubviews[1].isHidden = true
        }
    }
}
Lepage answered 13/10, 2016 at 9:19 Comment(1)
I had this same problem with a UIStackView in a UITableViewCell. Because cells get reused, when you scroll quickly through the table, a stackview might have components hidden/unhidden in rapid succession. My solution was to unhide in the UITableViewCell's prepareForReuse method, and then only hide when necessary. This seems to work. Might help someone else out there :)Evans
P
83

The bug is that hiding and showing views in a stack view is cumulative. Weird Apple bug. If you hide a view in a stack view twice, you need to show it twice to get it back. If you show it three times, you need to hide it three times to actually hide it (assuming it was hidden to start).

This is independent of using animation.

So if you do something like this in your code, only hiding a view if it's visible, you'll avoid this problem:

if !myView.isHidden {
    myView.isHidden = true
}
Peppi answered 9/8, 2017 at 20:12 Comment(4)
That's true and it's not documented anywhere. It drove me crazy to debug why my view is not showing in the stackview. Words cannot tell how stupid this implementation is! Thank you so much, Dave!Pluto
I've had problems with this before, but I want to say that this is fixed as of Xcode 9.2.Ashtray
That's a really annoying bug, it's been screwing up my UI until I found this answer.Telefilm
You sir, rock 🤘 This still occurs in iOS 14.2 and the easiest solution for me was to follow Sandy Chapman's suggestion below: https://mcmap.net/q/503288/-swift-disappearing-views-from-a-stackview.Sumption
C
21

Building on the nice answer by Dave Batton, you can also add a UIView extension to make the call site a bit cleaner, IMO.

extension UIView {

    var isHiddenInStackView: Bool {
        get {
            return isHidden
        }
        set {
            if isHidden != newValue {
                isHidden = newValue
            }
        }
    }
}

Then you can call stackView.subviews[someIndex].isHiddenInStackView = false which is helpful if you have multiple views to manage within your stack view versus a bunch of if statements.

Chainey answered 29/3, 2018 at 11:24 Comment(1)
This is great and easy to use. You just need to replace isHidden with isHiddenInStackView for any arranged views.Offcenter
L
5

In the end, after trying all the suggestions here I still couldn't work out why it was behaving like this so I got in touch with Apple who asked me to file a bug report. I did however find a work around, by unhiding both views first, which solved my problem:

func updateView(segment: String) {
    UIView.animate(withDuration: 1) {
        self.stackView.arrangedSubviews[1].isHidden = false
        self.stackView.arrangedSubviews[2].isHidden = false
        if(segment == "First") {
            self.stackView.arrangedSubviews[2].isHidden = true
        } else {
            self.stackView.arrangedSubviews[1].isHidden = true
        }
    }
}
Lepage answered 13/10, 2016 at 9:19 Comment(1)
I had this same problem with a UIStackView in a UITableViewCell. Because cells get reused, when you scroll quickly through the table, a stackview might have components hidden/unhidden in rapid succession. My solution was to unhide in the UITableViewCell's prepareForReuse method, and then only hide when necessary. This seems to work. Might help someone else out there :)Evans
S
2

Based on what I can see, this weird behavior is caused by the animation duration. As you can see, it takes one second for the animation to complete, but if you start switching the segmentControl faster than that, then I would argue that is what is causing this behavior.

What you should do is deactivate the user interactivity when the method is called, and then re-enable it once the animation is complete.

It should look something like this:

func updateView(segment: String) {

    segmentControl.userInteractionEnabled = false
    UIView.animateWithDuration(1.0, animations: {
        if(segment == "First") {
            self.stackView.arrangedSubviews[1].isHidden = false
            self.stackView.arrangedSubviews[2].isHidden = true
        } else {
            self.stackView.arrangedSubviews[1].isHidden = true
            self.stackView.arrangedSubviews[2].isHidden = false

        }
        print("Updating views")
        print("View 1 is \(self.stackView.arrangedSubviews[1].isHidden ? "hidden" : "visible")")
        print("View 2 is \(self.stackView.arrangedSubviews[2].isHidden ? "hidden" : "visible")")
    }, completion: {(finished: Bool) in
        segmentControl.userInteractionEnabled = true
    }
}

While this will prevent from fast switching (which you may see as a downside), the only other way I am aware of that solve this is by removing the animations altogether.

Soil answered 12/10, 2016 at 14:50 Comment(8)
Sorry Benjamin, that still doesn't seem to fix the issue - thanks for trying though!Lepage
@Lepage Huh, that's weird. I'll take a second lookSoil
Thanks, I've just updated above to say I can reproduce it if I go to First > Second > Third > Second > First segments, if that helps!Lepage
@Lepage Is it still reproduced if you slowly click through them in that order (e.g. with more than one second of pause between clicks)?Soil
Yep - doesn't make any difference.Lepage
Do the outputs of your print functions corroborate the bug? (i.e. are they saying that both views are hidden? Or something else?)Soil
Yes - they both report as hidden once the bug has occurred, whenever I click on 'First'Lepage
Then it seems that we are indeed dealing with a thread issue since nowhere in your code do you simultaneously assign the views as hidden. I would recommend debugging it and seeing how the values change in order to find the origin of the problem.Soil
I
0

Check the configuration and autolayout constraints on the stack view and the subviews, particularly the segmented control.

The segmented control complicates the setup for the stack view, so I'd take the segmented control out of the stack view and set its constraints relative to the main view.

With the segmented control out of the stack view, it's relatively straightforward to set up the stack view so that your code will work properly.

Reset the constraints on the stack view so that it is positioned below the segmented control and covers the rest of the superview. In the Attributes Inspector, set Alignment to Fill, Distribution to Fill Equally, and Content Mode to Scale to Fill.

Remove the constraints on the subviews and set their Content Mode to Scale to Fill.

Adjust the indexing on arrangedSubviews in your code and it should work automagically.

Inform answered 12/10, 2016 at 15:18 Comment(1)
No luck - it's still not allowing the first view to be unhidden after going to First > Second > Third > Second > First.Lepage

© 2022 - 2024 — McMap. All rights reserved.