49

How can I add a header view / top view (not section header) at the top of a UICollectionView?

It should act excactly as UITableView's tableHeaderView property.

So it needs to sit on top of the first section header view (before the section at index 0), scroll along with the rest of the content, and have user interaction.

The best I've come up with so far is to make a special XIB (with MyCollectionReusableView subclass of UICollectionReusableView as the File's owner) for the first section header view that is big enough to also contain my subviews in header, it's kind of a hack, I think, and I haven't managed to detect touches.

Not sure if I can make my MyCollectionReusableView subclass to allow touches or there's a better way.

Declan McKenna
  • 4,321
  • 6
  • 54
  • 72
ChrHansen
  • 1,502
  • 1
  • 14
  • 22

7 Answers7

63
self.collectionView  =  [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 320, self.view.frame.size.height) collectionViewLayout:flowlayout];
self.collectionView.contentInset = UIEdgeInsetsMake(50, 0, 0, 0);
UIImageView *imagev = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"015.png"]];
imagev.frame = CGRectMake(0, -50, 320, 50);
[self.collectionView addSubview: imagev];
[self.view addSubview: _collectionView];

I use the attribute contentInset to insert a frame to the top of the UICollectionView, then I add the image view to it, and it succeeds. I think it can act excactly as UITableView's tableHeaderView property. What do you think?

Robert
  • 5,735
  • 3
  • 40
  • 53
dxci
  • 631
  • 1
  • 5
  • 4
11

I've put a view at the top of a collection view by just adding a UIView as a subview of the collection view. It does scroll up with the collection view and it has normal UIView user interaction. This works ok if you have no section header, but doesn't work if you do. In that situation, I don't see anything wrong with the way you're doing it. I'm not sure why you're not detecting touches, they work fine for me.

rdelmar
  • 103,982
  • 12
  • 207
  • 218
  • Thanks, I do already use section headers, so the first one (section header at index 0) I've made bigger to contain both the standard section header and the view on top of the collection view. But you say you get touches inside your `MyCollectionReusableView` subclass? – ChrHansen Nov 23 '12 at 08:14
  • Just went back and tried again, and you're right - it works! I had been working too late and I used `UIImageView`'s to detect touches and forgot to tick the "User Interaction Enabled" for my image views in Xcode. – ChrHansen Nov 23 '12 at 08:20
  • How did you achieve this? I add it (it's a `UISearchBar` btw) to the collectionView (with constraints top, leading trailing) with required contentInset and it just shows a blank space at the top and in the view debugger it says collection view contentsize ambiguous. – Rakesha Shastri Jan 31 '19 at 12:44
  • I'm really stuck here. I'd really appreciate it if you could help me out. – Rakesha Shastri Feb 04 '19 at 09:50
9

Since iOS 13 a canonical way to set the header is to use UICollectionViewCompositionalLayoutConfiguration

This way allows setting boundarySupplementaryItems per section or globally for a collection view.

let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))

let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: "header", alignment: .top)

let config = NSCollectionViewCompositionalLayoutConfiguration()
config.boundarySupplementaryItems = [header]

let layout = UICollectionViewCompositionalLayout(sectionProvider: sectionProvider, configuration: config)

return layout

To learn more visit the official documentation at: https://developer.apple.com/documentation/appkit/nscollectionviewcompositionallayoutconfiguration

Sam Soffes
  • 14,831
  • 9
  • 76
  • 80
Tomasz Nazarenko
  • 1,064
  • 13
  • 31
  • Looks like NSCollectionViewCompositionalLayoutConfiguration (and even the link included in this answer) are MacOS only. – Justin Vallely Jan 09 '23 at 22:44
  • for UIKit it's UICollectionViewCompositionalLayoutConfiguration https://developer.apple.com/documentation/uikit/uicollectionviewcompositionallayoutconfiguration – Robert Juz Jan 17 '23 at 14:14
5

I ended up using a UICollectionView extension to add my custom header. Using a specific tag, I'm able to fake the stored property to identify my custom header (all transparent from outside). You might have to scroll to a specific offset if the layout of the UICollectionView is not completed when you add your custom header.

extension UICollectionView {
    var currentCustomHeaderView: UIView? {
        return self.viewWithTag(CustomCollectionViewHeaderTag)
    }

    func asssignCustomHeaderView(headerView: UIView, sideMarginInsets: CGFloat = 0) {
        guard self.viewWithTag(CustomCollectionViewHeaderTag) == nil else {
            return
        }
        let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
        headerView.frame = CGRect(x: sideMarginInsets, y: -height + self.contentInset.top, width: self.frame.width - (2 * sideMarginInsets), height: height)
        headerView.tag = CustomCollectionViewHeaderTag
        self.addSubview(headerView)
        self.contentInset = UIEdgeInsets(top: height, left: self.contentInset.left, bottom: self.contentInset.bottom, right: self.contentInset.right)
    }

    func removeCustomHeaderView() {
        if let customHeaderView = self.viewWithTag(CustomCollectionViewHeaderTag) {
            let headerHeight = customHeaderView.frame.height
            customHeaderView.removeFromSuperview()
            self.contentInset = UIEdgeInsets(top: self.contentInset.top - headerHeight, left: self.contentInset.left, bottom: self.contentInset.bottom, right: self.contentInset.right)
        } 
    }
}

CustomCollectionViewHeaderTag refers to the tag number you give to your header. Make sure it is not the tag of anotherView embedded in your UICollectionView.

Swift Rabbit
  • 1,370
  • 3
  • 14
  • 29
2

The best way to achieve this is to make a section header for the first section. Then set the UICollectionViewFlowLayout's property:

@property(nonatomic) BOOL sectionHeadersPinToVisibleBounds

or swift

var sectionHeadersPinToVisibleBounds: Bool

to NO (false for swift). This will ensure that the section header scrolls continuously with the cells and does not get pinned to the top like a section header normally would.

denvdancsk
  • 3,033
  • 2
  • 26
  • 41
  • this works for me , but what if I want some sectionHeader pinned to the top, but some not? this UICollectionViewFlowLayout's property likes a global set. – Neko Sep 13 '17 at 06:57
1

I think the best way to accomplish this it to subclass UICollectionViewLayout(UICollectionViewFlowLayout) and add the needed header or footer attributes.

Since all the items including the supplementary view in UICollectionView layout according to its collectionViewLayout property, it is the collectionviewlayout that decides whether there is a header(footer) or not.

Use contentInset is easier, but there may be problem when you want to hide or show the header dynamically.

I have written a protocol to accomplish this, which could be adopted by any subclass of UICollectionViewLayout to make it have a header or footer. You can find it here.

The main idea is to create a UICollectionViewLayoutAttributes for the header and return it in layoutAttributesForElements(in rect: CGRect) if needed, you will have to modify all the other attributes, all of their locations should be moved down by header height because the header is above them all.

Billthas
  • 21
  • 5
-2
private var PreviousInsetKey: Void?
extension UIView {

var previousInset:CGFloat {
    get {
        return objc_getAssociatedObject(self, &PreviousInsetKey) as? CGFloat ?? 0.0
    }
    set {
        if newValue == -1{
            objc_setAssociatedObject(self, &PreviousInsetKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
        else{
            objc_setAssociatedObject(self, &PreviousInsetKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}


func addOnCollectionView(cv: UICollectionView){
    if self.superview == nil{
        var frame = self.frame
        frame.size.width = SCREEN_WIDTH
        cv.frame = frame
        self.addOnView(view: cv)
        let flow = cv.collectionViewLayout as! UICollectionViewFlowLayout
        var inset = flow.sectionInset
        previousInset = inset.top
        inset.top = frame.size.height
        flow.sectionInset = inset
    }

}

func removeFromCollectionView(cv: UICollectionView){
    if self.superview == nil{
        return
    }
    self.removeFromSuperview()
    let flow = cv.collectionViewLayout as! UICollectionViewFlowLayout
    var inset = flow.sectionInset
    inset.top = previousInset
    flow.sectionInset = inset
    previousInset = -1
}

func addOnView(view: UIView){

    view.addSubview(self)

    self.translatesAutoresizingMaskIntoConstraints = false
    let leftConstraint = NSLayoutConstraint(item: self, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leading, multiplier: 1.0, constant: 0)
    let widthConstraint = NSLayoutConstraint(item: self, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: view.frame.size.width)
    let heightConstraint = NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: view.frame.size.height)
    let topConstraint = NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1.0, constant: 0)

    view.addConstraints([leftConstraint, widthConstraint, heightConstraint, topConstraint])
    self.layoutIfNeeded()
    view.layoutIfNeeded()
}
}