问题
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
}
}
回答1:
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.
回答2:
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
回答3:
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.
回答4:
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.
回答5:
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()
回答6:
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.
来源:https://stackoverflow.com/questions/33240635/hidden-property-cannot-be-changed-within-an-animation-block