2

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.

ashipma
  • 423
  • 5
  • 15
  • 2
    That is certainly weird. However, you're doing one thing wrong: don't use `po` in that way. Just use `p`. So `p self.myView.hidden`. Also don't waggle back and forth between `self.myView.hidden` and `self.myView.isHidden`. You're misusing `isHidden`; it is not really the name of a property, it is the getter method. So say `[self.myView isHidden]` or else `self.myView.hidden`. I'm not saying any of that solves the issue, I'm just suggesting you behave a little more correctly as a matter of good practice. – matt Oct 14 '21 at 23:25
  • 1
    There's definitely something else going on here. I took your code and added it to a blank Obj-C project (https://pastebin.com/zgZCHRxz) hooked up to an IBOutlet UIStackView, and changing the bool to YES/NO works as expected, respectively. – brandonscript Oct 15 '21 at 00:06
  • Yeah, I agree about that. We can't reproduce the issue, so _something_ else in your code is sneaking in and messing stuff up somehow. – matt Oct 15 '21 at 00:16
  • @matt Thanks for your explanations. I have updated the code so that I only use `myView.hidden` when I am setting the property and when I am reading it, I use `[myView isHidden]`. I also have updated my LLDB statements to be `p self.myView.hidden`. I am just not sure what could be wrong anymore. I am explicitly setting the property to be `NO`, but it's still `YES` unless I set it twice. I posted an update to the question with some new log statements and I just ran this and it shows a bit more detail about the objects involved. But it is still weird. I am just setting a property. Thanks again. – ashipma Oct 15 '21 at 00:32
  • @brandonscript Thank you for your help. Yeah, I did not think it would be easily reproducible. If it helps, I posted an update at the bottom of the question that has some additional logging. But essentially I set the hidden property to NO and then I have to set it again in order for the changes to take effect. I am not sure why though. Again, I am not overriding any setters or getters and I am not setting the hidden property anywhere else. I am just not sure what could be wrong or how to even go about debugging this. Thank you again for your help. – ashipma Oct 15 '21 at 00:36
  • What I do in these situations is abstract the code into a tiny project that does the same thing, to prove to myself that it should work correctly (like what brandonscript did). Then I think about what's different in the real project. – matt Oct 15 '21 at 01:12
  • @ashipma Are you running your code on main thread? – Kamil.S Oct 15 '21 at 06:12
  • @ashipma - tough to offer help when you post code that ***works***, but you haven't posted code that ***doesn't work***. If I add a view and set `.hidden = YES;` in `viewDidLoad`, then your "Updated" code in `viewDidAppear`, neither of the `"SHOULD NOT BE HERE..."` lines are logged. – DonMag Oct 15 '21 at 13:38
  • @Kamil.S Thanks for your comments. Yes, the code is on the main thread. – ashipma Oct 15 '21 at 14:03
  • @DonMag Thank you for your help. I know it works here, but I promise that when I run the same exact code (the only difference is the name of the view is not MyView), then it does not work. I will post an update showing that I ran some statements in the debugger, the exact same statements as my code, and it shows the behavior I am seeing. But my UIView class is just a standard view subclass. I am not overriding `setHidden:` or doing anything with the hidden property. When I try to step into the setter, I am unable to because it is the standard framework setter. I will post an update soon. – ashipma Oct 15 '21 at 14:07
  • @ashipma - if you're going to post more code, post code that ***reproduces the problem***. And, don't post line numbers -- pain in the neck to copy/paste your code and then have to delete all the line numbers. – DonMag Oct 15 '21 at 14:19
  • @DonMag (1 of 2) Sorry about the line numbers. I do not know how to post code that reproduces the problem. At face value, it is a very simple thing that I am trying to do: just update the hidden property from YES to NO. And so I would imagine in general, that would work fine and it normally does for me too (I can normally show or hide views). But for some unknown reason, it is not working in this case. But I am not doing anything special. I am just trying to update that property. I did post an update to the question titled "Update 2". – ashipma Oct 15 '21 at 14:49
  • @DonMag (2 of 2) I hope the update is helpful. It shows a debug session where I check the view.hidden value, see that it is YES and then try to update it to NO. It takes two writes in order for it to actually update. I know I am not posting code that can reproduce the issue but I do not think I will be able to. This is a really weird situation and under normal circumstances you should be able to update the hidden value with just one write. But not here for some reason. Do you have any suggestions on things to try or anything that I should post? Thank you. – ashipma Oct 15 '21 at 14:53
  • @ashipma - your "Update 2" has nothing to do with your issue. The **first** problem is that you posted nonsensical code... you say you have a line: `self.myView.hidden = NO;` followed by `while ([self.myView isHidden])` ... What in the world do you expect to happen there? You have to show the code that is actually running. Nobody can explain what's wrong with your "other" code. Strip things down to a [mre]. – DonMag Oct 15 '21 at 15:00
  • @DonMag (1 of 2) I am sorry, but that is exactly the code I have right now, the only difference is the name of the class. Other than that, it is identical. The while loop was added to keep setting hidden=NO until it actually succeeded. I realize this should not be necessary, but for some reason it is and that is what I am trying to understand. When I set hidden=NO, it still stays as YES until the second write. And I am not sure how to make a minimal reproducible example from this. I am sure that if I take this code and extract it into some smaller example, it will work. – ashipma Oct 15 '21 at 15:13
  • @DonMag (2 of 2) But I agree that the code I posted looks like nonsense. I am setting myView.hidden=NO and then I have a loop condition that should succeed only when hidden=YES, but I just set it to NO, so it should never get in the loop. But it does because hidden is still YES for some really weird reason. And that is what I showed in update 2. I showed manually setting hidden=NO and it requiring two writes to succeed. But again, this is a really weird situation and I do not expect any of the code I post here to reproduce it in a new project, even though it is the same code I am running now. – ashipma Oct 15 '21 at 15:17
  • @ashipma - *"it is the same code I am running now"* ... well, no, it's not. That code can't run by itself. What method is it in? Is it called in response to user interaction? Is it being called by a timer? Is it on some thread you've created? *"I am sure that if I take this code and extract it into some smaller example, it will work."* ... well, again, no. If you start stripping down your code, it will continue to be "problematic" until it's not. At that point, whatever the last code you stripped out is where you look for the issue. – DonMag Oct 15 '21 at 16:42
  • @DonMag (1 of 3) Again, I am sorry, but the code base for this app is well over 100K lines. Just the part that I have written is around 25K lines and it is over 120 classes. To answer your question, this view does get updated as part of a user interaction. The user selects an item from a collection view. That has a delegate which informs its containing view controller that a selection was made. That view controller then informs its parent view controller that a selection was made. – ashipma Oct 15 '21 at 17:20
  • @DonMag (2 of 3) That view controller then calls to a cache to get data or it will fetch data from a web service if the data is not present in the cache. Next, that containing view controller (which has a scroll view that has a stack view that has several chid view controllers, one of them is the view controller that has this problematic `MyView`. Then, that view controller takes the model data and decides to show or hide that view. And this happens on the main thread as it is updating UI state. – ashipma Oct 15 '21 at 17:21
  • @DonMag (3 of 3) The only thing that happens on a background thread is the web service call, but that has completed by the time this view is populated. In terms of stripping this down, I am not sure how I would go about that exactly. There are a lot of dependencies on other views and view controllers. I am really sorry for the trouble, but I am just not sure how to go about trying to make this minimal or reproducible. It is so counterintuitive with its current behavior where I assign a value for hidden but it is not getting updated until it is written twice. Anyway, thank you for your time. – ashipma Oct 15 '21 at 17:21
  • @DonMag I have tested it more and found out that for some reason, the more times I set `myView.hidden=YES`, then that means I have to set `myView.hidden=NO` that many times to actually get it to be set. So if I set it to YES once, then it only takes one write to set it to NO. But if I set it to YES three times, then the first two NO writes are essentially ignored. Only the third NO write will cause the property to be NO. I just wanted to say this as I thought it was an interesting (and weird) finding. – ashipma Oct 15 '21 at 19:09

2 Answers2

3

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

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • This is still an issue in iOS 16/Xcode 14.3.1. And the solution also still works just fine. This works even for subclass of UIView. – Pankaj Kulkarni Jul 26 '23 at 11:32
0

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.

ashipma
  • 423
  • 5
  • 15
  • Are you are also animating it? – DonMag Oct 15 '21 at 20:21
  • @DonMag Yes, it does get animated. I have the setter for this where it can either load the new data animated or not. When the page first loads this section, it is not animated, but when the collection view item is selected and the page reloads, it does so in an animated fashion. But I think the issue was just when setting the hidden property of a view that is in a stack view, the hidden=YES writes will accumulate. So if you set hidden=YES seven times, then you need to set it to NO that many times in order to overwrite it. This has been reported here: http://www.openradar.me/25087688 – ashipma Oct 15 '21 at 20:31
  • 1
    OK - that would have been very helpful information to provide. And, pretty simple to create a reproducible example... – DonMag Oct 15 '21 at 20:46