38

So far I have been just using text labels within cells that should auto-size themselves. I usually put constraints to all of the edges (content view or neighbors) and add the following lines to my viewDidLoad():

// 190.0 is the actual height of my custom cell (see next image) in the storyboard containing a label and a randomly sized UIImageView
tableView.estimatedRowHeight = 190.0
tableView.rowHeight = UITableViewAutomaticDimension

Now, when trying to include with images, I face several problems. During some research I found several approaches written in Objective-C but whereas I'm not sure which of those really addressed my central problem I'm still struggling with complex Objective-C code. So to resume: My final goal is to get a table with correctly sized (correct heights) cells, each containing a title and an image that fits the width of the screen. Those images can be of different aspect ratios and different sizes. To simplify some of the challenges, I prepared a new, more simple project:

  1. First, I created a UITableViewController and a CustomCell class with two IBOutlets title: String and postedImage: UIImage (UIImage randomly sized in the storyboard) to configure the cells. I defined constraints to the edges and to each other.

  1. When built, two cells get configured containing the same image but they are fetched from two individual files with different sizes and resolutions (900 x 171 vs. half-sized 450 x 86). The UIImage view mode is set to "Aspect Fit" but as I couldn't get it run the way I want so far I'm not sure this is the right setting. Whereas I had expected the cell height to be calculated based on the rescaled UIImage inclusively the constraints I had set, it seems to be based on the initial height of the image files (so 171 and 86). It doesn't actually adapt to the actual UIImage view height (around 70 I'd guess).

Both cell heights are based on the initial image file height and not on the actual UIImage height

In a somewhat more complex project of mine I want different images to automatically fit the width of the screen no matter whether they are portrait or landscape - and in case their width is less then the screen's width, they should be scaled up. Like in the project above each cell height should fit its individual content as defined by the constraints in the storyboard. Anyway, it starts with the problem discussed above: How can I let those two cells have the same, correct height, hugging its content like defined in the storyboard?

Thanks a million for any help!

alexeis
  • 2,152
  • 4
  • 23
  • 30
  • Fancy posting the test project up on github or somewhere? – Mike Pollard Sep 25 '14 at 15:56
  • @MikePollard Sorry, don't really get what you mean... Btw, I edited and specified my question. – alexeis Sep 25 '14 at 20:22
  • I was just suggesting uploading your code so we could have a play and help you... – Mike Pollard Sep 25 '14 at 20:24
  • @MikePollard The code is incredibly basic but maybe it is helpful combined with the storyboard where I put the constraints/settings so I uploaded the project anyway: https://www.wetransfer.com/downloads/534b22cf2a307ccd3d80ad34f7201bb020140925210426/1eb9691685b4b2bfbfb2310b26a5e0dc20140925210426/c841dc – alexeis Sep 25 '14 at 21:07
  • Thanks, I've had a play and found a workaround. Hope my answer helps... – Mike Pollard Sep 26 '14 at 09:52

5 Answers5

78

So I think the underlying issue is a kind of chicken and egg problem.

In order to 'aspect fit' the image to the UIImageView the system needs a size for the view, and yet we would like the height of the view to be determined after aspect fitting the image given a known width for the view.

A workaround is to calculate the aspect ratio of the image ourselves and set it as a constraint on the UIImageView when we set the image. That way the auto layout system has enough information to size the view (a width and an aspect ratio).

So I changed your custom cell class to:

class CustomCell: UITableViewCell {

    @IBOutlet weak var imageTitle: UILabel!

    @IBOutlet weak var postedImageView: UIImageView!

    internal var aspectConstraint : NSLayoutConstraint? {
        didSet {
            if oldValue != nil {
                postedImageView.removeConstraint(oldValue!)
            }
            if aspectConstraint != nil {
                postedImageView.addConstraint(aspectConstraint!)
            }
        }
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        aspectConstraint = nil
    }

    func setPostedImage(image : UIImage) {

        let aspect = image.size.width / image.size.height

        aspectConstraint = NSLayoutConstraint(item: postedImageView, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: postedImageView, attribute: NSLayoutAttribute.Height, multiplier: aspect, constant: 0.0)

        postedImageView.image = image
    }

}

And then in the delegate do this instead of setting the image directly:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as CustomCell

        cell.imageTitle.text = titles[indexPath.row]

        let image = images[titles[indexPath.row]]!
        cell.setPostedImage(image)

        return cell
}

And you will get:

enter image description here

Hope this helps!

Mike Pollard
  • 10,195
  • 2
  • 37
  • 46
  • Great, thanks! Just an optional question: How did you proceed analyzing that problem and get the right workaround? Or is it just pure experience?;-) Any key points that could help me getting somewhat further by my own next time? – alexeis Sep 26 '14 at 10:21
  • 5
    I started by adding some logging to print out the imageView's `intrinsicSize`, the image's `size` and the imageView's `frame` then tried to consider how the auto layout system could use the information to arrive at the answer you wanted. After a while it just dawned on me that the 'aspect fitting' part was separate from the auto layout part... It helps that my head is kind of in 'auto layout mode' at the moment having been recently wrestling with various problems to do with making layouts work nicely on 4, 5, 6 and 6plus. – Mike Pollard Sep 26 '14 at 10:39
  • This seems like a brilliant solution. Allthough I'm not the Swift kind of guy. So I've added a NSLayoutConstraint as a IBOutlet on my custom cell and tied it to the height constraint. Instead of adding and removing the height constraint every time, I want to just set the height. But somehow this causes a crash with a "not key value coding-compliant for the" layout constraint IBOutlet. What am I missing? – esbenr Apr 20 '15 at 21:52
  • Thanks for the great answer. I have difficulties in implementing your solution though. I'm removing constraint before adding new ones but it seems in the end all the cells share the same constraint ... And not each cell is own .... – gotye Jul 09 '15 at 04:02
  • This is phenomenal! I just created a category in obj-c for UIImageView to do this. – Brian Trzupek Jun 08 '16 at 19:32
  • @esbenr that won't work. You need to change the `multiplier` of the constraint which is read only. So the only way to make it work is to remove the old constraint and add a new one. – Fogmeister Aug 01 '18 at 12:17
  • I'm facing a similar issue. In my app a user can post images. They may be a perfect square, or landscape, or portrait. Because a user can post just text I am using a height constraint on the image so that if no image is there, set to 0 and hide the image. How can this be updated to use my height constraint so that when a new post is made the cell will initially snap to the image size (height) and also fill the width? – Luke Irvin May 30 '19 at 15:22
  • This is a great answer. I've added similar code to the cell's `awakeFromNib` function, and it seems to work fine. (The UIImage is already loaded when `awakeFromNib` is called) – Matthew Feb 28 '20 at 11:20
  • Thanks so much, this really helped me on a work problem. – msadoon Apr 27 '22 at 20:10
7

I translated Pollard's solution to objc version something like this.
Also I got warnings like "Unable to simultaneously satisfy constraints". My solution is set the priority of vertical space constraint as 999 rather than 1000.

@interface PostImageViewCell()

@property (nonatomic, strong) NSLayoutConstraint *aspectContraint;
@end

@implementation PostImageViewCell

- (void) setAspectContraint:(NSLayoutConstraint *)aspectContraint {

    if (_aspectContraint != nil) {
        [self.postImage removeConstraint:_aspectContraint];
    }
    if (aspectContraint != nil) {
        [self.postImage addConstraint:aspectContraint];
    }
}

- (void) prepareForReuse {
    [super prepareForReuse];
    self.aspectContraint = nil;
}

