UICollectionView Multiple Sections and Headers

2020-02-09 07:23发布

问题:

I am struggling trying to do multiple sections in my collection view with a header for each section. I don't know Obj-C and I've found a good amount of tutorials for it, but haven't been able to figure out how to convert it into Swift.

All my data is static so all I need is some sort of array or dictionary that I can use to create the multiple sections. I already have a collection view with 1 section working, so if you have any insight or code for multiple sections that'd be helpful.

I know how to set multiple sections using

func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int   {
    return sectionData.count
}

I think the main thing I need help with is implementing this func

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { }

and setting up the data!

UICollectionView and UITableView are almost exactly the same, so if you know how to do multiple sections in a UITableView in Swift, your help is also appreciated

回答1:

The cellForItemAtIndexPath function handles populating each section with cells, it does not handle sections or supplementaryViews, and therefore is not the main thing you need help with when it comes to creating section headers.

the method you need to implement is viewForSupplementaryElementOfKind. Its signature is:

func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {}

Assuming that your collectionView is working correctly for 1 section (you have properly filled out the body of your cellForItemAtIndexPath and your sectionData array properly reflects the number of sections you want to display), you should be able to implement section headers using the following pointers:

Along with cells, UICollectionView also supports "supplementary" view objects, typically used for headers or footers. These Supplementary Views act very similarly to UICollectionViewCell objects. In the same way that cellForItemAtIndexPath handles cells, The viewForSupplementaryElementOfKind function handles supplementary views.

To implement it, you will need to first prepare your ViewController to do so. First edit your layout object to reflect an appropriate header size, that each header will adhere to:

 let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
 layout.headerReferenceSize = CGSize(width: self.view.frame.size.width, height: 30)

NOTE: I am using a UICollectionViewFlowLayout

Next, if you haven't already done so, create a SectionHeader class that defines each section header object, so you can then register that class with your collectionView object like so:

  collectionView!.registerClass(SectionHeaderView.self, forSupplementaryViewOfKind:UICollectionElementKindSectionHeader, withReuseIdentifier: "SectionHeaderView");

Here, the first and third argument passed in are the same as a UICollectionViewCell Class registration, the first argument in this method is the reference to the section header class you created. The third is the reuse identifier for the Supplementary View.

The second argument is specific to Supplementary Views, this sets the kind of the SupplementaryView, which in this case is a header, the constant string provided by the UICollectionViewFlowLayout class UICollectionElementKindSectionHeader is used for it. If you noticed the parameters on the viewForSupplementaryElementOfKind, this kind is later passed in as the kind: String parameter.

Fill in the body of your viewForSupplementaryElementOfKind the same way you would for a cellForItemAtIndexPath function-- Using the dequeueReusableSupplementaryViewOfKind method to create a SectionHeader object, then set any attributes as necessary (labels, colors, etc.) and finally return the header object.

Hope this helps!!

Reference points:

https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UICollectionViewDataSource_protocol/index.html#//apple_ref/occ/intfm/UICollectionViewDataSource/

https://developer.apple.com/library/ios/documentation/UIKit/Reference/UICollectionViewFlowLayout_class/index.html#//apple_ref/c/data/UICollectionElementKindSectionHeade



回答2:

Define your UICollectionViewCell which will be your Header view of kind UICollectionElementKindSectionHeader - In my case I have two headers - OfferHeaderCell and APRHeaderCell defined as below:

verticalCollectionView.register(UINib(nibName: "OfferHeaderCell", bundle: nil), forSupplementaryViewOfKind:UICollectionElementKindSectionHeader, withReuseIdentifier: "OfferHeaderCell")
verticalCollectionView.register(UINib(nibName: "APRHeaderCell", bundle: nil), forSupplementaryViewOfKind:UICollectionElementKindSectionHeader, withReuseIdentifier: "APRHeaderCell")

Go ahead and return a header for each section and then set the size of the section header to have a size of zero in this UICollectionViewDelegateFlowLayout function

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
    if(section==0) {
        return CGSize.zero
    } else if (section==1) {
        return CGSize(width:collectionView.frame.size.width, height:133)
    } else {
        return CGSize(width:collectionView.frame.size.width, height:100)
    }

}

Important to define the viewForSupplementaryElementOfKind for two different sections as below:

func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {

    var reusableview = UICollectionReusableView()
    if (kind == UICollectionElementKindSectionHeader) {
        let section = indexPath.section
        switch (section) {
        case 1:
            let  firstheader: OfferHeaderCell = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "OfferHeaderCell", for: indexPath) as! OfferHeaderCell
            reusableview = firstheader
        case 2:
            let  secondHeader: APRHeaderCell = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "APRHeaderCell", for: indexPath) as! APRHeaderCell
            reusableview = secondHeader
        default:
            return reusableview

        }
    }
    return reusableview
}

And lastly the Datasource,

func numberOfSections(in collectionView: UICollectionView) -> Int {
    return 3
}

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    if (section==2) {
        return 2
    }
    return 0
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = verticalCollectionView.dequeueReusableCell(withReuseIdentifier: "ReviseOfferCell", for: indexPath)
    cell.backgroundColor = UIColor.white
    return cell
}

Note: Don't forgot to add UICollectionFlowLayout as below:

// MARK: UICollectionViewDelegateFlowLayout

extension MakeAnOfferController: UICollectionViewDelegateFlowLayout {

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

            if indexPath.item == 0 {
                return CGSize(width: self.view.frame.size.width, height: 626.0)
            }
            return CGSize()
        }

        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {

            if(section==0) {
                return CGSize.zero
            } else if (section==1) {
                return CGSize(width:collectionView.frame.size.width, height:133)
            } else {
                return CGSize(width:collectionView.frame.size.width, height:100)
            }
        }
    }


回答3:

Here is the code that worked for me

create the header cell. To do which i created a custom cell class and a nib to do the customization of the cell in the graphic editor

In viewDidLoad add the following

self.collectionView?.registerNib(UINib(nibName: "KlosetCollectionHeaderViewCell", bundle: nil), forSupplementaryViewOfKind:UICollectionElementKindSectionHeader, withReuseIdentifier: "HeaderCell")

Then you add the delegate function

override func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> KlosetCollectionHeaderViewCell {

    let headerCell = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: "HeaderCell", forIndexPath: indexPath) as? KlosetCollectionHeaderViewCell    

    return headerCell!
  }

This will put the HeaderCell in the SectionView of the PFCollectionView The controls that show in the cell you add them to the xib file as well as the outlets and actions



回答4:

After creating and registering custom header (and/or footers), you can easily specify different header (or footers for that matter) for different section. Here's an example:

    override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        switch kind {
        case UICollectionElementKindSectionHeader:
            let section = indexPath.section

            switch section {
            case 0:
                let userHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: userHeaderReuseIdentifier, for: indexPath) as! UserHeader
                return userHeader
            default:
                let postHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: postHeaderReuseIdentifier, for: indexPath) as! PostHeader
                return postHeader
            }
        case UICollectionElementKindSectionFooter:
            let userFooter = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: userFooterReuseIdentifier, for: indexPath) as! UserFooter
            return userFooter
        default:
            return UICollectionReusableView()
        }
    }

Make sure to specify correct number of sections, too:

    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 2
    }


回答5:

Here is the code to achieve UICollection multiple sections made programmatically using SnapKit

ViewController

import SnapKit
import UIKit

class SelectIconViewController: GenericViewController<SelectIconView>, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

    weak var delegate: SpaceAddViewController?
    struct Section {
        var sectionName : String
        var rowData : [String]
    }
    var sections : [Section]!

    init(delegate: SpaceAddViewController) {
        self.delegate = delegate
        super.init()
    }


    public required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }


    override func viewDidLoad() {
        super.viewDidLoad()

        contentView.closeButton.addTarget(self, action: #selector(self.back), for: .touchUpInside)

        self.sections = [
            Section(sectionName: "SPACES", rowData: ["Air Conditioner", "Apple HomePod"]),
            Section(sectionName: "HOME APPLIANCES", rowData: ["Ceiling Fan", "Fan", "Desk Lamp", "Iron", "PC on Desk", "Plug", "Power Strip", "Lorem", "Lorem", "Lorem", "Lorem"]),
        ]
        self.contentView.collectionView.dataSource = self
        self.contentView.collectionView.delegate = self
        self.contentView.collectionView.register(SelectIconHeaderViewCell.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: SelectIconHeaderViewCell.reuseId)
        self.contentView.collectionView.register(SelectIconViewCell.self, forCellWithReuseIdentifier: SelectIconViewCell.reuseId)

    }


    @objc func back() {
        self.dismiss(animated: true, completion: nil)
    }


    @objc func dismissKeyboard() {
        view.endEditing(true)
    }


    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return self.sections.count
    }


    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return self.sections[section].rowData.count
    }


    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
    }


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

        return CGSize(width: getTotalSpacing(), height: getTotalSpacing())
    }


    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
        let screenSize = UIScreen.main.bounds
        let screenWidth = screenSize.width-40
        return CGSize(width: screenWidth-80, height: 50)
    }


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

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


    // MARK: Cells

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let cell = self.contentView.collectionView.dequeueReusableCell(withReuseIdentifier: SelectIconViewCell.reuseId, for: indexPath as IndexPath) as! SelectIconViewCell
        cell.initializeUI()
        cell.createConstraints()
        cell.setValues(iconName: "", label: self.sections[indexPath.section].rowData[indexPath.row])
        return cell
    }


    // MARK: Header
    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {

        switch kind {
        case UICollectionView.elementKindSectionHeader:

            let cell = self.contentView.collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: SelectIconHeaderViewCell.reuseId, for: indexPath) as! SelectIconHeaderViewCell
            cell.initializeUI()
            cell.createConstraints()
            cell.setTitle(title: self.sections[indexPath.section].sectionName)
            return cell
        default:  fatalError("Unexpected element kind")
        }
    }


    func getTotalSpacing() -> CGFloat {

        let screenSize = UIScreen.main.bounds
        let screenWidth = screenSize.width
        let numberOfItemsPerRow:CGFloat = 3
        let spacingBetweenCells:CGFloat = 0
        let sideSpacing:CGFloat = 20
        return (screenWidth-(2 * sideSpacing) - ((numberOfItemsPerRow - 1) * spacingBetweenCells))/numberOfItemsPerRow
    }

}

The View:

import UIKit
import SnapKit


class SelectIconView: GenericView {

    private let contentView = UIView(frame: .zero)
    private (set) var closeButton = UIButton(type: .system)
    internal var collectionView: UICollectionView!


    internal override func initializeUI() {

        self.backgroundColor = Theme.Color.white
        self.addSubview(contentView)

        contentView.addSubview(closeButton)
        if let image = UIImage(named: "icon_close") {
            image.withRenderingMode(.alwaysTemplate)
            closeButton.setImage(image, for: .normal)
            closeButton.tintColor = Theme.Color.text
        }

        let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
        layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
        layout.minimumInteritemSpacing = 0
        collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout)
        contentView.addSubview(collectionView)
        collectionView.backgroundColor = Theme.Color.background
    }


    internal override func createConstraints() {

        contentView.snp.makeConstraints { (make) in
            make.top.equalTo(safeAreaLayoutGuide.snp.top).priority(750)
            make.left.right.equalTo(self).priority(1000)
            make.bottom.equalTo(safeAreaLayoutGuide.snp.bottom)
        }

        closeButton.snp.makeConstraints { make in
            make.right.equalTo(safeAreaLayoutGuide.snp.right).offset(-10)
            make.top.equalTo(contentView.snp.top).offset(10)
            make.height.equalTo(40)
            make.width.equalTo(40)
        }

        collectionView.snp.makeConstraints { make in
            make.top.equalTo(closeButton.snp.bottom).offset(20)
            make.left.equalTo(safeAreaLayoutGuide.snp.left)
            make.right.equalTo(safeAreaLayoutGuide.snp.right)
            make.bottom.equalTo(contentView.snp.bottom)
        }

    }

}

The customized section Header

import UIKit

