Search code examples
iosobjective-cuiviewautolayoutvisual-format-language

Programmatically position UILabels added dynamically inside UIView with autolayout


I have an array of items to be display inside of my UITableViewCell. Each of these items will be dynamically displayed using UILabel. I use autolayout to setup the views layout.

Here's how I layout my tableViewCell:

+------------------------------------------------+
| [cellView]                                     |
| +---------------------------------------------+|
| |[otherView] <- fixed height                  ||
| | +------------------------------------------+||
| | |[UILabel]                                 |||
| | +------------------------------------------+||
| +---------------------------------------------+|
| +---------------------------------------------+|
| |[itemView]                                   ||
| | +---------------------------+ +------------+||
| | |[itemLabel]                |-|[priceLabel]|||
| | +---------------------------+ +------------+||
| | +---------------------------+ +------------+||
| | |[itemLabel]                |-|[priceLabel]|||
| | +---------------------------+ +------------+||                                            
| |                                             ||
| |     --Add the next UILabels dynamically--   ||
| +---------------------------------------------+|
+------------------------------------------------+   

For clarity

  • otherView: have 2 UILabel inside of it. Fixed height.
  • itemView: have unknown number of itemLabel & priceLabel inside. Will resize height based on the number of items.
  • Each of the views constraints is set using storyboard except for itemLabel & priceLabel.

Inside my cellForRowAtIndexPath, I set the itemLabel & priceLabel constraints using constraintsWithVisualFormat:

for (NSDictionary *item in items) {
    UILabel *itemLabel = [[UILabel alloc] init];
    itemLabel.translatesAutoresizingMaskIntoConstraints = NO;
    itemLabel.font = [UIFont systemFontOfSize:14.0f];
    itemLabel.backgroundColor = [UIColor yellowColor];
    itemLabel.text = [item valueForKey:@"item_name"];
    [cell.itemView addSubview:itemLabel];
    
    UILabel *priceLabel = [[UILabel alloc] init];
    priceLabel.translatesAutoresizingMaskIntoConstraints = NO;
    priceLabel.font = [UIFont systemFontOfSize:14.0f];
    priceLabel.textAlignment = NSTextAlignmentRight;
    priceLabel.backgroundColor = [UIColor greenColor];
    priceLabel.text = [NSString stringWithFormat:@"RM %@", [item valueForKey:@"price"]];
    [cell.itemView addSubview:priceLabel];
    
    NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(cell.itemView, itemLabel, priceLabel);
    [cell.itemView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[itemLabel]-5-[priceLabel(70)]|"
                                                                         options:0
                                                                         metrics:nil
                                                                           views:viewsDictionary]];
    [cell.itemView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[itemLabel]|"
                                                                          options:0
                                                                          metrics:nil
                                                                            views:viewsDictionary]];
    [cell.itemView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[priceLabel]|"
                                                                          options:0
                                                                          metrics:nil
                                                                            views:viewsDictionary]];


}

The end result is as below. The only problem is that the itemLabel and priceLabel created are overlapping with each others instead of aligning nicely from top to bottom.

Results:

End results

View debug:

View debug

How do I set the constraints properly so that the itemLabel & priceLabel will align nicely from top to bottom and the itemView will resize based on the number of items inside it?


Solution

  • You have insufficient constraints. The problem is that you are not setting constraints between, let's say, one itemLabel and the previous itemLabel. As you loop, you need to keep track of the previous item label so you can space this one down from it.

    To put it another way: the technique you are looking for is that there are three cases:

    • The first label. It is pinned to whatever is above it (possibly the top of the superview).

    • All other item labels except the last. Each is pinned to the previous label.

    • The last label. It is pinned to the previous label and to the bottom, thus giving the cell its height.

    By an incredible coincidence, I actually have on hand some code that illustrates this principle; the situation is obviously not 100% identical to yours, but the principle is exactly the sort of thing you want to be following here:

    UILabel* previousLab = nil;
    for (int i=0; i<30; i++) {
        UILabel* lab = [UILabel new];
        // lab.backgroundColor = [UIColor redColor];
        lab.translatesAutoresizingMaskIntoConstraints = NO;
        lab.text = [NSString stringWithFormat:@"This is label %d", i+1];
        [sv addSubview:lab];
        [sv addConstraints:
         [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(10)-[lab]"
                                                 options:0 metrics:nil
                                                   views:@{@"lab":lab}]];
        if (!previousLab) { // first one, pin to top
            [sv addConstraints:
             [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(10)-[lab]"
                                                     options:0 metrics:nil
                                                       views:@{@"lab":lab}]];
        } else { // all others, pin to previous
            [sv addConstraints:
             [NSLayoutConstraint
              constraintsWithVisualFormat:@"V:[prev]-(10)-[lab]"
              options:0 metrics:nil
              views:@{@"lab":lab, @"prev":previousLab}]];
        }
        previousLab = lab;
    }
    
    // last one, pin to bottom, this dictates content size height
    [sv addConstraints:
     [NSLayoutConstraint constraintsWithVisualFormat:@"V:[lab]-(10)-|"
                                             options:0 metrics:nil
                                               views:@{@"lab":previousLab}]];