Prototype collection view section header or footer views must have reuse identifiers

Hi, even though I have registered the footer with a reuseIdentifier and called it in viewForSupplementaryElementOfKind and set up it's referenceSizeForFooterInSection, I have been getting the error in the title. Could you please help ?


fileprivate let collectionView: UICollectionView = {

let layout = UICollectionViewFlowLayout()

layout.scrollDirection = .horizontal

let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)

cv.translatesAutoresizingMaskIntoConstraints = false

cv.register(CustomCell.self, forCellWithReuseIdentifier: "PreviewCell")

cv.register(CustomCell.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "FooterCell")

return cv

}()


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


if kind == UICollectionView.elementKindSectionFooter {

let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "FooterCell", for: indexPath) as! CustomCell

footer.backgroundColor = .blue

return footer

} else {

assert(false, "Unexpected element kind")

}

}


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

return CGSize(width: view.frame.width, height: 10)

}

Accepted Reply

The problem is that footer is a section property ; you cannot use it for appending some footer on each collection cell.


So :

- create a property in CustomCell to hold text, in the same way you created bg and bgImage

- add the view for the label and position it at the bottom anchor

- set the text value when you create the cell (in cellForItemAt indexPath)

Replies

It is not totally clear if you have set the identifier in IB:

- select the xib file

- open the Attributes inspector

-> Did you enter the identifier on second field ?

Thank you for your reply. Because, I have created the collectionView programmatically, I don't have a representation of it on the storyboard. Would you like me to send you the whole code ?

import Foundation

import UIKit

import Photos



class PhotoViewController: UIViewController {


@IBOutlet weak var photoImageView: UIImageView!

@IBOutlet var photoView: UIView!

@IBOutlet weak var menuStackView: UIStackView!


var imagePicker = UIImagePickerController()


override func viewDidLoad() {

super.viewDidLoad()


imagePicker.delegate = self

assignTap()


view.addSubview(collectionView)

collectionView.backgroundColor = .white

collectionView.topAnchor.constraint(equalTo: photoImageView.bottomAnchor, constant: 110).isActive = true

collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10).isActive = true

collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 10).isActive = true

collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -103).isActive = true


collectionView.clipsToBounds = true


collectionView.dataSource = self

collectionView.delegate = self

collectionView.isHidden = false


}


@IBAction func previewTapped(_ sender: UIButton) {

sender.pulsate()

if collectionView.isHidden == true {

collectionView.isHidden = false

} else {

collectionView.isHidden = true

}

}


@IBAction func effectsTapped(_ sender: UIButton) {

sender.flash()

}


@IBAction func exportTapped(_ sender: UIButton) {

sender.shake()

}



func assignTap() {

let tap = UITapGestureRecognizer(target: self, action: #selector(touchTapped(_:)))

photoView.addGestureRecognizer(tap)


}


@objc func touchTapped(_ sender: UITapGestureRecognizer) {

if menuStackView.isHidden == false {

collectionView.isHidden = true

menuStackView.isHidden = true

} else {

menuStackView.isHidden = false

collectionView.isHidden = false

}


}


func chooseImage(_ sender: Any) {


if UIImagePickerController.isSourceTypeAvailable(.photoLibrary) {

imagePicker.sourceType = .photoLibrary

imagePicker.allowsEditing = true

self.present(imagePicker,animated: true, completion: nil)

}

}


func accessVideo(_ sender: Any) {


if UIImagePickerController.isSourceTypeAvailable(.camera) {

imagePicker.sourceType = .camera

self.present(imagePicker, animated: true, completion: nil)

} else {

print("Camera not available")

}

}


public func chooseLastImage(_ sender: Any) {


print("photoImageView", photoImageView!)

print("photoImageView.bounds", photoImageView.bounds)


let targetSize: CGSize? = CGSize(width: photoImageView.bounds.width, height: photoImageView.bounds.height)

let options = PHImageRequestOptions()

options.isNetworkAccessAllowed = true

options.deliveryMode = .highQualityFormat


let imgManager = PHImageManager.default()

let fetchOptions = PHFetchOptions()

fetchOptions.sortDescriptors = [NSSortDescriptor(key:"creationDate", ascending: true)]


let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)


print("targetSize for request", targetSize!) //What is its value

imgManager.requestImage(for: fetchResult.lastObject!, targetSize: targetSize!, contentMode: .default, options: options) { (result, info) in

guard let result = result else { return }

DispatchQueue.main.async(execute: {

self.photoImageView.image = result

self.collectionView.reloadData()

})

}

}


