4

I have a custom cell with dynamic height inside a XIB file which shows a post with a message and an image. Because the image will be loaded asynchronously from my API the aspect ratio of the image will be send with the resource. Inside the updateConstraints method the aspect ratio constraint with the proper multiplier will be set.

The XIB file:

My XIB file

The cell class:

//
//  PostTableViewCell.swift
//  Lome
//
//  Created by Tobias Feistmantl on 10/09/15.
//  Copyright (c) 2015 Tobias Feistmantl. All rights reserved.
//

import UIKit
import Alamofire

class PostTableViewCell: UITableViewCell {

    @IBOutlet weak var userProfileImageView: TFImageView!
    @IBOutlet weak var usersNameLabel: UILabel!
    @IBOutlet weak var usernameLabel: UILabel!
    @IBOutlet weak var userProfileButton: TFCellButton!
    @IBOutlet weak var distanceLabel: UILabel!
    @IBOutlet weak var messageLabel: UILabel!
    @IBOutlet weak var postImageView: UIImageView!
    @IBOutlet weak var likeCountLabel: UILabel!
    @IBOutlet weak var timestampLabel: UILabel!
    @IBOutlet weak var likeButton: UIButton!

    @IBOutlet weak var constraintBetweenMessageLabelAndPostImageView: NSLayoutConstraint!

    var post: Post! {
        didSet {
            post.author.profileImage(version: .Thumbnail) { image, _ in
                self.userProfileImageView.image = image
            }

            if let name = post.author.fullName {
                usersNameLabel.text = name
                usernameLabel.text = post.author.username
            } else {
                usersNameLabel.text = post.author.username
                usernameLabel.hidden = true
            }

            if let attributedMessage = post.attributedMessage {
                messageLabel.attributedText = attributedMessage
                messageLabel.hidden = false
                constraintBetweenMessageLabelAndPostImageView.constant = 15
            } else {
                messageLabel.hidden = true
                messageLabel.attributedText = nil
                constraintBetweenMessageLabelAndPostImageView.constant = 0
            }

            post.image { image, _ in
                if let image = image {
                    self.postImageView.image = image
                    self.setNeedsUpdateConstraints()
                }
            }

            timestampLabel.text = "Posted \(post.createdAt.timeAgoSinceNow())"
            distanceLabel.text = post.distanceText
            likeCountLabel.text = "\(post.likesCount) Likes"
            likeButton.setImage(post.likeButtonImage, forState: .Normal)
        }
    }

    override func updateConstraints() {
        if let aspectRatio = post.imageAspectRatio {
            postImageAspectConstraint = NSLayoutConstraint(item: postImageView, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: postImageView, attribute: NSLayoutAttribute.Height, multiplier: CGFloat(aspectRatio), constant: 0)
        }

        super.updateConstraints()
    }

    func setupUserProfileButton(indexPath: NSIndexPath, viewController: UIViewController) {
        userProfileButton.indexPath = indexPath
        userProfileButton.addTarget(viewController, action: "userProfileButtonDidTouch:", forControlEvents: .TouchUpInside)
    }

    var postImageAspectConstraint: NSLayoutConstraint? {
        didSet {
            if let oldValue = oldValue {
                postImageView.removeConstraint(oldValue)
            }
            if let postImageAspectConstraint = postImageAspectConstraint {
                postImageView.addConstraint(postImageAspectConstraint)
            }
        }
    }

    override func prepareForReuse() {
        super.prepareForReuse()

        postImageAspectConstraint = nil
    }

}

The cellForRowAtIndexPath method:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("postCell", forIndexPath: indexPath) as! PostTableViewCell

    cell.post = posts[indexPath.row]
    cell.setupUserProfileButton(indexPath, viewController: self)

    return cell
}

On the app start everything works as expected, but if I scoll a little bit, the contraints begin to break and the aspect of the images aren't right anymore or some other strange things like below:

Broken cell

Errors:

2015-09-28 11:58:27.546 Lome[32812:528936] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) 
(
    "<NSLayoutConstraint:0x7f8a008bf6f0 V:[Lome.TFImageView:0x7f8a008bf470(35)]>",
    "<NSLayoutConstraint:0x7f89f9fbc490 V:|-(15)-[Lome.TFCellButton:0x7f8a0087fde0]   (Names: '|':UITableViewCellContentView:0x7f8a008bf270 )>",
    "<NSLayoutConstraint:0x7f8a00831520 Lome.TFCellButton:0x7f8a0087fde0.top == Lome.TFImageView:0x7f8a008bf470.top>",
    "<NSLayoutConstraint:0x7f8a008315c0 Lome.TFCellButton:0x7f8a0087fde0.bottom == Lome.TFImageView:0x7f8a008bf470.bottom>",
    "<NSLayoutConstraint:0x7f8a00831750 V:[Lome.TFCellButton:0x7f8a0087fde0]-(18)-[UILabel:0x7f8a008806d0'Water']>",
    "<NSLayoutConstraint:0x7f8a00831840 H:|-(0)-[UIImageView:0x7f8a00830910]   (Names: '|':UITableViewCellContentView:0x7f8a008bf270 )>",
    "<NSLayoutConstraint:0x7f8a00831890 H:[UIImageView:0x7f8a00830910]-(0)-|   (Names: '|':UITableViewCellContentView:0x7f8a008bf270 )>",
    "<NSLayoutConstraint:0x7f8a008318e0 V:[UILabel:0x7f8a008806d0'Water']-(15)-[UIImageView:0x7f8a00830910]>",
    "<NSLayoutConstraint:0x7f8a00831930 V:[UIImageView:0x7f8a00830910]-(20)-[UILabel:0x7f8a00830b90'0 Likes']>",
    "<NSLayoutConstraint:0x7f8a00831980 V:[UILabel:0x7f8a00830b90'0 Likes']-(20)-|   (Names: '|':UITableViewCellContentView:0x7f8a008bf270 )>",
    "<NSLayoutConstraint:0x7f8a00860870 UIImageView:0x7f8a00830910.width == 0.6672*UIImageView:0x7f8a00830910.height>",
    "<NSLayoutConstraint:0x7f8a0084e990 'UIView-Encapsulated-Layout-Height' V:[UITableViewCellContentView:0x7f8a008bf270(611)]>",
    "<NSLayoutConstraint:0x7f8a0084f930 'UIView-Encapsulated-Layout-Width' H:[UITableViewCellContentView:0x7f8a008bf270(375)]>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x7f8a008bf6f0 V:[Lome.TFImageView:0x7f8a008bf470(35)]>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.
2015-09-28 11:58:27.547 Lome[32812:528936] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) 
(
    "<NSLayoutConstraint:0x7f89f9fbc490 V:|-(15)-[Lome.TFCellButton:0x7f8a0087fde0]   (Names: '|':UITableViewCellContentView:0x7f8a008bf270 )>",
    "<NSLayoutConstraint:0x7f8a00831520 Lome.TFCellButton:0x7f8a0087fde0.top == Lome.TFImageView:0x7f8a008bf470.top>",
    "<NSLayoutConstraint:0x7f8a008315c0 Lome.TFCellButton:0x7f8a0087fde0.bottom == Lome.TFImageView:0x7f8a008bf470.bottom>",
    "<NSLayoutConstraint:0x7f8a00831750 V:[Lome.TFCellButton:0x7f8a0087fde0]-(18)-[UILabel:0x7f8a008806d0'Water']>",
    "<NSLayoutConstraint:0x7f8a00831840 H:|-(0)-[UIImageView:0x7f8a00830910]   (Names: '|':UITableViewCellContentView:0x7f8a008bf270 )>",
    "<NSLayoutConstraint:0x7f8a00831890 H:[UIImageView:0x7f8a00830910]-(0)-|   (Names: '|':UITableViewCellContentView:0x7f8a008bf270 )>",
    "<NSLayoutConstraint:0x7f8a008318e0 V:[UILabel:0x7f8a008806d0'Water']-(15)-[UIImageView:0x7f8a00830910]>",
    "<NSLayoutConstraint:0x7f8a00831930 V:[UIImageView:0x7f8a00830910]-(20)-[UILabel:0x7f8a00830b90'0 Likes']>",
    "<NSLayoutConstraint:0x7f8a00831980 V:[UILabel:0x7f8a00830b90'0 Likes']-(20)-|   (Names: '|':UITableViewCellContentView:0x7f8a008bf270 )>",
    "<NSLayoutConstraint:0x7f8a00860870 UIImageView:0x7f8a00830910.width == 0.6672*UIImageView:0x7f8a00830910.height>",
    "<NSLayoutConstraint:0x7f8a0084e990 'UIView-Encapsulated-Layout-Height' V:[UITableViewCellContentView:0x7f8a008bf270(611)]>",
    "<NSLayoutConstraint:0x7f8a0084f930 'UIView-Encapsulated-Layout-Width' H:[UITableViewCellContentView:0x7f8a008bf270(375)]>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x7f8a00860870 UIImageView:0x7f8a00830910.width == 0.6672*UIImageView:0x7f8a00830910.height>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.

I think it has something to do with the reusing of the cells but as you can see the constraint will be removed in the prepareForReuse method.

I'm currently pretty frustrated because I'm trying to solve this problem since a few days and don't find a solution.

I hope somebody have a solution.

Thanks in advance.

Tobias
  • 4,523
  • 2
  • 20
  • 40
  • Can you try to set the height of your image view rather than the width in `updateConstraints:`. From a first look, this seems to be an ambiguous situation as you already have pinned the `ImageView` with horizontal layouts with 0 spacing and then trying to change the width as per height. – Gandalf Sep 28 '15 at 10:35
  • This is an aspect ratio constraint not a width constraint. The height of UIImageView is 0.6672 times bigger than the width. However if i set a fixed height in IB and don't add this aspect ratio constraint it works but I have always the same height and a false aspect ratio because every image is different. – Tobias Sep 28 '15 at 10:55
  • 1
    I know that you have applied aspect ratio constraint. I think you misunderstood what i was referring to. In your aspect ration constraint, the `first item` is `width`. That means constraint will attempt to make the changes in `width` not to `height` of that view. So if you reverse the first and second item from it and inverse the aspect ratio in multiplier argument, you will have the same constraint but with a varying height as per image aspect ratio. – Gandalf Sep 28 '15 at 11:20
  • THANK YOU!! It solved my problem! Write this comment as answer and i will mark it as the correct answer. – Tobias Sep 28 '15 at 11:33
  • Thanks for the hint to override updateLayoutConstraints! This saved my life!!! Nice code btw :-) – blackjacx Oct 04 '17 at 17:16
  • did you still had to call beingupdates on tableview? how did you made sure the tableview updated accordingly? – perrohunter Jan 27 '19 at 08:47

