View with 2 labels of dynamic height

Hello,

I try to do the same as the UIListContentConfiguration. Because I want to have a UITableViewCell with an image at beginning, then 2 Labels and an image at the end (accessoryView/Type should also be usable). I also want to have the dynamic behavior of the two labels, that if one or both of them exceeds a limit, that they are put under each under. But I cannot use the UIListContentConfiguration.valueCell, because of the extra image at the end.

So I have tried to make a TextWrapper as an UIView, which contains only the two UILabels and the TextWrapper should take care of the dynamic height of the UILabels and put them side to side or under each other.

But here in this post Im only concentrating on the issue with the labels under each other, because I have managed it to get it working, that I have two sets of Constraints and switch the activeStatus of the constraints, depending on the size of the two labels. But currently only the thing with the labels under each under makes problems.

Following approaches I have tried for the TextWrapper:

  • Using only constraints in this Wrapper (results in ambiguous constraints)
  • Used combinations of constraints + intrinsicContentSize (failed because it seems that invalidateIntrinsicContentSize doesn't work for me)

Approach with constraints only

Following code snippet results in ambiguous vertical position and height. Because I have 3 constraints (those with the defaultHigh priorities).

class TextWrapper: UIView {
    let textLabel = UILabel()
    let detailLabel = UILabel()
    
    init() {
        super.init(frame: .zero)
        
        self.addSubview(self.textLabel)
        self.addSubview(self.detailLabel)
        
        self.textLabel.numberOfLines = 0
        self.detailLabel.numberOfLines = 0
        
        self.directionalLayoutMargins = .init(top: 8, leading: 16, bottom: 8, trailing: 8)
        
        self.translatesAutoresizingMaskIntoConstraints = false
        self.textLabel.translatesAutoresizingMaskIntoConstraints = false
        self.detailLabel.translatesAutoresizingMaskIntoConstraints = false
        
        // Content Size
        self.textLabel.setContentCompressionResistancePriority(.required, for: .vertical)
        self.detailLabel.setContentCompressionResistancePriority(.required, for: .vertical)
        
        // Constraints
        self.textLabel.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor).isActive = true
        self.textLabel.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor).constraint(with: .defaultHigh)
        self.textLabel.widthAnchor.constraint(lessThanOrEqualTo: self.layoutMarginsGuide.widthAnchor).isActive = true
        
        self.detailLabel.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor).isActive = true
        self.detailLabel.topAnchor.constraint(equalTo: self.textLabel.bottomAnchor, constant: 2).constraint(with: .defaultHigh)
        self.detailLabel.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor).constraint(with: .defaultHigh)
        self.detailLabel.widthAnchor.constraint(lessThanOrEqualTo: self.layoutMarginsGuide.widthAnchor).isActive = true
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

Approach with intrinsicContentSize

Pretty similar to the above, with only difference that I invalidate the intrinsicContentSize in the layoutSubviews, because at the first call of the intrinsicContentSize the width of the View is zero. I also tried different things like setNeedsLayout with layoutIfNeeded but nothing really works. After I invalidate the intrinsicContentSize in the layoutSubviews the intrinsicContentSize is called with the correct width of the View and calculates the correct height, but the TableView doesn't update the height accordingly.

class TextWrapper: UIView {
    let textLabel = UILabel()
    let detailLabel = UILabel()
    
    init() {
        super.init(frame: .zero)
        
        self.addSubview(self.textLabel)
        self.addSubview(self.detailLabel)
        
        self.textLabel.numberOfLines = 0
        self.detailLabel.numberOfLines = 0
        
        self.directionalLayoutMargins = .init(top: 8, leading: 16, bottom: 8, trailing: 8)
        
        self.translatesAutoresizingMaskIntoConstraints = false
        self.textLabel.translatesAutoresizingMaskIntoConstraints = false
        self.detailLabel.translatesAutoresizingMaskIntoConstraints = false
        
        // Content Size
        self.textLabel.setContentCompressionResistancePriority(.required, for: .vertical)
        self.detailLabel.setContentCompressionResistancePriority(.required, for: .vertical)
        
        // Constraints
        self.textLabel.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor).isActive = true
        self.textLabel.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor).constraint(with: .defaultHigh)
        self.textLabel.widthAnchor.constraint(lessThanOrEqualTo: self.layoutMarginsGuide.widthAnchor).isActive = true
        
        self.detailLabel.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor).isActive = true
        self.detailLabel.topAnchor.constraint(equalTo: self.textLabel.bottomAnchor, constant: 2).constraint(with: .defaultHigh)
        self.detailLabel.widthAnchor.constraint(lessThanOrEqualTo: self.layoutMarginsGuide.widthAnchor).isActive = true
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override var intrinsicContentSize: CGSize {
        let maxLabelWidth = self.bounds.width
        guard maxLabelWidth > 0 else {
            // The first time it has a width of 0, so we are giving a default height in this case, to dont produce a error in TableView
            return CGSize(width: UIView.noIntrinsicMetric, height: 44)
        }
        
        let textLabelSize = self.textLabel.sizeThatFits(CGSize(width: maxLabelWidth, height: .greatestFiniteMagnitude))
        let detailLabelSize = self.detailLabel.sizeThatFits(CGSize(width: maxLabelWidth, height: .greatestFiniteMagnitude))
        
        let totalHeight = textLabelSize.height + detailLabelSize.height + 16
        return CGSize(width: UIView.noIntrinsicMetric, height: totalHeight)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        self.invalidateIntrinsicContentSize()
    }

I also tried to use the intrinsicContentSize with only layoutSubviews, but this haven't worked also.

Does anybody have ran into such issues? And know how to fix that?

IMHO, even though it is possible to do this with constraints, I fear it is too complex.

If I understand what you want to achieve, I would position the labels manually in their View.

And call needsToUpdate when the label length changes.

There is a similar example of a custom cell like this in the Modern Collection Views sample code here: https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views

See the CustomListCell class in the CustomCellListViewController.swift source file of that project.

The key thing that particular example demonstrates is how you can utilize and embed a UIListContentView manually inside of a custom cell, in order to add additional view(s) alongside it (such as an extra UIImageView or UILabel).

If you really want to make this reusable and composable just like UIListContentConfiguration, you can even create a custom UIContentConfiguration and paired UIContentView, where inside the custom UIContentView you internally embed a UIListContentView as an implementation detail.

View with 2 labels of dynamic height
 
 
Q