I have a UIView subclass that is added as an arranged subview of a UIStackView. Depending on the data in the model, I want to either hide or show the arranged subview (called myView
), but the problem is that when I go to hide it, even if I set myView.hidden = NO
, it still shows that myView.hidden = YES
.
For example, the following is the code that I have. It starts out with the view being hidden and depending on whether or not myModel.someProperty
is set, it will show myView. Or that is what is supposed to happen.
I have set a breakpoint and stepped through this code and used LLDB to verify that self.myView.hidden == YES
before line 4 is executed. I then checked the value right after stepping over line 4 and it was still YES. But line 4 explicitly sets it to NO and nothing in the implementation of myView overrides or even sets or checks the hidden property of itself. So setting hidden on this just goes to the standard UIView setHidden:
method. So how could it still be YES?
1. //currently, self.myView.hidden is YES
2.
3. if (self->_myModel.someProperty) {
4. self.myView.hidden = NO;
5.
6. //for some reason, self.myView.hidden is still YES
7.
8. while (self.myView.isHidden) {
9. NSLog(@"myView is hidden, but it should not be");
10. self.myView.hidden = NO;
11. }
12. NSLog(@"myView is no longer hidden");
13. }
I added a loop on line 8 that will cause the view to be hidden again. It works this time. So if I set myView.hidden = NO
two times, then it actually will get set to NO. But if I only set it one time, then it stays at YES. I do not understand what is going on.
Does anyone know what might be wrong here or how to troubleshoot this further? I have used LLDB's po
command to view the value of myView.isHidden
before and after each set of the property. So before line 4, it was set to YES, which is correct. Then, after line 4, I checked it and it was still set to YES, even though it was explicitly set to NO on the previous line. Then, I checked and it entered the loop on line 8 (even though it should not have if it would have been non-hidden like it should have been). And then I checked again before line 10 and myView.hidden
was still YES and I checked after line 10 and it was finally correctly set to NO.
But I am just not sure what is going on. This is very counterintuitive as I am explicitly setting it to NO, but it is not getting set until I set it twice to NO.
Is there a good way to troubleshoot this or to figure out what is wrong or does anyone have any suggestions on what might be the problem?
Update
I have updated the code to add some extra log statements. I have also used p self.myView.hidden
when checking that property in LLDB.
1. // at this point, self.myView.hidden = YES
2.
3. if (self->_myModel.someProperty) {
4. NSLog(@"Before setting hidden=NO: %@", self->_myView);
5. self.myView.hidden = NO;
6. NSLog(@"After setting hidden=NO: %@", self->_myView);
7.
8. while ([self.myView isHidden]) {
9. NSLog(@"SHOULD NOT BE HERE - Before setting hidden=NO again: %@", self->_myView);
10. self.myView.hidden = NO;
11. NSLog(@"SHOULD NOT BE HERE - After setting hidden=NO again: %@", self->_myView);
12. }
13.
14. NSLog(@"Finally, no longer hidden: %@", self->_myView);
15. }
Here are the log statements from this code. The first log statement is correct, as it shows myView.hidden == YES. The second log statement, however, seems wrong to me because it is still showing myView.hidden == YES even though on the previous line it was just set to NO.
Before setting hidden=NO: <MyView: 0x117ef6eb0; frame = (0 49.6667; 123.667 20.3333); hidden = YES; layer = <CALayer: 0x280ddaa20>>
After setting hidden=NO: <MyView: 0x117ef6eb0; frame = (0 49.6667; 123.667 20.3333); hidden = YES; layer = <CALayer: 0x280ddaa20>>
The next set of log statements are inside the loop, which it should not even enter anyway since I am setting myView.hidden to NO, but it goes in anyway because the value is still YES. And here it looks like it works correctly. The first log statement shows it is visible and then the next log statement shows it is hidden.
SHOULD NOT BE HERE - Before setting hidden=NO again: <MyView: 0x117ef6eb0; frame = (0 49.6667; 123.667 20.3333); hidden = YES; layer = <CALayer: 0x280ddaa20>>
SHOULD NOT BE HERE - After setting hidden=NO again: <MyView: 0x117ef6eb0; frame = (0 49.6667; 123.667 20.3333); layer = <CALayer: 0x280ddaa20>>
Finally, no longer hidden: <MyView: 0x117ef6eb0; frame = (0 49.6667; 123.667 20.3333); layer = <CALayer: 0x280ddaa20>>
Update 2
I know this code seems to be working on its own, but it is not working for me in my project. I will show the code for my view class here and also the output from a debug session showing the same behavior observed in the code.
And I know it might be in my code, but at the same time, I just do not see how. All my code consists of here is a call to setHidden:
. Nothing extra. Before calling setHidden, the value of hidden is YES. After calling setHidden:NO
, the value is still YES. I do not understand that. I am wondering if this is maybe a compiler issue. I know these compilers are very well tested, but at the same time I also do not understand how it is my code. I am simply setting hidden = NO, but it is not working unless I do it twice.
Debug Session
Here is the output from LLDB. I set a breakpoint right before the view was about to be unhidden (line 3 in the previous code snippets). At this point, myView.hidden = YES
.
So all I did was to print the value of hidden for that view, and it correctly showed YES. After this, I ran call self.myView.hidden = NO
to try to update it, but that doesn't work as can be seen in the debug statement that is printed out right below the call statement. It still shows hidden = YES;
. I also went ahead and printed the value again just to be sure, and it still shows hidden = YES.
(lldb) p self.myView.hidden
(BOOL) $12 = YES
(lldb) call self.myView.hidden = NO
<MyView: 0x12b138980; frame = (0 49.6667; 123.667 20.3333); hidden = YES; layer = <CALayer: 0x283addfe0>> MyView::setHidden:NO
(BOOL) $13 = NO
(lldb) p self.myView.hidden
(BOOL) $15 = YES
Next, I just set the value to NO again and this time it works as can be seen by the debug statement and I also printed the value again for good measure.
(lldb) call self.myView.hidden = NO
<MyView: 0x12b138980; frame = (0 49.6667; 123.667 20.3333); layer = <CALayer: 0x283addfe0>> MyView::setHidden:NO
(BOOL) $16 = NO
(lldb) p self.myView.hidden
(BOOL) $17 = NO
Here is the code for my view class that gets shown and hidden. I am not overriding or doing anything with the hidden property, so any call to setHidden:
goes straight to the method on UIView.
MyView.h
#import <UIKit/UIKit.h>
#import "MyModel.h"
@interface MyView : UIView
@property (strong, nonatomic, nullable) MyModel *myModel;
@end
MyView.m
#import "MyView.h"
@interface MyView ()
@property (strong, nonatomic) UILabel *label;
//other UI components are here, but they are just more labels and an image view
@end
@implementation MyView
- (instancetype)init {
return [self initWithFrame:CGRectZero];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
[self initialize];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self initialize];
}
return self;
}
- (void)initialize {
[self addSubview:self.label];
//add other labels and the image view
[NSLayoutConstraint activateConstraints:@[
[self.label.leadingAnchor constraintGreaterThanOrEqualToAnchor:self.leadingAnchor],
[self.label.topAnchor constraintGreaterThanOrEqualToAnchor:self.topAnchor],
[self.label.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
//more constraints for the other labels and the image
]];
}
- (void)setMyModel:(MyModel *)myModel {
self->_myModel = myModel;
[self updateDisplay];
}
- (void)updateDisplay {
//set the text of all the labels based on the model
}
- (UILabel *)label {
if (!self->_label) {
self->_label = [[UILabel alloc] init];
self->_label.translatesAutoresizingMaskIntoConstraints = NO;
self->_label.numberOfLines = 0;
self->_label.text = @"My Text:";
[self->_label setContentHuggingPriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal];
[self->_label setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
}
return self->_label;
}
@end
Please let me know if there is anything else that I should post that would help or if there is anything I could try. I can just write the value twice in my code, but without understanding why I have to do it, I feel that is sort of dangerous because how do I know that two times will always be sufficient? Plus, it is just weird to have to set a variable to the same value twice in a row for it to work.
Thank you to everyone for your help with this.
CodePudding user response:
Yes, there is a bug / quirk when animating the showing / hiding of arranged subviews in a UIStackView
.
You should be able to correct the issue by adding this to your custom view class:
- (void)setHidden:(BOOL)hidden {
if (self.isHidden != hidden) {
[super setHidden:hidden];
}
}
Here is a complete example that shows the problem, and shows the "fix": https://github.com/DonMag/StackViewBug
CodePudding user response:
It looks like this is due to a bug in the UIStackView where if you hide a view more than once, it accumulates the hidden count for that view. So, for example, imagine a view hierarchy like this:
- UIStackView
- MyView
Then, if you set MyView
hidden=YES three times, it will take three times of setting hidden=NO to allow it to actually be set to NO. This does not appear to be an issue going the other way. So if you set it hidden=NO three times, you can set it to hidden=YES just once and it will be hidden.
There is more information in this StackOverflow answer: https://stackoverflow.com/a/45599835/5140550
I am not sure if this bug has been reported to Apple or not, but it appears to be a bug in UIStackView. Now I just need to figure out a clean way to handle this issue in my code.