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.

Ben
  • 4,707
  • 5
  • 34
  • 55
  • 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. – zsteed Oct 12 '16 at 15:08
  • 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? – Felix Oct 12 '16 at 15:59
  • @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?). – Ben Oct 12 '16 at 16:08
  • @zsteed 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. – Ben Oct 12 '16 at 16:15
  • i think so, StackView handle automatically when you show and hide view. – Parth Patel Dec 27 '18 at 11:03

5 Answers5

81

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
}
Zaporozhchenko Oleksandr
  • 4,660
  • 3
  • 26
  • 48
Dave Batton
  • 8,795
  • 1
  • 46
  • 50
  • 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! – codingFriend1 Oct 19 '17 at 09:46
  • I've had problems with this before, but I want to say that this is fixed as of Xcode 9.2. – Ruiz Mar 26 '18 at 15:47
  • That's a really annoying bug, it's been screwing up my UI until I found this answer. – Au Ris Jan 17 '19 at 23:38
  • 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://stackoverflow.com/a/49554868/2328732. – m_katsifarakis Feb 17 '21 at 10:51
20

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.

Sandy Chapman
  • 11,133
  • 3
  • 58
  • 67
  • This is great and easy to use. You just need to replace `isHidden` with `isHiddenInStackView` for any arranged views. – Jason Moore Oct 10 '19 at 15:55
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
        }
    }
}
Ben
  • 4,707
  • 5
  • 34
  • 55
  • 5
    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 :) – Stijn Jul 17 '17 at 09:53
1

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.

Benjamin Lowry
  • 3,730
  • 1
  • 23
  • 27
  • Sorry Benjamin, that still doesn't seem to fix the issue - thanks for trying though! – Ben Oct 12 '16 at 14:54
  • @Ben Huh, that's weird. I'll take a second look – Benjamin Lowry Oct 12 '16 at 14:55
  • 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! – Ben Oct 12 '16 at 14:58
  • @Ben Is it still reproduced if you slowly click through them in that order (e.g. with more than one second of pause between clicks)? – Benjamin Lowry Oct 12 '16 at 15:01
  • Yep - doesn't make any difference. – Ben Oct 12 '16 at 15:08
  • Do the outputs of your print functions corroborate the bug? (i.e. are they saying that both views are hidden? Or something else?) – Benjamin Lowry Oct 12 '16 at 15:11
  • Yes - they both report as hidden once the bug has occurred, whenever I click on 'First' – Ben Oct 12 '16 at 15:29
  • 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. – Benjamin Lowry Oct 12 '16 at 15:32
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.

H. M. Madrone
  • 31
  • 1
  • 3
  • No luck - it's still not allowing the first view to be unhidden after going to First > Second > Third > Second > First. – Ben Oct 12 '16 at 15:44