Hidden property cannot be changed within an animation block
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.
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
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.