1

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;
}
Community
  • 1
  • 1
rizzes
  • 1,532
  • 20
  • 33

2 Answers2

1

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"];
});
Justin Moser
  • 2,005
  • 3
  • 22
  • 28
  • I get the following error when duplicating your code into a new project (called in `viewDidAppear`): `*** Assertion failure in -[RLReusableView layoutSublayersOfLayer:], /SourceCache/UIKit_Sim/UIKit-2935.137/UIView.m:8794` `Auto Layout still required after executing -layoutSubviews. RLReusableView's implementation of -layoutSubviews needs to call super.` – rizzes Aug 04 '14 at 18:40
  • Interesting. You are certain you are implementing [super layoutSubviews] in the layoutSubviews method of RLReusableView? Are you overriding any other methods? – Justin Moser Aug 04 '14 at 18:49
  • Yeah, it's interesting. I am sure I implemented [super layoutSubviews]. I found that question as well but I couldn't find an answer that wasn't specific to UITableViewCell. Interestingly enough, I can move all the constraints code from layoutSubviews into the init and it works as it should. Strange, no? – rizzes Aug 04 '14 at 19:06
0

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.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Do you mean something like this? `textLabel.frame = CGRectMake(textLabel.frame.origin.x, textLabel.frame.origin.y, [textLabel systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].width, [textLabel systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height);` When would this be called? And I feel like my code above is wrong because it sets the frame and setting frames is generally a no-no when using autolayout. – rizzes Aug 02 '14 at 23:06
  • 1
    The `textLabel` will size itself; there is no need to set its size. I'm talking about the question you asked about how to size _the UIView that contains the textLabel_ in response to a change in the size of the label. I'm saying that if that's what you want to do, you would need to do that manually somehow. And yes, if you're using auto layout you would have to do it with constraints, not by setting the frame directly. – matt Aug 03 '14 at 02:53