6.9 C
New York
Thursday, November 28, 2024

swift – How you can use header with dynamic heigh / AutoLayout in UICollectionView in iOS 18?


Though I discovered a number of tutorials / questions on dynamically sizing cells UICollectionView I used to be not in a position to obtain the identical for the header view.

TL;DR How you can autosize a UICollectionView header? Fetching a mock header in collectionView:structure:referenceSizeForHeaderInSection crashes in iOS 18 and utilizing preferredLayoutAttributesFitting in UICollectionReusableView subclass has no impact.

The purpose:

A UICollectionReusableView subclass containing two UILabel for title and content material:

+------------------+
|        20        |      
|20 TitleLabel   20|
|        10        |
|20 ContentLabel 20|
|        20        |
+------------------+

The ContentLabel is configured to point out a number of traces. So, the header ought to auto-size based on the textual content in ContentLabel.

Answer earlier than iOS 18 crashes now

Till know I used the next code in SomeViewController to dimension the header view to its content material:

func collectionView(_ collectionView: UICollectionView, structure collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection part: Int) -> CGSize {
    // Question mock header to fetch its dimension
    let indexPath = IndexPath(row: 0, part: part)
    let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: sort, withReuseIdentifier: HeaderId, for: indexPath)

     // ...
    
    return headerView.systemLayoutSizeFitting(CGSize(width: collectionView.body.width, top: UIView.layoutFittingCompressedSize.top), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
}

This labored advantageous till compiling the venture with Xcode 16 / iOS 18. Now the app crashes since it isn’t allowed to dequeue a view manually (mentioned right here)

preferredLayoutAttributesFitting

I discovered a number of sources, indicating that utilizing preferredLayoutAttributesFitting inside the headerView subclass of UICollectionReusableView ought to do the trick. Nonetheless, it doesn’t matter what I return right here, it has completely no impact.

Full instance

class SomeViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
    let CellId = "CellId"
    let HeaderId = "HeaderId"
    
    
    @IBOutlet weak var collectionView: UICollectionView!
    
    override func viewDidLoad() {
        tremendous.viewDidLoad()
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: CellId)
        collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: HeaderId)
    }
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 2
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection part: Int) -> Int {
        return part == 0 ? 5 : 10
    }
        
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CellId, for: indexPath)
        cell.backgroundColor = indexPath.part == 0 ? .crimson : .blue
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, structure collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: 50, top: 50)
    }
    
    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind sort: String, at indexPath: IndexPath) -> UICollectionReusableView {
        let view = collectionView.dequeueReusableSupplementaryView(ofKind: sort, withReuseIdentifier: HeaderId, for: indexPath)
        
        if let headerView = view as? HeaderView {
            if indexPath.part == 0 {
                headerView.configure(title: "Part 1", data: "That is part 1")
            } else {
                headerView.configure(title: "Part 2", data: "That is part 2 with an extended textual content. The peak of the header is mechanically adjusted to suit the textual content. And we make the textual content even longer to see the way it works. And we make the textual content even longer to see the way it works. And we make the textual content even longer to see the way it works.")
            }
        }
        
        return view
    }
       
    
    func collectionView(_ collectionView: UICollectionView, structure collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection part: Int) -> CGSize {
        // That is the one place which permits to alter the header top at runtime.
        // Returning completely different sizes for various sections is not any drawback. Nonetheless,
        // with out dequeuing a mock header and measuring its top, one can solely
        // return an estimate.
        //
        // With out this technique all headers are created with the reference top given
        // within the movement structure.
        return CGSize(width: collectionView.body.width, top: 200)
    }
}


class HeaderView: UICollectionReusableView {
    let titleLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = .preferredFont(forTextStyle: .headline)
        label.textColor = .black
        label.numberOfLines = 0
        label.backgroundColor = .white
        return label
    }()
    
    let infoLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = .preferredFont(forTextStyle: .physique)
        label.textColor = .black
        label.numberOfLines = 0
        label.backgroundColor = .white
        return label
    }()
    
    // Initialisierung
    override init(body: CGRect) {
        tremendous.init(body: body)
        setupView()
    }
    
    required init?(coder: NSCoder) {
        tremendous.init(coder: coder)
        setupView()
    }
    
    // Setup der View und Constraints
    personal func setupView() {
        backgroundColor = .inexperienced
        
        addSubview(titleLabel)
        addSubview(infoLabel)
        
        NSLayoutConstraint.activate([
            // Title-Label Constraints
            titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 20),
            titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
            titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
            
            // Info-Label Constraints
            infoLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10),
            infoLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
            infoLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
            infoLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20)
        ])
    }
    
    // Methoden zur Konfiguration der Labels
    func configure(title: String, data: String) {
        titleLabel.textual content = title
        infoLabel.textual content = data
    }
    
    /*override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        let autoLayoutAttributes = tremendous.preferredLayoutAttributesFitting(layoutAttributes)
        
        // It doesn't matter what is returned right here, the consequence has no impact. So this
        // instance returns a set worth as a substitute a calculated one utilizing 
        // systemLayoutSizeFitting
        autoLayoutAttributes.body = CGRect(origin: autoLayoutAttributes.body.origin, dimension: CGSize(width: autoLayoutAttributes.body.width, top: 50))

        return autoLayoutAttributes
        
    }*/
}

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles