How to add HeaderView in UICollectionView like UIT

2019-01-21 09:28发布

问题:

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.

回答1:

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.



回答2:

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?



回答3:

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.



回答4:

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.



回答5:

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.



回答6:

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()
}
}