Dynamic UITableViewCell Producing Undesired Results

104 Views Asked by At

I'm attempting to create a dynamically sized table view cell. I've read every SO question, website, article, and example Github project and can't get the layout I want without errors (in its current form, there are no errors, but the end result is as depicted in the last image).

I have a table with multiple sections. The first section has a single cell that is dynamically sized. My goal is to display this cell correctly and without errors. Here are the two different visual states the cell may have:

Here is the desired look of the cell with a Message at the bottom: Cell w/ dynamically sized message

Here is desired look of the cell without the message at all:

Cell w/o message at all

For the code shown below, here is the result:

enter image description here

Here is the TableViewController:

//
// The TableViewController
//

#import <Masonry.h>
#import "CustomCell.h"
#import "MyViewController.h"

@interface MyViewController()

@property (retain, nonatomic) CheckoutHeaderView *headerView;
@property (retain, nonatomic) CustomCell *customCell;

@end


@implementation MyViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.tableView = [[UITableView alloc] init];
    self.tableView.delegate = self;
    self.tableView.dataSource = self;
    self.tableView.allowsSelection = NO; 
    [self.tableView registerClass:[CustomCell class] forCellReuseIdentifier:@"customCell"];

    [self.view addSubview:self.headerView];
    [self.view addSubview:self.tableView];

    [self.headerView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.view);
        make.left.equalTo(self.view);
        make.right.equalTo(self.view);
    }]; 

    [self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.headerView.mas_bottom);
        make.left.equalTo(self.view);
        make.right.equalTo(self.view);
        make.bottom.equalTo(self.view);
    }]; 

}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    return self.customerCell;
}


- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    static dispatch_once_t onceToken;
    static CustomCell *customCell;
    dispatch_once(&onceToken, ^{
        customCell = [[CustomCell alloc] initWithStyle:UITableViewCellStyleDefault
                                       reuseIdentifier:@"customCell"];
        self.customCell = customerCell;
    }); 

    self.customCell.model = self.model;

    return [self calculateHeightForConfiguredSizingCell:self.customCell];
}

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

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

- (CGFloat)calculateHeightForConfiguredSizingCell:(UITableViewCell *)sizingCell {
    [sizingCell setNeedsUpdateConstraints];
    [sizingCell updateConstraintsIfNeeded];

    sizingCell.bounds = CGRectMake(0.0f, 0.0f, CGRectGetWidth(self.tableView.bounds), CGRectGetHeight(self.tableView.bounds));

    [sizingCell setNeedsLayout];
    [sizingCell layoutIfNeeded];

    CGFloat height = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;

    height += 1.0f;

    return height;
}

@end

Here is the cell class:

#import "CustomCell.h"
#import <Masonry.h>
#import "Label.h"
#import "Order.h"
#import "Helper.h"
#import "Theme.h"

@interface CustomCell()

@property (assign, nonatomic) BOOL didSetupConstraints;

@property (retain, nonatomic) Label *dateOneLabel;
@property (retain, nonatomic) Label *dateTwoToLabel;
@property (retain, nonatomic) Label *messageLabel;

@property (retain, nonatomic) Label *dateOneValue;
@property (retain, nonatomic) Label *dateTwoToValue;
@property (retain, nonatomic) Label *messageText;

@property (retain, nonatomic) NSMutableArray *messageConstraints;
@property (retain, nonatomic) MASConstraint *pinBottomOfDateTwoLabelToBottomOfContentViewConstraint;

@end

@implementation CustomCell

- (NSMutableArray *)messageConstraints {
    return _messageConstraints ? _messageConstraints : [@[] mutableCopy];
}

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        [self setup];
    }   
    return self;
}

- (void)awakeFromNib {
    [self setup];
}

- (void) setup {

    self.didSetupConstraints = NO; 

    self.dateOneLabel = [UILabel new];
    self.dateOneLabel.text = @"Date One";

    self.dateTwoLabel = [UILabel new];
    self.dateTwoLabel.text = @"Date Two";

    self.messageLabel = [UILabel new];
    self.messageLabel.text = @"Message";

    self.dateOneValue = [UILabel new];
    self.dateTwoToValue = [UILabel new];

    // The actual message text label that spans numerous lines.
    self.messageText = [UILabel new];
    self.messageText.numberOfLines = 0;
    self.messageText.adjustsFontSizeToWidth = NO; 


    [self.contentView addSubview:self.dateOneLabel];
    [self.contentView addSubview:self.dateTwoToLabel];
    [self.contentView addSubview:self.messageLabel];
    [self.contentView addSubview:self.dateOneValue];
    [self.contentView addSubview:self.dateTwoToValue];
    [self.contentView addSubview:self.messageText];
}