- (void)setPicture:(UIImage *)image {

    CGFloat aspect = image.size.width / image.size.height;
    self.aspectContraint = [NSLayoutConstraint constraintWithItem:self.postImage
                                                        attribute:NSLayoutAttributeWidth
                                                        relatedBy:NSLayoutRelationEqual
                                                           toItem:self.postImage
                                                        attribute:NSLayoutAttributeHeight
                                                       multiplier:aspect
                                                         constant:0.0];
    [self.postImage setImage:image];
}
@end
허광호
  • 71
  • 1
  • 3
1

Working in Swift 3 + iOS 8

Using AutoLayout and variable sized UITableViewCell objects -

For imageview, set top, bottom, leading and trailing space constraints to 0.

Code to be added to viewDidLoad()

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 200

Define UITableViewDelegate's heightForRowAtIndexPath ()

extension ViewController: UITableViewDelegate {

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        let image = UIImage(named: list_images[indexPath.row])
        let aspect = (image?.size.width)! / (image?.size.height)!
        let cellHeight = (tableView.frame.width / aspect) + <<Height of image title label>>
        return cellHeight
    }
}
c_sharma
  • 135
  • 1
  • 12
  • I've fought hard at this problem - and still currently doing - and from my experience, using tableView:heightForRowAtIndexPath: makes the UITableView very slow; scrolling performance is affected badly. You don't want to use this function, except maybe for testing / checking a possible solution. – dinesharjani Aug 07 '17 at 10:32
0

Instead of having custom setPostedImage method it's more convenient to update aspect constraint in updateConstraints. That way, you can change UIImageView's image directly without additional helper method:

static NSLayoutConstraint *constraintWithMultiplier(NSLayoutConstraint *constrain, CGFloat multiplier) {
    return [NSLayoutConstraint constraintWithItem:constrain.firstItem
                                        attribute:constrain.firstAttribute
                                        relatedBy:constrain.relation
                                           toItem:constrain.secondItem
                                        attribute:constrain.secondAttribute
                                       multiplier:multiplier
                                         constant:constrain.constant];
}

-(void)viewDidLoad
{
    UIView *contentView = self.contentView;

    _imageView = [[UIImageView alloc] init];
    _imageView.translatesAutoresizingMaskIntoConstraints = NO;
    _imageView.contentMode = UIViewContentModeScaleAspectFit;
    [contentView addSubview:_imageView];

    _imageAspectConstraint = [_imageView.heightAnchor constraintEqualToAnchor:_imageView.widthAnchor multiplier:1.0f];
    NSArray <NSLayoutConstraint *> *constraints = @[
            [_imageView.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor constant:0],
            [_imageView.trailingAnchor constraintEqualToAnchor:contentView.trailingAnchor constant:0],
            [_imageView.topAnchor constraintEqualToAnchor:contentView.topAnchor constant:0],
            [_imageView.bottomAnchor constraintEqualToAnchor:contentView.bottomAnchor constant:0],
    ];
    [NSLayoutConstraint activateConstraints:constraints];
}

-(void)updateConstraints
{
    const CGSize size = _imageView.image.size;
    const CGFloat multiplier = size.width / size.height;
    _imageAspectConstraint.active = NO;
    _imageAspectConstraint = constraintWithMultiplier(_imageAspectConstraint, multiplier);
    _imageAspectConstraint.active = YES;
    [super updateConstraints];
}
wonder.mice
  • 7,227
  • 3
  • 36
  • 39
0

Swift 4

Calculate image height using UIImage extension.

You can use following extension of UIImage to get Height according to AspectRatio of image.

extension UIImage {
    var cropRatio: CGFloat {
        let widthRatio = CGFloat(self.size.width / self.size.height)
        return widthRatio
    }
}

So, inside your override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat method.

override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
            let currentImage = dataSource[indexPath.row].image
            let imageCrop = currentImage.cropRatio
            var _height = tableView.frame.width / imageCrop
            return _height
}
Rashesh Bosamiya
  • 609
  • 1
  • 9
  • 13