3 Answers3

1

This issue is caused by 2 conflicting constraints applied to the ImageView. If you look at the debugger output

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x7f8a00860870 UIImageView:0x7f8a00830910.width == 0.6672*UIImageView:0x7f8a00830910.height>

You already have horizontal constraint to the ImageView pinned to the superview with 0 spacing.

Now while updating the aspect ratio constraint you are trying to set the width(by keeping width as first item) of ImageView as a ratio of it's height, which will lead to conflict with it's Leading/Trailing constraint. You need to set the height of this ImageView by interchanging the first and second items and inverting the aspect ratio factor.

postImageAspectConstraint = NSLayoutConstraint(item: postImageView, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: postImageView, attribute: NSLayoutAttribute.Width, multiplier: 1/CGFloat(aspectRatio), constant: 0)
Gandalf
  • 2,399
  • 2
  • 15
  • 19
0

Once you get the image height, you should update your array, e.g. rowHeightArray, which keeps track of all the rowHeights, e.g.

[rowHeightArray replaceObjectAtIndex:indexPath.row withObject:[NSNumber numberWithFloat:imageHeight]];

Then call

[aTableView reloadRowsAtIndexPaths:indexPath withRowAnimation:YES];

It will call

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return [[rowHeightArray objectAtIndex:indexPath.row] floatValue];   
}

where you return the object at the indexPath with the row height.

Leonardo
  • 751
  • 8
  • 15
  • The row height of the cells is set to `UITableViewAutomaticDimension`. Through the auto layout constraints the row height will be automatically defined. The problem I have is that the aspect ratio constraint gets broken after scrolling up and down. – Tobias Sep 28 '15 at 11:14
  • Did you try to add a constraint to the UIImageView ? Have the images the same width and height or at least the same ratio? If not, you should remove the previous constraint and add a new constraint or check the ScaleToFit or ScaleToFill... I do not really know the way you do. – Leonardo Sep 28 '15 at 11:22
0

To remove constraints, check:

[aView setTranslatesAutoresizingMaskIntoConstraints:NO];
[aView removeConstraints:view.constraints];

or

for(NSLayoutConstraint *constraint in aView.constraints){
    [aView.view removeConstraint:constraint];
}
Leonardo
  • 751
  • 8
  • 15