Extending UITableViewCell with UIStackView in Objective-C

Viewed 59

I have my own cross platform widget layout system that I use for most things so I have never had to use layout constraints before, so this is my first dive into it. My code is in Objective-C not Swift and all of the examples I find are sadly in Swift. By default my UITableView will only show an Icon and Text. However it has extra "column" icons and text that would be in individual columns on MacOS or other desktop platforms. I have a toggle that will allow this additional information to be displayed in an either horizontal or vertical UIStackView. So if toggled I want to add the UIStackView to the UITableViewCell placed under the textLabel expanding the UITableViewCell by whatever size the UIStackView requires. My thought was I could do this by adding 3 constraints connecting the UIStackView, contentView and textLabel together. This didn't produce the expected result. The first two constraints correctly position the UIStackView below the textLabel but did not expand the UITableViewCell. Adding the third (bottom) constraint to expand the cell, instead clips the UIStackView out of the cell entirely. Can someone point out what I am doing wrong?

NSLayoutConstraint *constraint;

stack = [[[UIStackView alloc] init] retain];
[stack setTranslatesAutoresizingMaskIntoConstraints:NO];
[stack setSpacing:5.0];
[[self contentView] addSubview:stack];

/* Leading */
constraint = [NSLayoutConstraint constraintWithItem:stack attribute:NSLayoutAttributeLeft
                                          relatedBy:NSLayoutRelationEqual toItem:[self contentView]
                                          attribute:NSLayoutAttributeLeft multiplier: 1.0 constant:0.0];
[[self contentView] addConstraint:constraint];

/* Top */
constraint = [NSLayoutConstraint constraintWithItem:stack attribute:NSLayoutAttributeTop
                                          relatedBy:NSLayoutRelationEqual toItem:[self textLabel]
                                          attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0];
[[self contentView] addConstraint:constraint];

/* Bottom */
constraint = [NSLayoutConstraint constraintWithItem:[self contentView] attribute:NSLayoutAttributeBottom
                                          relatedBy:NSLayoutRelationEqual toItem:stack
                                          attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0];
[[self contentView] addConstraint:constraint];
1 Answers

You can save yourself a lot of typing, as well as making code much more readable, by using "modern" constraint syntax.

For example, if we've created a label and stack view, and added them to the cell's content view:

    // let's use the default margins
    UILayoutGuide *g = self.contentView.layoutMarginsGuide;

    [NSLayoutConstraint activateConstraints:@[

        // constrain label Top/Leading/Trailing to content margins guide
        [label.topAnchor constraintEqualToAnchor:g.topAnchor constant:0.0],
        [label.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:0.0],
        [label.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:0.0],
        //  no Height, because we'll use the label's intrinsic size

        // constrain stack view Top to label bottom plus 8-points
        [stack.topAnchor constraintEqualToAnchor:label.bottomAnchor constant:8.0],
        // Leading/Trailing/Bottom to content margins guide
        [stack.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:0.0],
        [stack.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:0.0],
        [stack.bottomAnchor constraintEqualToAnchor:g.bottomAnchor constant:0.0],
        //  no Height, because we'll use the arranged subviews heights

    ]];

So, we can write a cell class like this:

StackTableViewCell.h

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN
@interface StackTableViewCell : UITableViewCell
@end
NS_ASSUME_NONNULL_END

StackTableViewCell.m

#import "StackTableViewCell.h"

@interface StackTableViewCell()
{
    UIStackView *stack;
    UILabel *label;
}
@end

@implementation StackTableViewCell

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        [self commonInit];
    }
    return self;
}
- (instancetype)init
{
    self = [super init];
    if (self) {
        [self commonInit];
    }
    return self;
}
- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self commonInit];
    }
    return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
    self = [super initWithCoder:coder];
    if (self) {
        [self commonInit];
    }
    return self;
}

- (void)commonInit {

    // create a label
    label = [UILabel new];
    [label setBackgroundColor:UIColor.greenColor];
    [label setNumberOfLines:0];

    // create a vertical stack view
    stack = [UIStackView new];
    [stack setAxis:UILayoutConstraintAxisVertical];
    [stack setSpacing:5.0];

    // add label and stack view to content view
    [label setTranslatesAutoresizingMaskIntoConstraints:NO];
    [stack setTranslatesAutoresizingMaskIntoConstraints:NO];
    [[self contentView] addSubview:label];
    [[self contentView] addSubview:stack];

    // let's use the default margins
    UILayoutGuide *g = self.contentView.layoutMarginsGuide;

    [NSLayoutConstraint activateConstraints:@[

        // constrain label Top/Leading/Trailing to content margins guide
        [label.topAnchor constraintEqualToAnchor:g.topAnchor constant:0.0],
        [label.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:0.0],
        [label.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:0.0],
        //  no Height, because we'll use the label's intrinsic size

        // constrain stack view Top to label bottom plus 8-points
        [stack.topAnchor constraintEqualToAnchor:label.bottomAnchor constant:8.0],
        // Leading/Trailing/Bottom to content margins guide
        [stack.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:0.0],
        [stack.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:0.0],
        [stack.bottomAnchor constraintEqualToAnchor:g.bottomAnchor constant:0.0],
        //  no Height, because we'll use the arranged subviews heights

    ]];

    // let's give the stack view a border so we can see its frame
    stack.layer.borderColor = UIColor.redColor.CGColor;
    stack.layer.borderWidth = 1.0;

}

@end

along with a basic controller...

StackTableViewController.h

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN
@interface StackTableViewController : UITableViewController
@end
NS_ASSUME_NONNULL_END

StackTableViewController.m

#import "StackTableViewController.h"
#import "StackTableViewCell.h"

@interface StackTableViewController ()

@end

@implementation StackTableViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self.tableView registerClass:StackTableViewCell.class forCellReuseIdentifier:@"c"];
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 30;
}


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    StackTableViewCell *cell = (StackTableViewCell *)[tableView dequeueReusableCellWithIdentifier:@"c" forIndexPath:indexPath];

    // Configure the cell...
    return cell;
}

@end

At this point, because we're not giving the cells any data - and labels and stack view have no intrinsic height when empty - it will look like this:

enter image description here

So, let's add a fillData method:

StackTableViewCell.h

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN
@interface StackTableViewCell : UITableViewCell
// add this line
- (void)fillData:(NSInteger)n;
@end
NS_ASSUME_NONNULL_END

StackTableViewCell.m

// add this method
- (void)fillData:(NSInteger)n {

    label.text = [NSString stringWithFormat:@"Cell %ld", (long)n];
    
    // cells are reused, so first clear any existing labels in the stack view
    //  this is inefficient, but we're just demonstrating the cell sizing
    for (UIView *v in stack.arrangedSubviews) {
        [v removeFromSuperview];
    }
    
    // now add n number of labels
    for (int i = 0; i < n; i++) {
        UILabel *v = [UILabel new];
        v.text = [NSString stringWithFormat:@"Stack Label %ld", (long)i];
        v.backgroundColor = UIColor.yellowColor;
        [stack addArrangedSubview:v];
    }
    
}

and modify the controller to set some cell data:

StackTableViewController.m

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    StackTableViewCell *cell = (StackTableViewCell *)[tableView dequeueReusableCellWithIdentifier:@"c" forIndexPath:indexPath];

    // Configure the cell...
    [cell fillData:(indexPath.row % 4) + 1];
    
    return cell;
}

and now the output looks like this:

enter image description here

Related