Search code examples
swiftuiviewuicollectionviewuicollectionviewcellxib

Subviews are nil when UICollectionView and UICollectionViewCell are in XIB files


Hi there! I've created a sample project that shows my problem exactly. Click to download!

I've created an app which contains a subclass of UICollectionViewCell, it contains an ImageView as subview. It also has a XIB file containing the view.

When I use this UICollectionViewCell within a UICollectionView that is directly embedded in a ViewController, the cell works just fine. Here's the code of the WorkingViewController containing the code that registers the cell:

class WorkingViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
  @IBOutlet weak var collectionView: UICollectionView!

  override func viewDidLoad() {
    super.viewDidLoad()

    let cellNib = UINib(nibName: "ImageCell", bundle: nil)
    collectionView.register(cellNib, forCellWithReuseIdentifier: "ImageCell")

    collectionView.reloadData()
  }

  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
  }

  func randomInt(min: Int, max: Int) -> Int {
    return Int(arc4random_uniform(UInt32(max - min + 1))) + min
  }

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

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

  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCell", for: indexPath) as? ImageCell else {
      fatalError("Could not load cell")
    }

    let randomNumber = randomInt(min: 1, max: 10)
    cell.imageView.image = UIImage(named: "stockphoto\(randomNumber)")

    return cell
  }
}

Now move the Storyboard Entry Point over to the BuggyViewController. Here the UICollectionView is not directly embedded. It is contained within a UIView that also has an XIB. Here's the code for that view, containing the registering of the cell from earlier:

class DetailView: UIView, UICollectionViewDelegate, UICollectionViewDataSource {
  @IBOutlet var contentView: UIView!
  @IBOutlet weak var collectionView: UICollectionView!

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

    setup()
  }

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)

    setup()
  }

  private func setup() {
    Bundle.main.loadNibNamed("DetailView", owner: self, options: nil)

    self.addSubview(self.contentView)
    self.contentView.frame = self.bounds
    self.autoresizingMask = [.flexibleWidth, .flexibleHeight]

    collectionView.delegate = self
    collectionView.dataSource = self

    let cellNib = UINib(nibName: "ImageCell", bundle: nil)
    collectionView.register(cellNib, forCellWithReuseIdentifier: "ImageCell")
    collectionView.register(ImageCell.self, forCellWithReuseIdentifier: "ImageCell")

    collectionView.reloadData()
  }

  func randomInt(min: Int, max: Int) -> Int {
    return Int(arc4random_uniform(UInt32(max - min + 1))) + min
  }

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

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

  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCell", for: indexPath) as? ImageCell else {
      fatalError("Could not load cell")
    }

    let randomNumber = randomInt(min: 1, max: 10)
    cell.imageView.image = UIImage(named: "stockphoto\(randomNumber)")

    return cell
  }
}

But this results in an error in the DetailView, line 55:

Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

So here the imageView of the cell is suddenly nil which makes me think I did something wrong registering the cell or instantiating it. But I just cannot resolve it.


Solution

  • You should delete the line where you register the cell class:

    collectionView.register(ImageCell.self, forCellWithReuseIdentifier: "ImageCell")
    

    Indeed you've defined your cell in a Nib and should register it through the method func register(_ nib: UINib?, forCellWithReuseIdentifier identifier: String). Registering it through the cellClass method will return nil.

    And as the Apple doc states:

    If you previously registered a class or nib file with the same reuse identifier, the class you specify in the cellClass parameter replaces the old entry. You may specify nil for cellClass if you want to unregister the class from the specified reuse identifier.

    This explains why you were getting nil.