Skip to content

salahamassi/Stretchy-Header-Collection-View

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 

Repository files navigation

Stretchy Header Collection View with images slider

We can add a Stretchy Hader view for collection view simply using "SupplementaryView". we can achieve this using custom "UICollectionViewFlowLayout" and use the "layoutAttributesForElements" method to update attribute frames base on scroll content offset y.

  override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let layoutAttributes = super.layoutAttributesForElements(in: rect),
              let collectionView = collectionView else { return nil }
        layoutAttributes.forEach({ (attribute) in
            if attribute.representedElementKind == UICollectionView.elementKindSectionHeader {
                let contentOffsetY = collectionView.contentOffset.y
                if contentOffsetY < 0 {
                    let width = collectionView.frame.width
                    // as contentOffsetY is -ve, the height will increase based on contentOffsetY
                    let height = attribute.frame.height - contentOffsetY
                    attribute.frame = CGRect(x: 0, y: contentOffsetY, width: width, height: height)
                }
            }
        })
        return layoutAttributes
    }

Full implemntation here

But what if we need to add a collection view inside the header view?

The client wants to replace the fixed product image with an array of products images, so we need to remove the image view and add a collection view with images cells

    lazy var collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
        let view = UICollectionView(frame: frame, collectionViewLayout: layout)
        view.isPagingEnabled = true
        return view
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(collectionView)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.backgroundColor = .blue
        NSLayoutConstraint.activate([
            collectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
            collectionView.topAnchor.constraint(equalTo: topAnchor),
            collectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
        collectionView.dataSource = self
        collectionView.delegate = self
        collectionView.register(AttributeCell.self, forCellWithReuseIdentifier: AttributeCell.cellId)
    }
  extension PDPHeaderView: UICollectionViewDataSource {

      func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
          20
      }

      func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
          let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AttributeCell.cellId, for: indexPath)
          cell.backgroundColor = UIColor(hue: CGFloat(drand48()), saturation: 1, brightness: 1, alpha: 1)
          return cell
      }
  }
  extension PDPHeaderView: UICollectionViewDelegateFlowLayout {

      func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
          .zero
      }

      func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
          .zero
      }

      func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
          .zero
      }

      func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
          collectionView.frame.size
      }
  }

If you try that unfortunately you will end up with this weird behavior

Mar-05-2021 01-37-10

Instead of this normal one

Mar-05-2021 01-38-54

And of course, you will find the console start screaming with warnings.

The problem is when we update the layout attribute frame for the header view this will update the collection view frame (the one inside the header view), but not the image cells.

so when we are scrolling we see the blue color (the background color for the collection view inside the header view) and not the image cell color.

we need to notify the header collection view to update the size for his cells at the same time when we update the layout attribute frame for the header view itself. and we can do that simply using

     headerView.collectionView.collectionViewLayout.invalidateLayout()

this will invoke all the "UICollectionViewDelegateFlowLayout" methodes and the size will be updated.

So one solution we can do is have closure on our custom flow layout class and we can invoke this closure when we update the header layout attribute frame.

    typealias UpdateLayoutAttributesObserver = (() -> Void)
    
    var onUpdateLayoutAttributesForSectionHeader: UpdateLayoutAttributesObserver?
    
        override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let layoutAttributes = super.layoutAttributesForElements(in: rect),
              let collectionView = collectionView else { return nil }
        layoutAttributes.forEach({ (attribute) in
            if attribute.representedElementKind == UICollectionView.elementKindSectionHeader {
                let contentOffsetY = collectionView.contentOffset.y
                if contentOffsetY < 0 {
                    let width = collectionView.frame.width
                    // as contentOffsetY is -ve, the height will increase based on contentOffsetY
                    let height = attribute.frame.height - contentOffsetY
                    attribute.frame = CGRect(x: 0, y: contentOffsetY, width: width, height: height)
                }
                onUpdateLayoutAttributesForSectionHeader?()
            }
        })
        return layoutAttributes
    }

The client (the view controller on our case) can use this closure like this:

        if let layout = collectionViewLayout as? CustomPDPLayout {
            layout.onUpdateLayoutAttributesForSectionHeader = updateHeaderViewLayout()
        }
    private func updateHeaderViewLayout () -> CustomPDPLayout.UpdateLayoutAttributesObserver {
        { [weak self] in
            guard let self = self else { return }
            let firstSection = IndexPath(item: .zero, section: .zero)
            guard let headerView = self.collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionHeader, at: firstSection) as? PDPHeaderView else { return }
            headerView.collectionView.collectionViewLayout.invalidateLayout()
        }
    }

Now everything will work smoothly.

About

iOS collection view with stretchy header that responds to scroll gestures using custom flow layout

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages