iosswiftuitableviewuitableviewautomaticdimension

UITableView Cell with dynamic height and width not getting reused properly


I have a UITableView cell with dynamic height and width. Initially, it works properly, but when reusing an old cell the constraints are not set correctly. I am deactivating all the old constrains and activating them again. I have also called setNeedsLayout() and layoutIfNeeded(). But it's not helping.

Automatic height setup: (I think this is causing an issue)

discussionTableView.rowHeight = UITableViewAutomaticDimension
discussionTableView.estimatedRowHeight = 10

My table view cell:

class DiscussionChatMessageCell: UITableViewCell {
    
    private let messageLabel: UILabel
    private let senderNameLabel: UILabel
    private let messageBubble: UIView
    
    let screenWidth: CGFloat
    
    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        messageLabel = UILabel()
        senderNameLabel = UILabel()
        screenWidth = UIScreen.main.bounds.size.width
        messageBubble = UIView()
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        
        //        self.contentView.backgroundColor = .clear
        
        self.contentView.addSubview(messageBubble)
        messageBubble.translatesAutoresizingMaskIntoConstraints = false
        
        messageBubble.addSubview(senderNameLabel)
        senderNameLabel.translatesAutoresizingMaskIntoConstraints = false
        senderNameLabel.numberOfLines = 0
        senderNameLabel.lineBreakMode = .byCharWrapping
        senderNameLabel.font = UIFont.boldSystemFont(ofSize: 15)
        senderNameLabel.textColor = .white
        
        messageBubble.addSubview(messageLabel)
        messageLabel.translatesAutoresizingMaskIntoConstraints = false
        messageLabel.numberOfLines = 0
        messageLabel.lineBreakMode = .byWordWrapping
        messageLabel.font = UIFont.systemFont(ofSize: 13)
        messageLabel.textColor = .white
        
        NSLayoutConstraint.activate([
            messageBubble.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 10),
            messageBubble.bottomAnchor.constraint(equalTo:  self.contentView.bottomAnchor, constant: -10),
            messageBubble.widthAnchor.constraint(lessThanOrEqualToConstant: screenWidth - 100),
            
            senderNameLabel.topAnchor.constraint(equalTo: messageBubble.topAnchor, constant: 10),
            senderNameLabel.leadingAnchor.constraint(equalTo: messageBubble.leadingAnchor, constant: 10),
            senderNameLabel.trailingAnchor.constraint(equalTo: messageBubble.trailingAnchor, constant: -10),
            
            messageLabel.topAnchor.constraint(equalTo: senderNameLabel.bottomAnchor, constant: 10),
            messageLabel.leadingAnchor.constraint(equalTo: messageBubble.leadingAnchor, constant: 10),
            messageLabel.trailingAnchor.constraint(equalTo: messageBubble.trailingAnchor, constant: -10),
            messageLabel.bottomAnchor.constraint(equalTo: messageBubble.bottomAnchor, constant: -10),
        ])
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configureCell(message: String, isSender: Bool) {
        senderNameLabel.text = "Default Sender"
        messageLabel.text = message
        
        for constraint in messageBubble.constraints {
            //            messageBubble.removeConstraint(constraint)
            constraint.isActive = false
        }
        for constraint in messageLabel.constraints {
            //            messageLabel.removeConstraint(constraint)
            constraint.isActive = false
        }
        for constraint in senderNameLabel.constraints {
            //senderNameLabel.removeConstraint(constraint)
            constraint.isActive = false
        }
        
        NSLayoutConstraint.deactivate([
            messageBubble.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -10),
            messageBubble.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 10)
        ])
        NSLayoutConstraint.activate([
            messageBubble.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 10),
            messageBubble.bottomAnchor.constraint(equalTo:  self.contentView.bottomAnchor, constant: -10),
            messageBubble.widthAnchor.constraint(lessThanOrEqualToConstant: screenWidth - 100),
            
            senderNameLabel.topAnchor.constraint(equalTo: messageBubble.topAnchor, constant: 10),
            senderNameLabel.leadingAnchor.constraint(equalTo: messageBubble.leadingAnchor, constant: 10),
            senderNameLabel.trailingAnchor.constraint(equalTo: messageBubble.trailingAnchor, constant: -10),
            
            messageLabel.topAnchor.constraint(equalTo: senderNameLabel.bottomAnchor, constant: 10),
            messageLabel.leadingAnchor.constraint(equalTo: messageBubble.leadingAnchor, constant: 10),
            messageLabel.trailingAnchor.constraint(equalTo: messageBubble.trailingAnchor, constant: -10),
            messageLabel.bottomAnchor.constraint(equalTo: messageBubble.bottomAnchor, constant: -10),
        ])
        
        messageBubble.backgroundColor = isSender ? accentColor : .gray
        if isSender {
            
            NSLayoutConstraint.activate([
                messageBubble.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -10)
            ])
            
            //            let corners: UIRectCorner  = [.topLeft, .topRight, .bottomLeft]
            //            roundCorners(corners: corners, isSender: isSender)
            
        } else {
            NSLayoutConstraint.activate([
                messageBubble.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 10)
            ])
            
            //            let corners: UIRectCorner  = [.topLeft, .topRight, .bottomRight]
            //            roundCorners(corners: corners, isSender: isSender)
        }
    }