fileprivate let collectionView: UICollectionView = {

let layout = UICollectionViewFlowLayout()

layout.scrollDirection = .horizontal

let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)

cv.translatesAutoresizingMaskIntoConstraints = false

cv.register(CustomCell.self, forCellWithReuseIdentifier: "PreviewCell")

cv.register(CustomCell.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "FooterCell")

return cv

}()


}


extension PhotoViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {


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

return CGSize(width: collectionView.frame.width/5, height: collectionView.frame.width/4)

}


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


if kind == UICollectionView.elementKindSectionFooter {

let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "FooterCell", for: indexPath) as! CustomCell

footer.backgroundColor = .blue

return footer

} else {

assert(false, "Unexpected element kind")

}

}

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

return 11

}


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


let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PreviewCell", for: indexPath) as! CustomCell

DispatchQueue.main.async {

cell.bgImage = self.photoImageView.image

}

return cell

}


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

return UIEdgeInsets(top: 10, left: 5, bottom: 10, right: 5)

}


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

return 1

}


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


return CGSize(width: view.frame.width, height: 10)

}


}


class CustomCell: UICollectionViewCell {


fileprivate let bg: UIImageView = {

let iv = UIImageView()


iv.translatesAutoresizingMaskIntoConstraints = false

iv.contentMode = .scaleAspectFit

iv.clipsToBounds = true

return iv

}()


var bgImage: UIImage? {

get {

bg.image

}

set {

bg.image = newValue

}

}


override init(frame: CGRect) {

super.init(frame: frame)


contentView.addSubview(bg)

bg.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true

bg.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true

bg.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true

bg.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true


}

required init?(coder: NSCoder) {

fatalError("init(coder:) has not been implemented")

}

}


extension PhotoViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {


func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {

if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {

photoImageView.image = image

}

dismiss(animated: true, completion: nil)

}


func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {

dismiss(animated: true, completion: nil)

}


}

Update: Not getting an error code anymore but the footer appears as the last item in the collectionView( as the 12th item in the collectionView), what could be the cause of that ?

If you can post the project somewhere or post an email address, that could be useful.


And please, format code with code formatter tool (<>)


import Foundation
import UIKit
import Photos

class PhotoViewController: UIViewController {

  @IBOutlet weak var photoImageView: UIImageView!
  @IBOutlet var photoView: UIView!
  @IBOutlet weak var menuStackView: UIStackView!

  var imagePicker = UIImagePickerController()

  override func viewDidLoad() {
    super.viewDidLoad()
 
    imagePicker.delegate = self
    assignTap()
 
    view.addSubview(collectionView)
    collectionView.backgroundColor = .white
    collectionView.topAnchor.constraint(equalTo: photoImageView.bottomAnchor, constant: 110).isActive = true
    collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10).isActive = true
    collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 10).isActive = true
    collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -103).isActive = true
 
    collectionView.clipsToBounds = true
 
    collectionView.dataSource = self
    collectionView.delegate = self
    collectionView.isHidden = false
 
  }

  @IBAction func previewTapped(_ sender: UIButton) {
    sender.pulsate()
    if collectionView.isHidden == true {
      collectionView.isHidden = false
    } else {
      collectionView.isHidden = true
    }
  }

  @IBAction func effectsTapped(_ sender: UIButton) {
    sender.flash()
  }

  @IBAction func exportTapped(_ sender: UIButton) {
    sender.shake()
  }


  func assignTap() {
    let tap = UITapGestureRecognizer(target: self, action: #selector(touchTapped(_:)))
    photoView.addGestureRecognizer(tap)
 
  }

  @objc func touchTapped(_ sender: UITapGestureRecognizer) {
    if menuStackView.isHidden == false {
      collectionView.isHidden = true
      menuStackView.isHidden = true
    } else {
      menuStackView.isHidden = false
      collectionView.isHidden = false
    }
  }

  func chooseImage(_ sender: Any) {
 
    if UIImagePickerController.isSourceTypeAvailable(.photoLibrary) {
      imagePicker.sourceType = .photoLibrary
      imagePicker.allowsEditing = true
      self.present(imagePicker,animated: true, completion: nil)
    }
  }

  func accessVideo(_ sender: Any) {
 
    if UIImagePickerController.isSourceTypeAvailable(.camera) {
      imagePicker.sourceType = .camera
      self.present(imagePicker, animated: true, completion: nil)
    } else {
      print("Camera not available")
    }
  }

  public func chooseLastImage(_ sender: Any) {
 
    print("photoImageView", photoImageView!)
    print("photoImageView.bounds", photoImageView.bounds)
 
    let targetSize: CGSize? = CGSize(width: photoImageView.bounds.width, height: photoImageView.bounds.height)
    let options = PHImageRequestOptions()
    options.isNetworkAccessAllowed = true
    options.deliveryMode = .highQualityFormat
 
    let imgManager = PHImageManager.default()
    let fetchOptions = PHFetchOptions()
    fetchOptions.sortDescriptors = [NSSortDescriptor(key:"creationDate", ascending: true)]
 
    let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
 
    print("targetSize for request", targetSize!)          //What is its value
    imgManager.requestImage(for: fetchResult.lastObject!, targetSize: targetSize!, contentMode: .default, options: options) { (result, info) in
   
      guard let result = result else { return }
   
      DispatchQueue.main.async(execute: {
        self.photoImageView.image = result
        self.collectionView.reloadData()
      })
    }
  }

  fileprivate let collectionView: UICollectionView = {
    let layout = UICollectionViewFlowLayout()
    layout.scrollDirection = .horizontal
    let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
    cv.translatesAutoresizingMaskIntoConstraints = false
    cv.register(CustomCell.self, forCellWithReuseIdentifier: "PreviewCell")
    cv.register(CustomCell.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "FooterCell")
    return cv
  }()

}

extension PhotoViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {

  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return CGSize(width: collectionView.frame.width/5, height: collectionView.frame.width/4)
  }

  func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
 
    if kind == UICollectionView.elementKindSectionFooter {
      let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "FooterCell", for: indexPath) as! CustomCell
      footer.backgroundColor = .blue
      return footer
   
    } else {
      assert(false, "Unexpected element kind")
    }
  }
  func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return 11
  }

  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
 
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PreviewCell", for: indexPath) as! CustomCell
    DispatchQueue.main.async {
   
      cell.bgImage = self.photoImageView.image
    }
    return cell
  }

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

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

  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
 
    return CGSize(width: view.frame.width, height: 10)
  }

}

class CustomCell: UICollectionViewCell {

  fileprivate let bg: UIImageView = {
    let iv = UIImageView()
 
    iv.translatesAutoresizingMaskIntoConstraints = false
    iv.contentMode = .scaleAspectFit
    iv.clipsToBounds = true
    return iv
  }()

  var bgImage: UIImage? {
    get {
      bg.image
    }
    set {
      bg.image = newValue
    }
  }

  override init(frame: CGRect) {
    super.init(frame: frame)
 
    contentView.addSubview(bg)
    bg.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
    bg.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
    bg.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
    bg.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true

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

extension PhotoViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {

  func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
  
    if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
      photoImageView.image = image
    }
    dismiss(animated: true, completion: nil)
  }

  func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
    dismiss(animated: true, completion: nil)
  }
}

Question: why do you dispatch on line 151 ?

Regarding line 151, I thought it would improve the UI performance. If it's not necessary, I will delete it.

The problem is that footer is a section property ; you cannot use it for appending some footer on each collection cell.


So :

- create a property in CustomCell to hold text, in the same way you created bg and bgImage

- add the view for the label and position it at the bottom anchor

- set the text value when you create the cell (in cellForItemAt indexPath)