This is my first time implementing UICollectionViewCompositionalLayout
and im confused on how to implement a Stretchy Header here. Right now I have just modified the dataSource.supplementaryViewProvider
function to include my custom HeaderView but I don't know how to make it attach to top.
I did find some code for other types of collectionView layouts but those don't work with UICollectionViewCompositionalLayout
. For other layouts I found that I need to override this override func layoutAttributesForElements(in rect: CGRect)
but where and how? I would like to know a method from the scratch. Below is my method which does not work at all with UICollectionViewCompositionalLayout
.
This is how im creating my Header:
class StretchyCollectionHeaderView: UICollectionReusableView {
static let reuseIdentifier = "stretchyCollectionHeaderView-reuse-identifier"
let imageView: UIImageView = {
let iv = UIImageView(image: UIImage(named: "HeaderHomePage"))
iv.contentMode = .scaleAspectFill
return iv
}()
override init(frame: CGRect) {
super.init(frame: frame)
// custom code for layout
backgroundColor = .red
addSubview(imageView)
imageView.fillSuperview()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Im using UICollectionReusableView
instead of UIView
because all section headers are passed as UICollectionReusableView
. Im using an extension which will connect imageView's bottom to header view and for other constraints, I did not include that because I think it isn't even used which I will come back to at the end of explanation.
This is my Layout for CollectionViewLayout with Stretchy Header:
class StretchyHeaderLayout: UICollectionViewCompositionalLayout {
// we want to modify the attributes of our header component somehow
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)
layoutAttributes?.forEach({ (attributes) in
if attributes.representedElementKind == UICollectionView.elementKindSectionHeader && attributes.indexPath.section == 0 {
guard let collectionView = collectionView else { return }
let contentOffsetY = collectionView.contentOffset.y
print(contentOffsetY)
if contentOffsetY > 0 {
return
}
let width = collectionView.frame.width
let height = attributes.frame.height - contentOffsetY
// header
attributes.frame = CGRect(x: 0, y: contentOffsetY, width: width, height: height)
}
})
return layoutAttributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
}
Then I simply register all cells and supplementary views. Then im setting up my collectionViewLayout like this:
func generateLayout() -> UICollectionViewLayout {
let layout = StretchyHeaderLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let isWideView = layoutEnvironment.traitCollection.horizontalSizeClass == .regular
let sectionLayoutKind = Section.allCases[sectionIndex]
switch (sectionLayoutKind) {
case .locationTab: return self.generateLocationLayout(isWide: isWideView)
case .selectCategory: return self.generateCategoriesLayout()
case .valueAddedServices: return self.generatValueAddedServicesLayout(isWide: isWideView)
}
}
return layout
}
im setting layout = StretchyHeaderLayout
as this was the only possible way I could think of adding Stretchy Header layout.
And finally this is how im setting up my section headers:
dataSource.supplementaryViewProvider = { (
collectionView: UICollectionView,
kind: String,
indexPath: IndexPath) -> UICollectionReusableView? in
if indexPath.section == 0 {
guard let supplementaryView = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: StretchyCollectionHeaderView.reuseIdentifier,
for: indexPath) as? StretchyCollectionHeaderView else { fatalError("Cannot create header view") }
supplementaryView.imageView.image = UIImage(named: "HeaderHomePage")
return supplementaryView
}
else {
guard let supplementaryView = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: HeaderView.reuseIdentifier,
for: indexPath) as? HeaderView else { fatalError("Cannot create header view") }
supplementaryView.label.text = Section.allCases[indexPath.section].rawValue
return supplementaryView
}
}
Here im just using StretchyCollectionHeaderView
for first section and for others im using another HeaderView
which just contains a label.
What I think is happening is because of above function, its just setting header view with StreatchyCollectionHeaderView
for first section but not accessing any code inside StretchyHeaderLayout
and thus not sticking to top.
Unlike UITableView
, we cannot attach a CollectionView header instead of adding section header or in my case section header with image for first section and attach to top?
How to create a stretchy header for UICollectionViewCompositionalLayout
properly?
CodePudding user response:
I found an answer by modifying some code I found for StretchyTableHeaderView
. I will try to explain what I did in short before adding the code below. So first I simply created CollectionViewReusableView
like for any SupplementaryView you create for CollectionViews
. Actually I just found this code for stretchy TableViewHeader, I just converted it to UICollectionReusableView
. But this header view contains a scrollViewDidScroll function which manipulates the bottom constraints for your containerView and imageView and also manipulates the height of imageView so it increases with offset of the scrollView.
After that you just register this to your collectionView
, pass this as supplementaryView
for First Section and give it a height while creating the layout. And finally in scrollViewDidScroll
delegate method, look for your supplementary view and if found, just call the scrollViewDidScroll function inside your header.
This is the StretchHeaderCollectionResulableView class:
final class StretchyCollectionHeaderView: UICollectionReusableView {
static let reuseIdentifier = "stretchy-homePage-header-view-reuse-identifier"
public let imageView: UIImageView = {
let imageView = UIImageView()
imageView.clipsToBounds = true
imageView.contentMode = .scaleAspectFill
return imageView
}()
private var imageViewHeight = NSLayoutConstraint()
private var imageViewBottom = NSLayoutConstraint()
private var containerView = UIView()
private var containerViewHeight = NSLayoutConstraint()
// MARK: - Init
override init(frame: CGRect) {
super.init(frame: frame)
createViews()
setViewConstraints()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
/// Create Subviews
private func createViews() {
addSubview(containerView)
containerView.addSubview(imageView)
}
/// Setup View Constraints
func setViewConstraints() {
NSLayoutConstraint.activate([
widthAnchor.constraint(equalTo: containerView.widthAnchor),
centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
heightAnchor.constraint(equalTo: containerView.heightAnchor)
])
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.widthAnchor.constraint(equalTo: imageView.widthAnchor).isActive = translatesAutoresizingMaskIntoConstraints
containerViewHeight = containerView.heightAnchor.constraint(equalTo: self.heightAnchor)
containerViewHeight.isActive = true
imageView.translatesAutoresizingMaskIntoConstraints = false
imageViewBottom = imageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
imageViewBottom.isActive = true
imageViewHeight = imageView.heightAnchor.constraint(equalTo: containerView.heightAnchor)
imageViewHeight.isActive = true
}
/// Notify View of scroll change from container
public func scrollviewDidScroll(scrollView: UIScrollView) {
containerViewHeight.constant = scrollView.contentInset.top
let offsetY = -(scrollView.contentOffset.y scrollView.contentInset.top)
containerView.clipsToBounds = offsetY <= 0
imageViewBottom.constant = offsetY >= 0 ? 0 : -offsetY / 2
imageViewHeight.constant = max(offsetY scrollView.contentInset.top, scrollView.contentInset.top)
}
}
public func scrollviewDidScroll(scrollView: UIScrollView)
is the function that you need to call in your main ViewController
's scrollViewDidScroll
method.
Register above class to your collectioView:
collectionView.register(StretchyCollectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: StretchyCollectionHeaderView.reuseIdentifier)
Add this to your section header while creating layout for collectionView:
func generateFirstSectionLayout(isWide: Bool) -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(44))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)
// Set header properties here
let headerSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalWidth(isWide ? 2/4 : 2/3))
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: headerSize,
elementKind: HomePageViewController.sectionHeaderElementKind,
alignment: .top)
let section = NSCollectionLayoutSection(group: group)
section.boundarySupplementaryItems = [sectionHeader]
return section
}
Add data to your header:
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if indexPath.section == 0 {
guard let supplementaryView = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: StretchyCollectionHeaderView.reuseIdentifier,
for: indexPath) as? StretchyCollectionHeaderView else { fatalError("Cannot create header view") }
supplementaryView.imageView.image = UIImage(named: "HeaderHomePage")
return supplementaryView
}
else {
guard let supplementaryView = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: HomePageAutoxHeaderView.reuseIdentifier,
for: indexPath) as? HeaderView else { fatalError("Cannot create header view") }
return supplementaryView
}
}
Add to first section and then finally call this in your ViewController:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let header = homePageCollectionView.supplementaryView(forElementKind: HomePageViewController.sectionHeaderElementKind, at: IndexPath(item: 0, section: 0)) as? StretchyCollectionHeaderView {
header.scrollviewDidScroll(scrollView: homePageCollectionView)
}
}
In case your header only stretches after you start scrolling, call above function in viewDidAppear()
too.