rizzes
rizzes

Reputation: 1542

iOS - Multiline UILabel Height With Constraints

I am trying to create a reusable message UIView subclass that adjusts its height based on the text inside its UILabel. This answer says it can't be done. Is that really the case? iOS message cell width/height using auto layout

My issue is that the UILabel's CGFrame's height is too large. (The UILabel has a green background color.)

The height is much too large for the next needed.

Here's my code (by the way, [autolayoutView] sets translatesAutoresizingMaskIntoConstraints to NO):

SSLStickyView *stickyView = [[SSLStickyView alloc] initWithText:@"Try keeping a steady beat to help me get past the notes! Press the bass drum to jump!"];

SSLStickyView.m

- (instancetype)initWithText:(NSString *)text
{
    self = [super initWithFrame:CGRectZero];
    if (self)
    {

        _stickyImageView = [UIImageView autoLayoutView];
        _stickyImageView.backgroundColor = [UIColor blueColor];
        _stickyImageView.image = [UIImage imageNamed:@"element_sticky"];
        [self addSubview:_stickyImageView];

        float padding = 5;
        NSMutableAttributedString *attributedText =
        [[NSMutableAttributedString alloc]
         initWithString:text
         attributes:@
         {
         NSFontAttributeName: [UIFont boldSystemFontOfSize:30],
         NSForegroundColorAttributeName: [UIColor purpleColor]
         }];

        UILabel *textLabel = [UILabel autoLayoutView];
        textLabel.preferredMaxLayoutWidth = 50;
        textLabel.attributedText = attributedText;
        textLabel.numberOfLines = 0; // unlimited number of lines
        textLabel.lineBreakMode = NSLineBreakByWordWrapping;
        textLabel.backgroundColor = [UIColor greenColor];

        [_stickyImageView addSubview:textLabel];

        NSLayoutConstraint *stickyWidthPin =
        [NSLayoutConstraint constraintWithItem:_stickyImageView
                                     attribute:NSLayoutAttributeWidth
                                     relatedBy:NSLayoutRelationEqual
                                        toItem:textLabel
                                     attribute:NSLayoutAttributeWidth
                                    multiplier:1
                                      constant:padding * 2];
        NSLayoutConstraint *stickyHeightPin =
        [NSLayoutConstraint constraintWithItem:_stickyImageView
                                     attribute:NSLayoutAttributeHeight
                                     relatedBy:NSLayoutRelationEqual
                                        toItem:textLabel
                                     attribute:NSLayoutAttributeHeight
                                    multiplier:1
                                      constant:0];
        NSLayoutConstraint *stickyTextLabelTop =
        [NSLayoutConstraint constraintWithItem:_stickyImageView
                                     attribute:NSLayoutAttributeTop
                                     relatedBy:NSLayoutRelationEqual
                                        toItem:textLabel
                                     attribute:NSLayoutAttributeTop
                                    multiplier:1
                                      constant:0];
        NSLayoutConstraint *stickyTextLeftPin = [NSLayoutConstraint constraintWithItem:_stickyImageView
                                                                            attribute:NSLayoutAttributeLeft
                                                                            relatedBy:NSLayoutRelationEqual
                                                                               toItem:textLabel
                                                                            attribute:NSLayoutAttributeLeft
                                                                           multiplier:1
                                                                             constant:-padding * 2];
        NSDictionary *views = NSDictionaryOfVariableBindings(_stickyImageView, textLabel);
        [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|[_stickyImageView]" options:0 metrics:nil views:views]];
        [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_stickyImageView]" options:0 metrics:nil views:views]];
        [self addConstraints:@[stickyWidthPin, stickyHeightPin, stickyTextLeftPin, stickyTextLabelTop]];
        self.backgroundColor = [UIColor whiteColor];

    }

    return self;
}

Upvotes: 1

Views: 2604

Answers (2)

Justin Moser
Justin Moser

Reputation: 2005

Make your superview aware of changes of its content's size and adjust the superview's width and height accordingly. Since it may not be immediately obvious how to do this I have provided a UIView subclass that will resize itself based on its content (a UILabel).

Note: Only add constraints to ReusableMessageView that would affect its location within its superview. ReusableMessageView will adjust its width/height based on the message.

@interface ReusableMessageView : UIView
-(instancetype)initWithMessage:(NSString *)message preferredWidth:(CGFloat)width;
-(void)setMessage:(NSString *)message;
@end

@implementation ReusableMessageView {
    UILabel *_label;
}

-(instancetype)initWithMessage:(NSString *)message preferredWidth:(CGFloat)width {
    if (self = [super init]) {
        self.translatesAutoresizingMaskIntoConstraints = NO;
        //setup label
        _label = [UILabel new];
        _label.translatesAutoresizingMaskIntoConstraints = NO;
        _label.text = message;
        _label.preferredMaxLayoutWidth = width;
        _label.numberOfLines = 0;
        [self addSubview:_label];
    }
    return self;
}

-(void)layoutSubviews {
    [super layoutSubviews];
    // remove all previously added constraints
    [self removeConstraints:self.constraints];

    CGFloat width = _label.bounds.size.width;
    CGFloat height = _label.bounds.size.height;

    NSLayoutConstraint *c1,*c2,*c3,*c4;
    // set the view's width/height to be equal to the label's width/height
    c1 = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:width];
    c2 = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:height];
    // center the label
    c3 = [NSLayoutConstraint constraintWithItem:_label attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0];
    c4 = [NSLayoutConstraint constraintWithItem:_label attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0];
    // add all constraints
    [self addConstraints:@[c1,c2,c3,c4]];
}

-(void)setMessage:(NSString *)message {
    _label.text = message;
    // once the message changes, the constraints need to be adjusted
    [self setNeedsLayout];
}
@end

This could be improved by reusing existing constraints and only changing the "constant" property of each constraint. You could even animate the change if you did this.

Here is an example use inside of a viewController's viewDidLoad method:

ReusableMessageView *view = [[ReusableMessageView alloc]initWithMessage:@"This is the first message that is rather long in order to exaggerate the change in size" preferredWidth:50.0];
[self.view addSubview:view];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0]];
// demonstrate the change in size
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [view setMessage:@"This is the second message"];
});

Upvotes: 1

matt
matt

Reputation: 534893

Of course it can be done, but not without code. Views do not automatically make themselves smaller based on the constraints and sizes of what's inside them. That's not how auto layout behaves.

You can determine the size that something needs to be based on the constraints and sizes of what's inside them (using systemLayoutFittingSize) and you can set that thing to that size in code. But it isn't going to happen by some kind of magic.

Upvotes: 0

Related Questions