Reputation: 3090
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:
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:
Upvotes: 0
Views: 499
Reputation: 77691
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).
Upvotes: 1
Reputation: 80
You add subviews a lot of times, but it's REUSABLE. Don't forget about it. Add next code before .addSubview(....
contentView.subviews.forEach { $0.removeFromSuperview() }
Or change views values only, don't add it each time
Upvotes: -2
Reputation: 6324
I changed your code setting messageBuble
constraint relative to the cell instead of the content view:
messageBubble.topAnchor.constraint(equalTo: self.topAnchor, constant: 10),
messageBubble.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -10)
the just calling layoutIfNeeded()
:
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.layoutIfNeeded()
return discussionChatMessageCell
}
Upvotes: 0