UIImageView, Autolayout, and UITableView

I have a situation where I’m using a UITableView to handle a multi-user chat.  Seems to be the right tool for the job.  I’m also getting used to Auto-Layout.  I’m glad I held off learning it for a while, but now seems to be the right time, especially with all the different window sizes of the various iOS Devices out there.

I say I’m glad I held off because iOS 8 released a wonderful new feature for UITableView.  Auto-cell sizing.  With it, comes the dependency that you should really be trying to solve most of your layout problems with Interface Builder.  At least when it comes to constraints.

So I have the use case where I’d like to display messages in a table view.  Basically the tutorial over at www.raywenderlich.com is a very good starting point.

It got me part of the way, though I did still have a few gotchas, and perhaps it was the ADD, but when I got to the image cell portion of the tutorial, it started to smell overly custom for that application, and not a general enough solution.  I lost interest in the tutorial.

So yes, I’m building a chat app.  It should very roughly look like this, where on one side you have your messages, and on the other side the other person’s (not shown).

Screenshot 2015-04-27 16.26.21

The text cells were straightforward enough.  They follow a lot from the raywenderlich.com tutorial other than the situation with wrapping text;  I don’t want my labels to take up the whole width of the cell, but I also don’t want them to take up more width than necessary if there isn’t enough text.

So, making sure your UILabel instance has numLines set to 0 (unlimited), you proceed to set the label’s frame to it’s maximum width you want to allow it, then pin the top, leading, and trailing space to the contentView.  Now edit the trailing space constraint and set it from Equal to GreaterThanOrEqual.  This is then setting the maximum width, but it doesn’t have to be.  What you should also do for this UILabel is set its content compression resistance priority to 1000.  In doing so, you prevent the greater than or equal to constraint from trying to squish the content’s width, and truncating the label.

The other gotcha of a UILabel is that in this form right now, auto-rotation does not do what you would expect.  This aspect I have taken from the raywenderlich.com tutorial, and made a subclass of UILabel and override the setBounds: method.


@implementation MyCustomLabel

- (void)setBounds:(CGRect)bounds {
    [super setBounds:bounds];
    
    // you could get more specific with your logic here, 
    // making an if statement for the exact scenario, such as if numLines == 0, and so on.
  
    [self setNeedsUpdateConstraints];
}
@end

That should sort you out with the label part.

Now to make an ImageCell. This part got a little tricky, but what occurred to me is to not fight the way things should be done. This involves a custom implementation of:

- (CGSize)intrinsicContentSize;

I want to make a UIImageView that keeps its aspect ratio of the original image, but doesn’t grow too large, but can accommodate any size image.

Again, I specify how large the image frame can be (via constraints in IB, where I pin the width or height of the image view and specify the relation as LessThanOrEqual), and let the image view calculate its intrinsic content size.

Here. Code.


@implementation MyCustomCellImageView

- (void)setImage:(UIImage *)image
{
    [super setImage:image];
    [self invalidateIntrinsicContentSize];
}

- (void)setBounds:(CGRect)bounds {
    [super setBounds:bounds];
    [self setNeedsUpdateConstraints];
}

- (CGSize)intrinsicContentSize
{
    //  if I don't have an image, or haven't set Scale Aspect Fit, uh, I dunno.  
    //  Do what it normally would do.
    if (!self.image || self.contentMode != UIViewContentModeScaleAspectFit) {
        return [super intrinsicContentSize];
    }
    else
    {
        CGFloat aspectRatio = self.image.size.width / self.image.size.height;
        
        BOOL maxWidthSet = NO, maxHeightSet = NO;
        CGFloat maxWidth = 0.f, maxHeight = 0.f;
        
        NSPredicate *hwPred = [NSPredicate predicateWithFormat:@"firstAttribute == %d || firstAttribute == %d", NSLayoutAttributeWidth, NSLayoutAttributeHeight];
        NSArray *constraints = [self.constraints filteredArrayUsingPredicate:hwPred];
        
        for(NSLayoutConstraint *constraint in constraints)
        {
            // height and width will have nil for their second attribute
            if (constraint.firstAttribute == NSLayoutAttributeHeight) {
                if (constraint.relation == NSLayoutRelationLessThanOrEqual ||
                    constraint.relation == NSLayoutRelationEqual) {
                    maxHeightSet = YES;
                    maxHeight = constraint.constant;
                }
            }
            else if (constraint.firstAttribute == NSLayoutAttributeWidth)
            {
                if (constraint.relation == NSLayoutRelationLessThanOrEqual ||
                    constraint.relation == NSLayoutRelationEqual) {
                    maxWidthSet = YES;
                    maxWidth = constraint.constant;
                }
            }
            
            if (maxWidthSet && maxHeightSet) {
                break;
            }
        }
        
        if (!maxWidthSet && !maxHeightSet) {
            NSLog(@"Didn't expect there to be no constraints.  Using the max dimension of the current frame, though I have no idea what's gonna happen.");
            maxWidth = MAX(self.bounds.size.width, self.bounds.size.height);
            maxHeight = maxWidth;
        }
        else if (!maxWidthSet)
        {
            maxWidth = maxHeight * aspectRatio;
        }
        else if (!maxHeightSet)
        {
            maxHeight = maxWidth / aspectRatio;
        }
        
        // we figure out basically how big the image CAN be, then figure out what it size fits in that aspect ratio.
        
        CGSize intrinsicContentSize = [self sizeAspectFit:self.image.size destSize:(CGSize){maxWidth, maxHeight}];
        
        return intrinsicContentSize;
    }
}

- (CGSize)sizeAspectFit:(CGSize)sourceSize destSize:(CGSize)destSize
{
    CGFloat destScale = [self aspectScaleFit:sourceSize destSize:destSize];
    
    CGFloat newWidth = floorf(sourceSize.width * destScale);
    CGFloat newHeight = floorf(sourceSize.height * destScale);
    
    CGSize size = CGSizeMake(newWidth, newHeight);
    return size;
}

- (CGFloat)aspectScaleFit:(CGSize)sourceSize destSize:(CGSize)destSize
{
    CGFloat scaleW = destSize.width / sourceSize.width;
    CGFloat scaleH = destSize.height / sourceSize.height;
    return MIN(scaleW, scaleH);
}

@end

And found that this got the job done extremely well. The approach for setting it up in IB was also identical to the label, of course you also have to set the maximum width and height of the image view, OH, and set a placeholder intrinsic content size (which matches the frame)

What is also good about this approach is that you can put other subviews in the contentView hierarchy, either in front of, or behind the content views (the Label and the ImageView) and you can pin their top, left, bottom, right to those of the content.  (would be then negative constant values).  This way you can separate the presentation of the content views from the content itself.  (Think rounded rects, or chat bubbles, etc.)  Also won’t require any further custom code embedded in what we already have above, which is still pretty generic.

Worked for me! I might have not written enough info here. Please write in the comments if I can clarify further.

Advertisements

2 thoughts on “UIImageView, Autolayout, and UITableView

  1. I’m really happy I stumble upon your article after so many research on the topic UIImageView in UITableViewCell….
    Would you consider posting the demo project? I checked your Github and couldn’t find it. Cheers!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s