26

I want to implement a TableView with a Custom TableViewCell showing an image.

To make this simple, I simply put a UIImageView inside a tableviewcell using autolayout (illustrated below). enter image description here

What I want is to display the image inside a UIImageView however those images dimensions can be anything and are inconsistent (portrait, landscape, square) ...

Therefore, I'm looking to display an image with a fixed width (the width of the device) and a dynamic height that respect the ratio of the images. I want something like this:
enter image description here

I unfortunately didn't manage to reach that result.

Here's how I implemented it - (I'm using Haneke to load the image from an URL - image stored in Amazon S3):

class TestCellVC: UITableViewCell {

    @IBOutlet weak var selfieImageView: UIImageView!
    @IBOutlet weak var heightConstraint: NSLayoutConstraint!

    func loadItem(#selfie: Selfie) {        
        let selfieImageURL:NSURL = NSURL(string: selfie.show_selfie_pic())!

        self.selfieImageView.hnk_setImageFromURL(selfieImageURL, placeholder: nil, format: HNKFormat<UIImage>(name: "original"), failure: {error -> Void in println(error)
            }, success: { (image) -> Void in
                // Screen Width
                var screen_width = UIScreen.mainScreen().bounds.width

                // Ratio Width / Height
                var ratio =  image.size.height / image.size.width

                // Calculated Height for the picture
                let newHeight = screen_width * ratio

                // METHOD 1
                self.heightConstraint.constant = newHeight

                // METHOD 2
                //self.selfieImageView.bounds = CGRectMake(0,0,screen_width,newHeight)

                self.selfieImageView.image = image
            }
        )
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // Register the xib for the Custom TableViewCell
        var nib = UINib(nibName: "TestCell", bundle: nil)
        self.tableView.registerNib(nib, forCellReuseIdentifier: "TestCell")

        // Set the height of a cell dynamically
        self.tableView.rowHeight = UITableViewAutomaticDimension
        self.tableView.estimatedRowHeight = 500.0

        // Remove separator line from UITableView
        self.tableView.separatorStyle = UITableViewCellSeparatorStyle.None

        loadData()
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        var cell = tableView.dequeueReusableCellWithIdentifier("TestCell") as TestCellVC


        cell.loadItem(selfie: self.selfies_array[indexPath.row])
        // Remove the inset for cell separator
        cell.layoutMargins = UIEdgeInsetsZero
        cell.separatorInset = UIEdgeInsetsZero

        // Update Cell Constraints
        cell.setNeedsUpdateConstraints()
        cell.updateConstraintsIfNeeded()
        cell.sizeToFit()

        return cell
    }
}

