20

I've got two UILabels embedded within a UIStackView. The top label stays visible constantly, but the bottom label is toggled on and off via the hidden property. I wanted this effect to be animated, so I stuck it in an animation block:

private func toggleResultLabel(value:Double) {
    if value == 0 {
        UIView.animateWithDuration(0.25) { () -> Void in
            self.resultLabel.hidden = true
        }
    } else {
        UIView.animateWithDuration(0.25) { () -> Void in
            // Something weird is happening. I had to add 3 of the same statements to get 
            // the hidden flag to be false
            self.resultLabel.hidden = false
            self.resultLabel.hidden = false
            self.resultLabel.hidden = false
        }
    }
}

The problem is that the hidden property will not change unless I repeat the statement over and over (3 times in this case). I found this while breaking into the animation closure and seeing that the property would not change to it's assignment. Now I'm noticing the same problem occurring seemingly randomly again. The default value of the second label is true, if that's relevant.

Is there something I'm missing here, or is this a bug?

Update: For what it's worth, I got it working by adding removeArrangedSubview() and addArrangedSubview():

if value == 0 {
    UIView.animateWithDuration(0.25) { () -> Void in
        self.resultLabel.hidden = true
        self.heroStackView.removeArrangedSubview(self.resultLabel)
    }
 } else {
    UIView.animateWithDuration(0.25) { () -> Void in
        self.heroStackView.addArrangedSubview(self.resultLabel)
        self.resultLabel.hidden = false
    }
 }
Alex
  • 337
  • 2
  • 10
  • 1
    The value not changing is weird, but anyway to animate your label you should change the view's `alpha` value and not `hidden`. AFAIK, `hidden` is not animatable. – Guillaume Algis Oct 20 '15 at 15:32
  • Thanks @GuillaumeAlgis! I could try changing the alpha, but I don't think it will rearrange the StackView since it's only invisible and removed. The hidden property is animatable and works 95% of the time. For reference I used the section at the bottom of the page: [https://developer.apple.com/library/prerelease/tvos/documentation/UIKit/Reference/UIStackView_Class_Reference/index.html](https://developer.apple.com/library/prerelease/tvos/documentation/UIKit/Reference/UIStackView_Class_Reference/index.html) – Alex Oct 20 '15 at 15:42
  • Hmm if I understand the doc properly, this is a specific behavior of `UIStackView`. Outside of this special case `hidden` won't be animated. Are you using a `UIStackView` ? – Guillaume Algis Oct 20 '15 at 16:16
  • @GuillaumeAlgis Yes I am. – Alex Oct 20 '15 at 16:19
  • Oh yes, just saw that in the question, sorry, read too fast :| – Guillaume Algis Oct 20 '15 at 16:21
  • Okay other idea: Are your sure `toggleResultLabel` is not called multiple times? This would result in the `hidden` property changing value more or less randomly... – Guillaume Algis Oct 20 '15 at 16:24
  • @GuillaumeAlgis I'll look into this and see if this is causing the issue. – Alex Oct 20 '15 at 17:54

6 Answers6

44

On iOS 11 and prior, when hiding an arrangedSubview of a UIStackView using UIView animation API multiple times, the hidden property values "stack", and it requires setting hidden to false multiple times before the value actually changes.

At work we decided to use a UIView extension with a workaround method that sets hidden only once for given value.

extension UIView {

    // Workaround for the UIStackView bug where setting hidden to true with animation
    // mulptiple times requires setting hidden to false multiple times to show the view.
    public func workaround_nonRepeatingSetHidden(hidden: Bool) {
        if self.hidden != hidden {
            self.hidden = hidden
        }
    }
}

This is definitely a bug in UIKit, check out the sample project that reproduces it clearly.

enter image description here

Nikita Kukushkin
  • 14,648
  • 4
  • 37
  • 45
8

In considering UIStackView bug I decide to check hidden property.

if myView.hidden != hidden {
   myView.hidden = hidden
}

It's not the most elegant solution but it works for me.

CHiP-love-NY
  • 507
  • 6
  • 14
7

There seems to be correlation on how many times hidden flag is set to same state and how many times it must set to different state before it's actually changed back. In my case, I had hidden flag already set to YES before it was set to YES again in animation block and that caused the problem where I had to call hidden = NO twice in my other animation block to get it visible again. If I added more hidden = YES lines in first animation block for the same view, I had to have more hidden = NO lines in second animation block as well. This might be a bug in UIStackView's KVO observation for the hidden flag that doesn't check if the value is actually changed or not before changing some internal state that leads to this issue.

To temporarily fix the issue (until Apple fixes it), I made a category for UIView and swizzled setHidden: method to a version that first checks the original value and sets the new value only if it differs from the original. This seems to work without any ill effects.

@implementation UIView (MethodSwizzling)

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(setHidden:);
        SEL swizzledSelector = @selector(UIStackViewFix_setHidden:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod =
        class_addMethod(class,
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)UIStackViewFix_setHidden:(BOOL)hidden
{
    if (hidden != self.hidden) {
        [self UIStackViewFix_setHidden:hidden];
    }
}

@end
Raz0
  • 71
  • 2
6

Appears to be an Apple bug with UIStackView. See the following...

UIStackView: toggleing hidden with animations gets stuck in hidden mode http://www.openradar.me/22819594

My solution, although not ideal, was to hide the UIStackView without animation.

kwahn
  • 2,118
  • 2
  • 21
  • 17
2

As per Raz0's answer, who found out that only setting isHidden when necessary solves the issue, here's a swift workaround that made it work for me. I'm avoiding method swizzling because it's inherently unsafe, in favor of a show/hide approach that shouldn't mess up with the original methods:

extension UIView {
    func show() {
        guard isHidden else {
            return
        }
        isHidden = false
    }

    func hide() {
        guard !isHidden else {
            return
        }
        isHidden = true
    }
}

Use it like this:

view.show()
view.hide()
ldiqual
  • 15,015
  • 6
  • 52
  • 90
0

What worked for me is to set the hidden property outside of the animation and then animating layoutIfNeeded(), just like you would with animating constraints:

label.isHidden = true
UIView.animate(withDuration: 3) {
    self.view.layoutIfNeeded()
}

where label is an arranged subview of a UIStackView.

Jack Goossen
  • 799
  • 4
  • 14