Byte
Byte

Reputation: 2940

Multiple line UILabel autolayout get collapsed to 1 liner when pop

Here is the sample project demonstrating the problem

What is it?
Two UIViewControllers in stacks under UINavigationController. Each has nothing to do with each other other than presenting from one to another. In both controllers, there is a UILabel. Each uses Autolayout. Each label holds arbitrary number of lines label.numberOfLines = 0.

What works?
Transitioning from viewController A (root) to viewController B. B gets allocated and initiated. B looks well.

What went wrong?
Transitioning back from B to A. At ViewDidDisappear, label in B decided it will no longer show more than 1 line even though its numberOfLines is set at 0. When B was pushed into the stack, its label only shows 1 liner instead of multiple.

What caused it?
No idea. BUT looking in A the label numberOfLines was set to 0. If the line were to be removed, label in B would not have collapsed.

Questions: But WHY? and I like A to have multiple lines label, how can I overcome this?

Codes
A

@implementation FirstViewController
{
    BugController *_bugController;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    // Bar Button
    UIBarButtonItem *helpBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"BUG" style:UIBarButtonItemStylePlain target:self action:@selector(bugTapped)];
    [self.navigationItem setLeftBarButtonItem:helpBarButtonItem];

    // A sample label
    UILabel *someLabel = [[UILabel alloc] init];
    [someLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
    [someLabel setText:@"Tap BUG to see a bunch of text in many lines... tap back... then tap BUG again to see that the text has gone to 1 liner.... WTF?"];
    [self.view addSubview:someLabel];

    // Comment it out to see that problem is fixed
#warning This one liner is a culprit, removing it will make everything normal but WHY?
    [someLabel setNumberOfLines:0];
#warning end of warning


    //Constraints
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[someLabel]-|" options:0 metrics:0 views:NSDictionaryOfVariableBindings(someLabel)]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[someLabel]-|" options:0 metrics:0 views:NSDictionaryOfVariableBindings(someLabel)]];
}

- (void)bugTapped
{
    // Reuse controller
    if (!_bugController) {
        _bugController = [[BugController alloc] init];
    }
    [self.navigationController pushViewController:_bugController animated:YES];
}

B

@implementation BugController

- (void)viewDidLoad
{
    [super viewDidLoad];

    // Setting up stuff
    UILabel *header = [[UILabel alloc] init];
    [header setTranslatesAutoresizingMaskIntoConstraints:NO];
    [header setText:@"This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. "];
    [header setNumberOfLines:0];
    [self.view addSubview:header];

    // Constraints
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[header]-|" options:0 metrics:0 views:NSDictionaryOfVariableBindings(header)]];
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:header attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]];
}

Do check it out in the sample code link above!


Update 1 with workaround! After working with the code provided by Misha Vyrko, I realized that setting preferredMaxLayoutWidth to non-zero overcomes the bug in the UILabel.

Non broken sample project

Added to the BugViewController

// Constraints
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[header]-|" options:0 metrics:0 views:NSDictionaryOfVariableBindings(header)]];
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:header attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]];
    [header setPreferredMaxLayoutWidth:1]; // NEWLY ADDED

I am still looking for an explanation to why this happens and a better way if there is any.


Update 2 with corrections By setting PreferredMaxLayoutWidth to 1, the label.frame.size.height actually expands to the height expected, if the width were actually 1. This means if you have any constraints dependency on the height of your label, it will not work. You will need to explicitly set it to the estimate width. It will not handle rotation without aids so watch out for that!

Upvotes: 11

Views: 1842

Answers (4)

Misha Vyrko
Misha Vyrko

Reputation: 1000

In bugController you haven't got enough constraints to layout label if you set up all constraints all will be ok

@interface BugController ()

@property (nonatomic, strong) UILabel *header;

@end

@implementation BugController

- (void)viewDidLoad
{
    [super viewDidLoad];

    // Setting up stuff
    self.header = [[UILabel alloc] init];
    [self.header  setTranslatesAutoresizingMaskIntoConstraints:NO];
    [self.header  setText:@"This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. This is a multiple line thing! "];
    [self.header  setBackgroundColor:[UIColor redColor]];
    [self.header  setNumberOfLines:0];
    [self.view addSubview:self.header ];


    // Constraints
    //    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[header]-|"
    //                                                                      options:0
    //                                                                      metrics:0
    //                                                                        views:NSDictionaryOfVariableBindings(header)]];

    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.header
                                                      attribute:NSLayoutAttributeCenterY
                                                      relatedBy:NSLayoutRelationEqual
                                                         toItem:self.view
                                                      attribute:NSLayoutAttributeCenterY
                                                     multiplier:1
                                                       constant:0]];
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.header
                                                      attribute:NSLayoutAttributeCenterX
                                                      relatedBy:NSLayoutRelationEqual
                                                         toItem:self.view
                                                      attribute:NSLayoutAttributeCenterX
                                                     multiplier:1
                                                       constant:0]];

    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.header
                                                      attribute:NSLayoutAttributeHeight
                                                      relatedBy:NSLayoutRelationLessThanOrEqual
                                                         toItem:self.view
                                                      attribute:NSLayoutAttributeHeight
                                                     multiplier:1
                                                       constant:0]];
    self.header.preferredMaxLayoutWidth = self.view.frame.size.width;


    [self.view needsUpdateConstraints];
}

- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
    self.header.preferredMaxLayoutWidth = self.view.frame.size.width;
    [self.view needsUpdateConstraints];
    [super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
}

@end

Upvotes: 4

user2260054
user2260054

Reputation: 522

Here is the link for the working project.

The origin of problem is unclear, but following the principle "if you don't know what to do, do something", I've made it work by storing both labels as properties (this might be unnecessary, probably instance variables would be ok) and implementing following callbacks in both view controllers (remember, you said, that removing a line [someLabel setNumberOfLines:0]; in first controller fixes the second one, so here we go):

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    self.yourLabel.numberOfLines = 1; 
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    self.yourLabel.numberOfLines = 0; 
}

It smells like a bug in UILabel class, but I'm not that cool to be sure. Good luck!

Upvotes: 1

Nick
Nick

Reputation: 2369

The behavior you're experiencing is pretty strange. Here is one way around it:

I'd recommend you stop re-using your BugController. It is a clean solution, and you should not have to cache ViewControllers explicitly. (the OS can handle efficiency on its own, and you should be able to rebuild your view controller at any time based on underlying Model data if you're adhering to MVC standards)

to do this, remove the BugController iVar, and change bugTapped to:

- (void)bugTapped
{
    BugController *bugVC = [[BugController alloc] init];
    [self.navigationController pushViewController:bugVC animated:YES];
}

Upvotes: 0

Satyarth Kumar Prasad
Satyarth Kumar Prasad

Reputation: 26

Let me answer your question.

Why?

I didnt figure it out but going through my code might answer you. My wild guess is, it due to creating a new UILabel in viewDidLoad each time without clearing previous ones.

How can I overcome this?

I tried few things. Following things worked for me

  1. I tried to present _bugController instead of pushing it in navigation controller.

     [self presentViewController:_bugController animated:YES completion:nil]
    
  2. Not reusing the bugController object. Each time create a new object when you want to push it.

  3. Move your code to viewWillAppear and on disappear make all UILabel nil. Here is my code:

FirstViewController

  #import "FirstViewController.h"
  #import "BugController.h"
  @interface FirstViewController ()

  @end

  @implementation FirstViewController
  {
      BugController *_bugController;
  }

  - (void)viewDidLoad
  {
      [super viewDidLoad];
    // Bar Button
    UIBarButtonItem *helpBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"BUG" style:UIBarButtonItemStylePlain target:self action:@selector(bugTapped)];
    [self.navigationItem setLeftBarButtonItem:helpBarButtonItem];
    
  }

  - (void)viewWillAppear:(BOOL)animated {
    // A sample label
    UILabel *someLabel = [[UILabel alloc] init];
    [someLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
    [someLabel setText:@"Tap BUG to see a bunch of text in many lines... tap back... then tap BUG again to see that the text has gone to 1 liner.... WTF?"];
    
    [self.view addSubview:someLabel];
    
    // Comment it out to see that problem is fixed
  #warning This one liner is a culprit, removing it will make everything normal but WHY?
    [someLabel setNumberOfLines:0];
  #warning end of warning
    
    
    //Constraints
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[someLabel]-|" options:0 metrics:0 views:NSDictionaryOfVariableBindings(someLabel)]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[someLabel]-|" options:0 metrics:0 views:NSDictionaryOfVariableBindings(someLabel)]];
  }


  - (void)viewWillDisappear:(BOOL)animated {
    
    for (UIView *subView in self.view.subviews) {
      if ([subView isKindOfClass:[UILabel class]]) {
        UILabel *label = (UILabel *)subView;
        label.text = @"";
        label = nil;
      }
    }
    [super viewWillDisappear:animated];
  }

  - (void)bugTapped
  {
      // Reuse controller
      if (!_bugController) {
          _bugController = [[BugController alloc] init];
      }
      [self.navigationController pushViewController:_bugController animated:YES];
  }

  @end

BugController

//

  #import "BugController.h"

  @interface BugController ()

  @end

  @implementation BugController

  - (void)viewDidLoad
  {
      [super viewDidLoad];
  }

  - (void)viewWillAppear:(BOOL)animated {
    
    UILabel *header = [[UILabel alloc] init];
    [header setTranslatesAutoresizingMaskIntoConstraints:NO];
    [header setText:@"This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. This is a multiple line thing. "];
    [header setNumberOfLines:0];
    header.tag = 111;
    [self.view addSubview:header];
    
    // Constraints
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[header]-|" options:0 metrics:0 views:NSDictionaryOfVariableBindings(header)]];
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:header attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]];
  }

  - (void)viewWillDisappear:(BOOL)animated {
    for (UIView *subView in self.view.subviews) {
      if ([subView isKindOfClass:[UILabel class]]) {
        UILabel *label = (UILabel *)subView;
        label.text = @"";
        label = nil;
      }
    }
    [super viewWillDisappear:animated];
  }


  - (void)dismiss {
    [self dismissViewControllerAnimated:YES completion:nil];
  }


  @end

I hope this helps you!

Upvotes: 0

Related Questions