My calculation of the dynamic Height is correct (I've printed it). I've tried both method (describe in the code) but none of them worked:

  1. Set the Height Autolayout constraint of the UIImageView
  2. Modify the frame of the UIImageView

See Results of Method 1 and 2 here: image Image 2

Cœur
  • 37,241
  • 25
  • 195
  • 267
fabdarice
  • 885
  • 2
  • 11
  • 23
  • What is it displaying instead? Setting the image's new height constraint is correct, but AFAICT, nothing is telling your table cell to change its height based on the height of the image. – NRitH Aug 29 '15 at 04:28
  • Hi ! I've added the link to the results of both method. Where would I tell the table cell to change its height? I think, if I'm not wrong, Haneke's function "self.selfieImageView.hnk_setImageFromURL" is async so I don't really get the height of the Cell before it's loaded. – fabdarice Aug 29 '15 at 06:14
  • you could solve this problem , I'm in the same case? – Pablo Ruan Aug 23 '16 at 20:34
  • @fabdarice,any solution that you are using for this? – Ankit Kumar Gupta Dec 13 '18 at 13:41
  • any solution you found? @fabdarice – Utku Dalmaz Feb 04 '19 at 20:48
  • @fabdarice did you find a solution? I'm facing similar issue for posting new images. I have to scroll to get them to resize correctly. – Luke Irvin May 29 '19 at 20:41

7 Answers7

30

In the storyboard add trailing, leading, bottom and top constraints to your UIImageView. After you load your image all you need to do is add an aspect constraint to your UIImageView. Your images cell would look something like this:

class ImageTableViewCell: UITableViewCell {

    @IBOutlet weak var customImageView: UIImageView!

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

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

    func setCustomImage(image : UIImage) {

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

        let constraint = NSLayoutConstraint(item: customImageView, attribute: NSLayoutAttribute.width, relatedBy: NSLayoutRelation.equal, toItem: customImageView, attribute: NSLayoutAttribute.height, multiplier: aspect, constant: 0.0)
        constraint.priority = 999

        aspectConstraint = constraint

        customImageView.image = image
    }
}

You can check working example here DynamicTableWithImages

sash
  • 8,423
  • 5
  • 63
  • 74
  • To use this solution, simply call 'cell.setCustomImage(image: )' to set an image for the cell when dequeueing the cell. – Elardus Erasmus Apr 22 '17 at 12:17
  • Best answer in the universe. Thank you really much for the GitHub repo! – Aluminum May 17 '17 at 10:33
  • 7
    Unfortunately this does not refresh the cell height if image is fetched async - I think OP has this problem. – Jonny Jan 25 '18 at 08:11
  • 1
    For Swift 4 you have to use constraint.priority = UILayoutPriority(rawValue: 999) – Guilherme Carvalho Jun 26 '18 at 10:22
  • 2
    If your images are fetched asynchronously, look at https://stackoverflow.com/a/34079027/62. – Liron Yahdav Aug 15 '18 at 03:48
  • How can i set image view height above but image source is url(image load from url) – Pintu Rajput Dec 25 '18 at 15:47
  • I struggled a lot on this one. My mistake was that I added my UIImageView constraints to the tableViewCell insead of on its contentView. Changed it and everything works like a charm – Cerise May 07 '19 at 13:25
  • This is nice, but how can the table be refreshed if new images are being added in? Using based off the example my images are really small after posted and then resize after I scroll them off screen. – Luke Irvin May 29 '19 at 20:07
  • Instead of 999 priority you can also used predefined .defaultHigh constant and it will also work. – Leszek Szary Jun 25 '20 at 10:07
20

enter image description here

While loading images into the tableView each image can be with diffrent aspect ratio. With using of SDWebImage we can get the image from url as -

 testImageView.sd_setImage(with: url, placeholderImage: nil, options: [], completed: { (downloadedImage, error, cache, url) in
            print(downloadedImage?.size.width)//prints width of image
            print(downloadedImage?.size.height)//prints height of image
        })

In UITableViewCell set the constraint for imageView as top, bottom, leading, trailing, fixed height constraint with 999 priority And change height-constraint-constant according to image.

 var cellFrame = cell.frame.size
 cellFrame.height =  cellFrame.height - 15
 cellFrame.width =  cellFrame.width - 15
 cell.feedImageView.sd_setImage(with: url, placeholderImage: nil, options: [], completed: { (theImage, error, cache, url) in
            cell.feedImageHeightConstraint.constant = self.getAspectRatioAccordingToiPhones(cellImageFrame: cellFrame,downloadedImage: theImage!)

        })

Calculate cell frame & find the respective height based on that.

  func getAspectRatioAccordingToiPhones(cellImageFrame:CGSize,downloadedImage: UIImage)->CGFloat {
        let widthOffset = downloadedImage.size.width - cellImageFrame.width
        let widthOffsetPercentage = (widthOffset*100)/downloadedImage.size.width
        let heightOffset = (widthOffsetPercentage * downloadedImage.size.height)/100
        let effectiveHeight = downloadedImage.size.height - heightOffset
        return(effectiveHeight)
    }

Example on Github

Jack
  • 13,571
  • 6
  • 76
  • 98
  • If you have anything to add, feel free to comment or suggest an edit :) – Jack Oct 31 '17 at 08:10
  • What if you also have a label above the image. With this code the label seems to be under the image. – Jamie Nov 01 '17 at 22:28
  • Simple - add constraints of label in respective of images. – Jack Nov 02 '17 at 01:59
  • hello Jack its really nice, but when I try to make your solution I face a problem that image is displayed first in small size and after scrolling down and return back to the previous visible cell it works fine – Amr Angry May 26 '18 at 15:31
  • @AmrAngry i think we can't set image frame unless & until we know the image aspect ratio.Hence first time we have to download the image & get the aspect ratio from that. in another way image aspect ratio should come from web-service(Like what instagram do's). – Jack May 28 '18 at 09:00
  • @Jack , "aspect ratio should come from web-service(Like what instagram do's" can you give me more info about this, I solved my problem by making delegate method that after cal the hight I notify the tableview to reload the cell – Amr Angry May 30 '18 at 07:19
  • 1
    You can check at https://www.instagram.com/developer/endpoints/media/ where it gives `"low_resolution": { "url": "http://distillery.s3.amazonaws.com/media/2011/01/28/0cc4f24f25654b1c8d655835c58b850a_6.jpg", "width": 306, "height": 306 },` – Jack May 30 '18 at 07:27
  • Glad I could help:) – Jack Aug 04 '18 at 15:57
  • 1
    You saved my time :) Thanks a lot ! – Khush Mar 01 '19 at 11:06
4

If you want the UIImageView to tell the UITableView how tall it needs to be, then you need to configure the table view to enable automatic row calculation. You do this by setting estimatedRowHeight to a fixed positive value and setting rowHeight to UIViewAutomaticDimension. That's part one.

Now you need to configure the UIImageView to request a height based on the width which the table view requires and on the aspect ratio of the image. This will be part two.

First, set the contentMode to .AspectFit. This will cause the view to resize the appearance of the image based on whatever dimensions it takes. However, this still doesn't cause it to request the needed height with auto layout. Instead, it will request the height of the intrinsicContentSize, which may be quite different from the resized image. So, second, add a constraint to the UIImageView that is of the form width = height * multiplier, where the multiplier is based on the aspect ratio of the image itself.

Now the table view will require the correct width, the contentMode mechanism will ensure the image is resized correctly without distortion, and the aspect ration constraint will ensure that the image view requires the correct height via auto layout.

This works. The downside of it is that you will need to update the aspect ratio constraint every time you assign a new image to the image view, which could be quite often if the image view is in a cell that's getting re-used.

An alternative approach is to add only a height constraint and update it in the layoutSubviews of a view containing the image view. This has better performance but worse code modularity.

algal
  • 27,584
  • 13
  • 78
  • 80
  • Hi @algal, first of all, thanks for your answer, much appreciate it. If you look at my code, I did exactly steps by steps everything you just describe. The only part I didn't do fully is the multiplier part. The reason behind it is because if I add an autolayout constraint with a multiplier (width = height * multiplier), I cannot modify the multiplier programmatically as it is a read-only parameters. I've also tried the alternative approche (describe in method 1 above), withotu success unfortunately. – fabdarice Aug 29 '15 at 08:06
  • Yup, you can't modify the AL aspect ratio constraint in place. So what you need to do instead is remove the existing constraint and add a new one every time you change the image. You can do this with a property observer on the image. Also, best would be to use a flag so the remove happens immediately and the add happens in `updateConstraints`. – algal Aug 29 '15 at 15:45
  • 2
    Also, as regarding your code above, even if you're loading the image asynchronously, you should try to determine the aspect ratio and set the aspect ratio constraint immediately if possible. Otherwise all your table view's layout calculations will be initially speculative and wrong until the images load, and when the images do arrive you may need to trigger new layout and you may see jumpy scrolling. But there's no need to force a layout pass when you configure the cell. – algal Aug 29 '15 at 16:00
  • 3
    you were right, I end up calculating the dimension on the server side so I can set up the heightConstraint before displaying the Cell. This end up fixing my problems. Thanks – fabdarice Sep 02 '15 at 23:23
  • 1
    Dude, your code would be amazing right now :( Can you put it on a gist or something? – ChainFuse Jun 28 '16 at 04:06
  • Where exactly can I set the height of my image in a way that the cell calculates its own height? Currently I am adding the constraint in the hieghtForRow method but this does not work.And I guess in the cellForRow method it would be too late. – Robin Ellerkmann Aug 18 '16 at 09:06
  • 1
    @RobinEllerkmann You need to configure the table view to use automatic row height calculation. Then it will defer to the size the table view cell requires via AL constraints. Then you need to configure your cell to require a size that is implied by the image view it contains. Then in code you need to make sure that the the size it's requiring is not the intrinsic size of the image, but the appropriately aspect-resized size. So there's a chain of connections here. image size -> imageView size -> cell size -> table view resizing. – algal Aug 18 '16 at 19:46
  • @algal thanks for your help, I figured it out. First I thought I had to set the cells' constraint in the heightForRow method but then I found my mistake and now everything works fine. – Robin Ellerkmann Aug 19 '16 at 12:28
  • You should write a blog post about this @algal! – ChainFuse Sep 02 '16 at 18:15
  • @algal I've followed all steps, and still the cell itself does not resize upon the async call (using sdwebimage) returning with the new image + new aspect ratio constraint added, like in the answer: https://stackoverflow.com/a/42202572/129202 any ideas? Only when I scroll said cell out, then into visible view again it will have to correct size. – Jonny Jan 25 '18 at 08:58
  • @Jonny I'd bet your AL constraints are wrong. There are two key points. First, you need to place the image view in the table cell's contentView with AL constraints on all edges, so that the image view can express its desired size to its superview. Second, make sure the image view has `translatesAutoResizingMaskToConstraints = false` if you created it in code. Third, make sure the image view itself correctly updates its constraints with new content, as described here: https://stackoverflow.com/a/42654375/577888 . – algal Jan 25 '18 at 15:46
  • I did all that. Thanks anyway. For some reason it wouldn't work with sdwebimage and when 1) no image was initially cached or 2) the image was only cached to disk. If the image was cached to memory it worked. I wrote some lengthy code around it to check for the cache type and if not memory it would retrieve the image again, then do a reload of that cell from the table view. I'm not sure but I think there was a problem with the exact server I was using and sdwebimage... – Jonny Jan 25 '18 at 17:44
3

Swift 4.0

You can use simple code to manage the height of row according to the height of imageView in tableView cell.

   func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        if let height = self.rowHeights[indexPath.row]{
            return height
        }else{
            return defaultHeight
        }
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return imageArray.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let identifier = "editImageCell"
        let customCell : EdiPostImageCell = (self.imagesTableView.dequeueReusableCell(withIdentifier: identifier)) as! EdiPostImageCell
        let aspectRatio = (imageArray[indexPath.row]).size.height/(imageArray[indexPath.row]).size.width
        customCell.editlImageView.image = imageArray[indexPath.row]
        let imageHeight = self.view.frame.width*aspectRatio
        self.imagesTableView.beginUpdates()
        self.rowHeights[indexPath.row] = imageHeight
        tableView.endUpdates()
    }
  • well, that definitely wouldn't work bc you're not returning cell type, and also you forgot about tableView.beingUpdates(), not to mention any other lacks... – klapinski Nov 16 '18 at 09:14