Reusing cell:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let discussionChatMessageCell = tableView.dequeueReusableCell(withIdentifier: discussionChatId, for: indexPath) as? DiscussionChatMessageCell else { return UITableViewCell()}
        
        discussionChatMessageCell.configureCell(message: messages[indexPath.row], isSender: isSender[indexPath.row])

        discussionChatMessageCell.setNeedsLayout()
        discussionChatMessageCell.layoutIfNeeded()
        
        return discussionChatMessageCell
    }

Before reusing cell:

After reusing cell:

Edit

When using UITextView instead of UILabel for messageLabel, the constraints work very differently and the table view takes 2-3 seconds to load.

Changed settings for textView

// messageLabel.numberOfLines = 0
// messageLabel.lineBreakMode = .byWordWrapping
messageLabel.isEditable = false
messageLabel.dataDetectorTypes = .all
messageLabel.textContainer.lineBreakMode = .byWordWrapping
messageLabel.setContentCompressionResistancePriority(.required, for: .vertical)
messageLabel.setContentHuggingPriority(.required, for: .vertical)

Output:

Here's the code for the updated cell, where I have also added a time label. So what is needed is UILable, UITextView, UILabel. And right now this is UILabel, UILabel, UILabel.

class DiscussionChatMessageCell: UITableViewCell {
    
    private let messageLabel: UILabel
    private let senderNameLabel: UILabel
    private let messageSentTimeLabel: UILabel
    private let messageBubble: UIView
    
    private var bubbleLeadingConstraint: NSLayoutConstraint!
    private var bubbleTrailingConstraint: NSLayoutConstraint!
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        
        messageLabel = UILabel()
        senderNameLabel = UILabel()
        messageSentTimeLabel = UILabel()
        messageBubble = UIView()
        
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        
        self.contentView.addSubview(messageBubble)
        messageBubble.translatesAutoresizingMaskIntoConstraints = false
        
        messageBubble.addSubview(senderNameLabel)
        senderNameLabel.translatesAutoresizingMaskIntoConstraints = false
        senderNameLabel.numberOfLines = 0
        senderNameLabel.lineBreakMode = .byCharWrapping
        senderNameLabel.font = UIFont.boldSystemFont(ofSize: 15)
        senderNameLabel.textColor = .white
        
        messageBubble.addSubview(messageLabel)
        messageLabel.translatesAutoresizingMaskIntoConstraints = false
        
        //        messageLabel.isEditable = false
        //        messageLabel.dataDetectorTypes = .all
        //        messageLabel.textContainer.lineBreakMode = .byWordWrapping
        
        messageLabel.numberOfLines = 0
        messageLabel.lineBreakMode = .byWordWrapping
        messageLabel.font = UIFont(name: "Helvetica Neue", size: 13)!
        
        messageBubble.addSubview(messageSentTimeLabel)
        messageSentTimeLabel.translatesAutoresizingMaskIntoConstraints = false
        messageSentTimeLabel.lineBreakMode = .byCharWrapping
        messageSentTimeLabel.numberOfLines = 0
        messageSentTimeLabel.font = UIFont(name: "HelveticaNeue-Italic", size: 11)!
        
        // set hugging and compression resistance for Name label
        senderNameLabel.setContentCompressionResistancePriority(.required, for: .vertical)
        senderNameLabel.setContentHuggingPriority(.required, for: .vertical)
        
