I have a UILabel that sits inside a UICollectionViewCell. The UILabel is multiple lines and may be different sizes based off of the text supplied. The cell sits in a collectionview with a simple UICollectionViewCompositionalLayout.
The label is resizing correctly, but the text in the label sometimes draws incorrectly, with the text not actually starting at the top, which results in the label being cut off.
If you take a look at my screenshot above, you can see that it says "Fifth person did something to " but was cut off. You can see it doesn't actually start drawing from the top, unlike the other cells (I've put an outline around the UILabel). The label is actually being sized correctly, it's just the text that's off.
Depending on which simulator I'm using, this cutoff might happen in the cells with longer text, or the cells with shorter text. The only commonality is that when I use text of uniform size, I don't see it happening, but when I have a divergent size it happens.
I've tried just about every configuration of layoutSubviews(), sizeToFit(), setLayoutIfNeeded() that I can think of. I've tried using a UITextView which interestingly also cuts off.
I've made a minimum reproducible example, but the code for the cell is here:
class NotificationsCell: UICollectionViewCell {
// Structure
var mainView = UIStackView(.vertical)
var mainStack = UIStackView(.horizontal, spacing: 5)
// Left side
var leftArea = UIView()
var imageView = ImageBall(fontSize: 16)
// Right Side
var rightArea = UIView()
var rightStack = UIStackView(.vertical, spacing: 5)
var titleLabel = UILabel()
// Data
var data: UserNotification? {
didSet {
self.updateContent()
}
}
// MARK: - Init
override init(frame: CGRect) {
super.init(frame: frame)
//print("RecipeCell - init")
setUpViews()
setUpUI()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
//print("RecipeCell - init")
setUpViews()
setUpUI()
}
func setUpViews() {
// Main
self.constrain(mainView, using: .edges, padding: 2, debugName: "mainView -> cell")
mainView.add([mainStack, Spacer(1, .vertical, color: .grayColor)])
mainStack.add([leftArea, rightArea])
// Right
rightArea.constrain(rightStack, using: .scale, widthScale: 0.9, heightScale: 0.8)
rightStack.add([titleLabel])
// Left
leftArea.constrain(imageView, using: .scale, widthScale: 0.9, except: [.height, .centerY])
imageView.widthAnchor.constraint(equalToConstant: 40).isActive = true
imageView.topAnchor.constraint(equalTo: rightStack.topAnchor).isActive = true
}
func setUpUI() {
titleLabel.lineBreakMode = .byWordWrapping
titleLabel.textAlignment = .left
titleLabel.numberOfLines = -1
titleLabel.layer.borderWidth = 1
}
func updateContent() {
guard let update = self.data else {
print("NotificationsCell - Update Content - No data")
return
}
self.titleLabel.attributedText = update.generateNotificationString()
self.imageView.set(color: .lightBluePrimary)
self.imageView.set(labelText: "DA")
self.layoutSubviews()
}
override func prepareForReuse() {
titleLabel.text = ""
}
}
and the code for the view / collectionview is here:
class NotificationsView: UIView {
// View Controller
weak var viewController: NotificationsViewController!
// Main Stack
var mainStack = UIStackView()
// Title View
var titleArea = UIStackView(.horizontal)
var titleTextView: UITextView = {
let textView = UITextView()
textView.textColor = .orangeColor
textView.font = .systemFont(ofSize: 40, weight: .bold)
textView.isScrollEnabled = false
textView.isEditable = false
return textView
}()
// Body
var bodyView = UIStackView(.vertical)
// Collection View
var collectionView: UICollectionView?
var collectionViewLayout: UICollectionViewLayout?
init() {
super.init(frame: .zero)
setUpViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: Setup
extension NotificationsView {
private func setUpViews() {
self.backgroundColor = .white
// Mainstack
constrain(mainStack, using: .edges, padding: 15, safeAreaLayout: true, debugName: "NotificationsView - mainStack")
mainStack.constrain(titleArea, using: .edges, except: [.bottom])
mainStack.constrain(bodyView, using: .edges, except: [.top])
// Title Area
titleArea.bottomAnchor.constraint(equalTo: bodyView.topAnchor).isActive = true
titleArea.add([titleTextView, UIView()])
titleTextView.text = "Notifications"
// Body View
setUpCollectionView()
bodyView.constrain(collectionView!, using: .edges)
}
// Collection View
private func setUpCollectionView() {
self.collectionViewLayout = createCollectionViewLayout()
self.collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout!)
collectionView!.register(NotificationsCell.self, forCellWithReuseIdentifier: String(describing: NotificationsCell.self))
}
private func createCollectionViewLayout() -> UICollectionViewLayout {
let padding: CGFloat = 0
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(50))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize,
subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 0
section.contentInsets = .init(top: 0, leading: 15, bottom: padding, trailing: 15)
return UICollectionViewCompositionalLayout(section: section)
}
}
If you do look at the MRP, try running it in the iPhone SE 3rd generation simulator to duplicate. You can try adjusting the length of the notifications in the NotificationManager. The MRP is here: https://drive.google.com/file/d/1GokqMkh-cTyN1nBxmlnyJqc_OKl48SGL/view?usp=sharing
The issue is happening due to setting an incorrect height for titleLabel
. Something prevents it from growing vertically. This line is particularly suspicious:
rightArea.constrain(rightStack, using: .scale, widthScale: 0.9, heightScale: 0.8)
Your bug disappears if set heightScale
to 1.0.
My understanding is that by setting 0.8 multiplier for height, an ambiguous situation is created. This can be satisfied by autolayout engine either by setting the height of titleLabel to 0.8 of the current cell's height or by setting the cell's height to 1/0.8 = 1.25 times of the fully grown titleLabel's height.
My suggestion would be to avoid using multipliers for height in auto-sizing cells. Instead, use a fixed padding + growing label + fixed padding approach.