- (void)layoutSubviews {
    [super layoutSubviews];

    [self.contentView setNeedsLayout];
    [self.contentView layoutIfNeeded];

    self.messageText.preferredMaxLayoutWidth = CGRectGetWidth(self.messageText.frame);
}

- (void)updateConstraints {

    if (!self.didSetupConstraints) {

        __weak typeof (self.contentView) contentView = self.contentView;

        // Topmost label, pinned to left side of cell.
        [self.dateOneLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
            make.left.equalTo(contentView).with.offset(14);
            make.right.lessThanOrEqualTo(self.dateOneValue.mas_left).with.offset(-20);
            make.top.equalTo(contentView).with.offset(14);
        }]; 

        // Second label, pinned to left side of cell and below first label.
        [self.dateTwoToLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
            make.left.equalTo(self.dateOneLabel);
            make.top.equalTo(self.dateOneLabel.mas_bottom).with.offset(6);
            make.right.lessThanOrEqualTo(self.dateTwoToValue.mas_left).with.offset(-20);
        }]; 

        // First date value, pinned to right of cell and baseline of its label.
        [self.dateOneValue mas_remakeConstraints:^(MASConstraintMaker *make) {
            make.right.equalTo(contentView).with.offset(-14).priorityHigh();
            make.baseline.equalTo(self.dateOneLabel);
        }]; 

        // Second date value, pinned to right of cell and baseline of its label.
        [self.dateTwoToValue mas_remakeConstraints:^(MASConstraintMaker *make) {
            make.right.equalTo(self.dateOneValue);
            make.baseline.equalTo(self.dateTwoToLabel);
        }]; 

        self.didSetupConstraints = YES;
    }   

    [super updateConstraints];
}

- (void)uninstallMessageConstraints {
    [self.pinBottomOfDateTwoLabelToBottomOfContentViewConstraint uninstall];
    for (MASConstraint *constraint in self.messageConstraints) {
        [constraint uninstall];
    }   
    [self.contentView mas_remakeConstraints:^(MASConstraintMaker *make) {
        self.pinBottomOfDateTwoLabelToBottomOfContentViewConstraint = make.bottom.equalTo(self.dateTwoToLabel).with.offset(14);
    }]; 
}

- (void)installMessageConstraints {

    __weak typeof (self.contentView) contentView = self.contentView;

    [self.pinBottomOfDateTwoLabelToBottomOfContentViewConstraint uninstall];

    // Below, add constraints of `self.messageConstraints` into an array so
    // they can be removed later.

    [self.messageConstraints addObjectsFromArray:[self.messageLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.dateOneLabel);
        make.top.equalTo(self.dateTwoToLabel.mas_bottom).with.offset(6);
    }]];

    self.messageConstraints = [[self.messageText mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.messageLabel);
        make.top.equalTo(self.messageLabel.mas_bottom).with.offset(6);
        make.right.equalTo(contentView).with.offset(-14);
    }] mutableCopy];

    [contentView mas_makeConstraints:^(MASConstraintMaker *make) {
        self.pinBottomOfDateTwoLabelToBottomOfContentViewConstraint = make.bottom.equalTo(self.messageText).with.offset(14);
    }]; 

}


- (void)setModel:(MyModel *)model {
    if (!model.message || model.message.length < 1) {
        [self uninstallMessageConstraints];
        self.messageText.text = @"";
        [self.messageLabel removeFromSuperview];
        [self.messageText removeFromSuperview];
    } else {
        self.messageText.text = model.message;
        if (![self.contentView.subviews containsObject:self.messageLabel]) {
            [self.contentView addSubview:self.messageLabel];
        }   
        if (![self.contentView.subviews containsObject:self.messageText]) {
            [self.contentView addSubview:self.messageText];
        }   
        [self installMessageConstraints];
    }   

    self.dateOneValue.text = model.dateOne;
    self.dateTwoValue.text = model.dateTwo;

    [self.contentView setNeedsDisplay];
    [self.contentView setNeedsLayout];
}

@end

I've been tinkering with this for two days, and at certain points, it looked as desired, but with Autolayout Errors. I have no idea where my errors lie, so my general question is: What is wrong with my code and what needs to change to produce the correct result?

Many thanks.

1

There are 1 best solutions below

3
On

I think you need to add self.messageText.lineBreakMode = NSLineBreakByWordWrapping to force it to multiple lines.