        //        messageLabel.setContentCompressionResistancePriority(.required, for: .vertical)
        //        messageLabel.setContentHuggingPriority(.required, for: .vertical)
        
        // create bubble Leading and Trailing constraints
        bubbleLeadingConstraint = messageBubble.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 10)
        bubbleTrailingConstraint = messageBubble.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -10)
        
        // priority will be changed in configureCell()
        bubbleLeadingConstraint.priority = .defaultHigh
        bubbleTrailingConstraint.priority = .defaultLow
        
        NSLayoutConstraint.activate([
            
            bubbleLeadingConstraint,
            bubbleTrailingConstraint,
            
            messageBubble.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 10),
            messageBubble.bottomAnchor.constraint(equalTo:  self.contentView.bottomAnchor, constant: -10),
            
            messageBubble.widthAnchor.constraint(lessThanOrEqualTo: self.contentView.widthAnchor, constant: -100),
            
            senderNameLabel.topAnchor.constraint(equalTo: messageBubble.topAnchor, constant: 10),
            senderNameLabel.leadingAnchor.constraint(equalTo: messageBubble.leadingAnchor, constant: 10),
            senderNameLabel.trailingAnchor.constraint(equalTo: messageBubble.trailingAnchor, constant: -10),
            
            messageLabel.topAnchor.constraint(equalTo: senderNameLabel.bottomAnchor, constant: 10),
            messageLabel.leadingAnchor.constraint(equalTo: messageBubble.leadingAnchor, constant: 10),
            messageLabel.trailingAnchor.constraint(equalTo: messageBubble.trailingAnchor, constant: -10),
            messageLabel.bottomAnchor.constraint(equalTo: messageSentTimeLabel.topAnchor, constant: -10),
            
            messageSentTimeLabel.leadingAnchor.constraint(equalTo: messageBubble.leadingAnchor, constant: 10),
            messageSentTimeLabel.trailingAnchor.constraint(equalTo: messageBubble.trailingAnchor, constant: -10),
            messageSentTimeLabel.bottomAnchor.constraint(equalTo: messageBubble.bottomAnchor, constant: -10),
            
        ])
        
        // corners will have radius: 10
        messageBubble.layer.cornerRadius = 10
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configureCell(message: DiscussionMessage, isSender: Bool) {
        senderNameLabel.text = message.userName + " " + message.userCountryEmoji
        
        let date = Date(timeIntervalSince1970: message.messageTimestamp)
        
        let dayTimePeriodFormatter = DateFormatter()
        dayTimePeriodFormatter.timeZone = .current
        
        dayTimePeriodFormatter.dateFormat = "hh:mm a"
        let dateString = dayTimePeriodFormatter.string(from: date)
        
        messageLabel.text = message.message
        
        messageSentTimeLabel.text = dateString
        
        messageLabel.textColor = isSender ? .black : .white
        senderNameLabel.textColor = isSender ? .black : .white
        messageSentTimeLabel.textColor = isSender ? .black : .white
        messageSentTimeLabel.textAlignment = isSender ? .right : .left
        
        bubbleLeadingConstraint.priority = isSender ? .defaultLow : .defaultHigh
        bubbleTrailingConstraint.priority = isSender ? .defaultHigh : .defaultLow
        
        messageBubble.backgroundColor = isSender ? accentColor : .gray
        
        let senderCorners: CACornerMask = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner]
        let nonSenderCorners: CACornerMask =  [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMaxXMaxYCorner]
        
        if #available(iOS 11.0, *) {
            messageBubble.layer.maskedCorners = isSender ?
                // topLeft, topRight, bottomRight
                senderCorners
                :
                // topLeft, topRight, bottomLeft
                nonSenderCorners
        } else {
            // Fallback on earlier versions
            // All corners will be rounded
        }
    }
}

Current output with the time label added to sender name label and message label:


Solution

  • You are modifying constraints way more than you need to.

    A better approach would be to create both Leading and Trailing constraints for your "bubble" --- and change their Priority to determine which one is used.

    So, if it's a "Received" message, we set the Leading constraint Priority to High, and the Trailing constraint Priority to Low. If it's a "Sent" message, we do the opposite.

    Give this a try:

    class DiscussionChatMessageCell: UITableViewCell {
        
        let accentColor: UIColor = .systemYellow
        
        private let messageLabel: UILabel
        private let senderNameLabel: UILabel
        private let messageBubble: UIView
        
        private var bubbleLeadingConstraint: NSLayoutConstraint!
        private var bubbleTrailingConstraint: NSLayoutConstraint!
    
        // not needed
        //let screenWidth: CGFloat
    
        // wrong signature
        //override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            
            messageLabel = UILabel()
            senderNameLabel = UILabel()
            messageBubble = UIView()
    
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            
            //        self.contentView.backgroundColor = .clear
            
            self.contentView.addSubview(messageBubble)
            messageBubble.translatesAutoresizingMaskIntoConstraints = false
            
            messageBubble.addSubview(senderNameLabel)
            senderNameLabel.translatesAutoresizingMaskIntoConstraints = false
            senderNameLabel.numberOfLines = 0
            senderNameLabel.lineBreakMode = .byCharWrapping
            senderNameLabel.font = UIFont.boldSystemFont(ofSize: 15)
            senderNameLabel.textColor = .white
            
            messageBubble.addSubview(messageLabel)
            messageLabel.translatesAutoresizingMaskIntoConstraints = false
            messageLabel.numberOfLines = 0
            messageLabel.lineBreakMode = .byWordWrapping
            messageLabel.font = UIFont.systemFont(ofSize: 13)
            messageLabel.textColor = .white
            
            // set hugging and compression resistance for Name label
            senderNameLabel.setContentCompressionResistancePriority(.required, for: .vertical)
            senderNameLabel.setContentHuggingPriority(.required, for: .vertical)
            
            // create bubble Leading and Trailing constraints
            bubbleLeadingConstraint = messageBubble.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 10)
            bubbleTrailingConstraint = messageBubble.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -10)
    
            // priority will be changed in configureCell()
            bubbleLeadingConstraint.priority = .defaultHigh
            bubbleTrailingConstraint.priority = .defaultLow
            
            NSLayoutConstraint.activate([
                
                bubbleLeadingConstraint,
                bubbleTrailingConstraint,
                
                messageBubble.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 10),
                messageBubble.bottomAnchor.constraint(equalTo:  self.contentView.bottomAnchor, constant: -10),
                
                messageBubble.widthAnchor.constraint(lessThanOrEqualTo: self.contentView.widthAnchor, constant: -100),
                
                senderNameLabel.topAnchor.constraint(equalTo: messageBubble.topAnchor, constant: 10),
                senderNameLabel.leadingAnchor.constraint(equalTo: messageBubble.leadingAnchor, constant: 10),
                senderNameLabel.trailingAnchor.constraint(equalTo: messageBubble.trailingAnchor, constant: -10),
                
                messageLabel.topAnchor.constraint(equalTo: senderNameLabel.bottomAnchor, constant: 10),
                messageLabel.leadingAnchor.constraint(equalTo: messageBubble.leadingAnchor, constant: 10),
                messageLabel.trailingAnchor.constraint(equalTo: messageBubble.trailingAnchor, constant: -10),
                messageLabel.bottomAnchor.constraint(equalTo: messageBubble.bottomAnchor, constant: -10),
                
            ])
            
            // corners will have radius: 10
            messageBubble.layer.cornerRadius = 10
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        func configureCell(message: String, isSender: Bool) {
            senderNameLabel.text = "Default Sender"
            messageLabel.text = message
    
            bubbleLeadingConstraint.priority = isSender ? .defaultHigh : .defaultLow
            bubbleTrailingConstraint.priority = isSender ? .defaultLow : .defaultHigh
    
            messageBubble.backgroundColor = isSender ? accentColor : .gray
            
            messageBubble.layer.maskedCorners = isSender ?
                // topLeft, topRight, bottomRight
                [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMaxXMaxYCorner]
                :
                // topLeft, topRight, bottomLeft
                [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner]
    
        }
    
    }
    

    Side Note: neither of these lines is needed in cellForRowAt:

        //discussionChatMessageCell.setNeedsLayout()
        //discussionChatMessageCell.layoutIfNeeded()
    

    Edit - if you really want to support iOS prior to 11...

    I suggest you subclass your "BubbleView" like this:

    class BubbleView: UIView {
        var radius: CGFloat = 0
        var corners: UIRectCorner = []
        var color: UIColor = .clear
        
        lazy var shapeLayer: CAShapeLayer = self.layer as! CAShapeLayer
        
        override class var layerClass: AnyClass {
            return CAShapeLayer.self
        }
        
        override func layoutSubviews() {
            super.layoutSubviews()
            let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
            shapeLayer.path = path.cgPath
            shapeLayer.fillColor = color.cgColor
        }
    }
    

    and then use it like this:

    class DiscussionChatMessageCell: UITableViewCell {
        
        let accentColor: UIColor = .systemYellow
        
        private let messageLabel: UILabel
        private let senderNameLabel: UILabel
        
        // use custom BubbleView class instead of standard UIView
        private let messageBubble: BubbleView
        
        private var bubbleLeadingConstraint: NSLayoutConstraint!
        private var bubbleTrailingConstraint: NSLayoutConstraint!
        
        // wrong signature - I beliee as of Swift 4.2
        // 'UITableViewCellStyle' has been renamed to 'UITableViewCell.CellStyle'
        //override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            
            messageLabel = UILabel()
            senderNameLabel = UILabel()
            messageBubble = BubbleView()
            
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            
            self.contentView.addSubview(messageBubble)
            messageBubble.translatesAutoresizingMaskIntoConstraints = false
            
            messageBubble.addSubview(senderNameLabel)
            senderNameLabel.translatesAutoresizingMaskIntoConstraints = false
            senderNameLabel.numberOfLines = 0
            senderNameLabel.lineBreakMode = .byCharWrapping
            senderNameLabel.font = UIFont.boldSystemFont(ofSize: 15)
            senderNameLabel.textColor = .white
            
            messageBubble.addSubview(messageLabel)
            messageLabel.translatesAutoresizingMaskIntoConstraints = false
            messageLabel.numberOfLines = 0
            messageLabel.lineBreakMode = .byWordWrapping
            messageLabel.font = UIFont.systemFont(ofSize: 13)
            messageLabel.textColor = .white
            
            // set hugging and compression resistance for Name label
            senderNameLabel.setContentCompressionResistancePriority(.required, for: .vertical)
            senderNameLabel.setContentHuggingPriority(.required, for: .vertical)
            
            // create bubble Leading and Trailing constraints
            bubbleLeadingConstraint = messageBubble.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 10)
            bubbleTrailingConstraint = messageBubble.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -10)
            
            // priority will be changed in configureCell()
            bubbleLeadingConstraint.priority = .defaultHigh
            bubbleTrailingConstraint.priority = .defaultLow
            
            NSLayoutConstraint.activate([
                
                bubbleLeadingConstraint,
                bubbleTrailingConstraint,
                
                messageBubble.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 10),
                messageBubble.bottomAnchor.constraint(equalTo:  self.contentView.bottomAnchor, constant: -10),
                
                messageBubble.widthAnchor.constraint(lessThanOrEqualTo: self.contentView.widthAnchor, constant: -100),
                
                senderNameLabel.topAnchor.constraint(equalTo: messageBubble.topAnchor, constant: 10),
                senderNameLabel.leadingAnchor.constraint(equalTo: messageBubble.leadingAnchor, constant: 10),
                senderNameLabel.trailingAnchor.constraint(equalTo: messageBubble.trailingAnchor, constant: -10),
                
                messageLabel.topAnchor.constraint(equalTo: senderNameLabel.bottomAnchor, constant: 10),
                messageLabel.leadingAnchor.constraint(equalTo: messageBubble.leadingAnchor, constant: 10),
                messageLabel.trailingAnchor.constraint(equalTo: messageBubble.trailingAnchor, constant: -10),
                messageLabel.bottomAnchor.constraint(equalTo: messageBubble.bottomAnchor, constant: -10),
                
            ])
            
            // corners will have radius: 10
            messageBubble.radius = 10
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        func configureCell(message: String, isSender: Bool) {
            senderNameLabel.text = "Default Sender"
            messageLabel.text = message
            
            bubbleLeadingConstraint.priority = isSender ? .defaultHigh : .defaultLow
            bubbleTrailingConstraint.priority = isSender ? .defaultLow : .defaultHigh
            
            messageBubble.color = isSender ? accentColor : .gray
    
            let senderCorners: UIRectCorner = [.topLeft, .topRight, .bottomRight]
            let nonSenderCorners: UIRectCorner =  [.topLeft, .topRight, .bottomLeft]
    
            messageBubble.corners = isSender ? senderCorners : nonSenderCorners
    
        }
        
    }
    

    That will keep the "bubble" view's shape and size, even when the cell changes (such as when rotating the device).

    enter image description here

    enter image description here