1

if you are using sdwebimage library then you can manage images after downloading and cache images with the same height and width that are showing on

                guard let myImageWidth = downloadedImage?.size.width else { return }
                guard let myImageHeight = downloadedImage?.size.height else { return }
                
                let myViewWidth = cell.imageViewPost.frame.size.width
                let ratio = myViewWidth / myImageWidth
                let scaledHeight = myImageHeight * ratio
                
                self.tableView.beginUpdates()
            
                let transformer = SDImageResizingTransformer(size: CGSize(width: myViewWidth, height: scaledHeight), scaleMode: .fill)
                cell.imageViewPost.sd_setImage(with: url, placeholderImage: nil, context: [.imageTransformer: transformer])
                cell.imageViewPost.heightAnchor.constraint(equalToConstant: scaledHeight).isActive = true
                self.tableView.endUpdates()
0

Remove the height constraint from your imageView. Choose the contentMode of image view that starts with "aspect" eg: aspect fill or aspect fit (as it fits according to your requirements).

NOTE: You also need to set the height of the row so that the imageView fits in it. Use

heightForRowAtIndexPath

to set custom row height.

Rohit Kumar
  • 877
  • 6
  • 20
  • 3
    Hi I've tried your method but it doesn't work. The reason is because the Haneke's function "self.selfieImageView.hnk_setImageFromURL" I'm using to load the image is async so I don't really get the height of the Cell before it's loaded. – fabdarice Aug 29 '15 at 08:56
-1
  1. make sure you have set top-bottom constraints in storyboard.
  2. save the thumbnail/image as data to local storage (I'm using core data)
  3. using the thumb image, calculate the aspect of the image
  4. customise the row height as you want in heightForRowAt method.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        let entity = self.fetchedResultController.object(at: indexPath as IndexPath) as! Message
        var newh:CGFloat = 100.00

        if let fileThumbnails =  entity.file_thumbnails as? NSArray{
            if fileThumbnails.count != 0{
                fileThumbnails.map { (thumbNail) in
                    newh = self.getImageHeight(image:UIImage(data: thumbNail as! Data)! , h: UIImage(data: thumbNail as! Data)!.size.height , w: UIImage(data: thumbNail as! Data)!.size.width)
                }

            }

        }

        if entity.fileStatus == "DeletedMedia" {
            newh = 100
        }
        if entity.fileStatus == nil{
            newh = 0.0
        }
        print ("newH " , newh)
        return newh
    }
    func getImageHeight(image : UIImage, h : CGFloat, w : CGFloat) -> CGFloat {
        let aspect = image.size.width / image.size.height
        var newH = (messagesTV.frame.size.width) / aspect
        // customise as you want in newH
        if(newH > 500){
            newH = 500
           //maximum height 500
        }
        if(newH < 100){
            newH = 100
            //minimum height = 100
        }
        return newH
    }

if the thumb image is deleted in local storage then a placeholder image will be shown. you can customise the newHvariable to get the desired output

if you want to change the params like a fixed width then change it in the newH variable in getImageHeight() method. eg: var newH = (messagesTV.frame.size.width * 0.6) / aspect I will get height with aspect based on the width I mentioned here in this example, my fixed width is 60% width of the screen. hope this helps!