Auto-Layout: Get UIImageView height to calculate cell height correctly

20,760

Solution 1

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!

Solution 2

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

Solution 3

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
    }
}
Share:
20,760
Alexei S.
Author by

Alexei S.

Co-founder at Rethinker | Swiss entrepreneur and developer | Former pro athlete, 3-times Swiss Champion in Beach Volleyball/Volleyball

Updated on August 02, 2020

Comments

  • Alexei S.
    Alexei S. over 3 years

    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!

    • Mike Pollard
      Mike Pollard over 9 years
      Fancy posting the test project up on github or somewhere?
    • Alexei S.
      Alexei S. over 9 years
      @MikePollard Sorry, don't really get what you mean... Btw, I edited and specified my question.
    • Mike Pollard
      Mike Pollard over 9 years
      I was just suggesting uploading your code so we could have a play and help you...
    • Alexei S.
      Alexei S. over 9 years
      @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: wetransfer.com/downloads/…
    • Mike Pollard
      Mike Pollard over 9 years
      Thanks, I've had a play and found a workaround. Hope my answer helps...
  • Alexei S.
    Alexei S. over 9 years
    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?
  • Mike Pollard
    Mike Pollard over 9 years
    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.
  • esbenr
    esbenr about 9 years
    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?
  • gotye
    gotye almost 9 years
    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 ....
  • Balazs Nemeth
    Balazs Nemeth over 8 years
    The 'set' is missing in the setter: _aspectContraint = aspectContraint;
  • Brian Trzupek
    Brian Trzupek almost 8 years
    This is phenomenal! I just created a category in obj-c for UIImageView to do this.
  • dinesharjani
    dinesharjani over 6 years
    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.
  • Fogmeister
    Fogmeister almost 6 years
    @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.
  • Luke Irvin
    Luke Irvin almost 5 years
    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?
  • Matthew
    Matthew about 4 years
    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)
  • msadoon
    msadoon about 2 years
    Thanks so much, this really helped me on a work problem.