class SelectIconHeaderViewCell: UICollectionViewCell {

    internal let mainView = UIView()
    internal var title = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }


    func initializeUI() {

        self.backgroundColor = UIColor.clear
        self.addSubview(mainView)
        mainView.backgroundColor = UIColor.clear

        mainView.addSubview(title)
        title.text = "Pick nameA"
        title.font = Theme.Font.body()
        title.textAlignment = .left
        title.textColor = Theme.Color.text
        title.numberOfLines = 1

    }


    internal func createConstraints() {

        mainView.snp.makeConstraints { (make) in
            make.edges.equalTo(self)
        }

        title.snp.makeConstraints { (make) in
            make.centerY.equalTo(mainView.snp.centerY)
            make.leading.equalTo(mainView).offset(20)
            make.trailing.equalTo(mainView).offset(-20)
        }
    }


    func setTitle(title: String)  {

        self.title.text = title
    }


    static var reuseId: String {
        return NSStringFromClass(self)
    }

}

And the cell:

import UIKit

class SelectIconViewCell: UICollectionViewCell {

    internal let mainView = UIView()
    internal var iconImage = UIImageView()
    internal var label = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }


    func initializeUI() {

        self.backgroundColor = UIColor.clear
        self.addSubview(mainView)
        mainView.backgroundColor = UIColor.clear
        mainView.layer.masksToBounds = true
        mainView.layer.borderColor = Theme.Color.backgroundCell.cgColor
        mainView.layer.borderWidth = 1.0

        mainView.addSubview(iconImage)
        iconImage.image = UIImage(named: "icons8-air-conditioner-100")

        mainView.addSubview(label)
        label.font = Theme.Font.footnote()
        label.textAlignment = .center
        label.textColor = Theme.Color.textInfo
        label.numberOfLines = 1
    }


    internal func createConstraints() {

        mainView.snp.makeConstraints { (make) in
            make.edges.equalTo(self)
        }

        iconImage.snp.makeConstraints { (make) in
            make.center.equalTo(mainView.snp.center)
            make.width.height.equalTo(20)
        }

        label.snp.makeConstraints { (make) in
            make.top.equalTo(iconImage.snp.bottom).offset(6)
            make.leading.equalTo(mainView).offset(5)
            make.trailing.equalTo(mainView).offset(-5)
        }
    }


    func setValues(iconName: String, label: String)  {

        //self.iconImage.image = UIImage(named: iconName)
        self.label.text = label
    }


    static var reuseId: String {
        return NSStringFromClass(self)
    }

}


回答6:

Worked solution for Swift-3

i)Create Custom Cell && corresponding xib

   class SectionHeaderView: UICollectionViewCell {
        static let kReuseIdentifier = "SectionHeaderView"
        @IBOutlet weak var invitationsSectionHeader: UILabel!
        @IBOutlet weak var numberOfPerson: UILabel!
 }

ii)Register Custom Collection View Cell for HeaderView

 self.collectionView.register(UINib(nibName: SectionHeaderView.kReuseIdentifier, bundle: nil), forSupplementaryViewOfKind:UICollectionElementKindSectionHeader, withReuseIdentifier: SectionHeaderView.kReuseIdentifier)

iii)Call delegate function to render Custom Header View.

  func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        switch kind {
        case UICollectionElementKindSectionHeader:
           let  headerView: SectionHeaderView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: SectionHeaderView.kReuseIdentifier, for: indexPath) as! SectionHeaderView
            return headerView
        default:
            return UICollectionReusableView()
        }
    }

iv)Mention Height of the Custom Header View

  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
        return CGSize(width:collectionView.frame.size.width, height:30)
}


回答7:

@Tarun's answer worked a treat for me; I was missing collectionView(_:layout:referenceSizeForHeaderInSection:), which I needed since sometimes the data to be shown would be sorted and sometimes not.

In addition, pinning the section header to the top of the screen (as in the table view Apple's Address Book app) was accomplished by adding the following to viewDidLoad() in the UICollectionViewController:

if let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout {
    flowLayout.sectionHeadersPinToVisibleBounds = true
}