Search code examples
iosswiftautolayoutuilabeluicollectionviewcell

UILabel not drawing text properly in UICollectionViewCell


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.

enter image description here

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


Solution

  • 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.