yoKurt
yoKurt

Reputation: 537

Why is my collectionView nil when using a delegate?

I have a TableView containing 2 CollectionViews, each one in one of the TableViewCells. When I select an item in the first CollectionView, I want to update the second CollectionView. I am using a delegate pattern, but it doesn't work because my second CollectionView seems to be nil when I want use the .reloadData method, but why and how can I achieve updating the second CollectionView when selecting an item in the first CollectionView?

protocol TableViewCellDelegate {
func update()}

class TableViewCell: UITableViewCell, UICollectionViewDelegate, UICollectionViewDataSource {
var delegate: TableViewCellDelegate?

//...
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        self.delegate = ExistingInterestsCell() as? TableViewCellDelegate
        delegate?.update()}}


class ExistingInterestsCell: UITableViewCell, TableViewCellDelegate {
func update() {
    collectionView.reloadData()
}}

The error message is:

Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value: file Suggest_Accounts_and_Docs/ExistingInterestsCell.swift, line 13

Upvotes: 0

Views: 483

Answers (2)

DonMag
DonMag

Reputation: 77480

This is the wrong way to use the protocol / delegate pattern. You're creating classes that are too dependent on each other.

A better approach is to have your first row tell the controller that one of its collection view cells was selected, then allow the controller to update the data used for the second row, and then reload that row.

So, your "first row" cell will have this:

class FirstRowTableViewCell: UITableViewCell, UICollectionViewDelegate, UICollectionViewDataSource {
    
    var didSelectClosure: ((Int) -> ())?
    
    var collectionView: UICollectionView!

    // cell setup, collection view setup, etc...
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        // tell the controller that a collection view cell was selected
        didSelectClosure?(indexPath.item)
    }

}

Your "second row" cell will have this:

class SecondRowTableViewCell: UITableViewCell, UICollectionViewDelegate, UICollectionViewDataSource {
    
    var collectionView: UICollectionView!
    
    var activeData: [String] = []
    
    // cell setup, collection view setup, etc...
    
}

In your table view controller, you'll have a var such as:

var dataType: Int = 0

and your cellForRowAt will look something like this:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    if indexPath.row == 0 {
        let c = tableView.dequeueReusableCell(withIdentifier: "firstRowCell", for: indexPath) as! FirstRowTableViewCell

        // set the closure so the first row can tell us one of its
        //  collection view cells was selected
        c.didSelectClosure = { [weak self] i in
            guard let self = self else { return }
            // only reload if different cell was selected
            if i != self.dataType {
                self.dataType = i
                self.tableView.reloadRows(at: [IndexPath(row: 1, section: 0)], with: .automatic)
            }
        }
        return c
    }

    let c = tableView.dequeueReusableCell(withIdentifier: "secondRowCell", for: indexPath) as! SecondRowTableViewCell
    switch dataType {
    case 1:
        c.activeData = numberData
    case 2:
        c.activeData = wordData
    default:
        c.activeData = letterData
    }
    c.collectionView.reloadData()
    return c
}

Here is a complete example...

First Row Table Cell

class FirstRowTableViewCell: UITableViewCell, UICollectionViewDelegate, UICollectionViewDataSource {
    
    var didSelectClosure: ((Int) -> ())?
    
    var collectionView: UICollectionView!
    
    let myData: [String] = [
        "Letters", "Numbers", "Words",
    ]
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        
        let fl = UICollectionViewFlowLayout()
        fl.scrollDirection = .horizontal
        fl.minimumInteritemSpacing = 8
        fl.minimumLineSpacing = 8
        fl.estimatedItemSize = CGSize(width: 80.0, height: 60)
        
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: fl)
        
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        
        contentView.addSubview(collectionView)
        
        let g = contentView.layoutMarginsGuide
        
        // to avoid auto-layout warnings
        let hConstraint = collectionView.heightAnchor.constraint(equalToConstant: 60.0)
        hConstraint.priority = .defaultHigh
        
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: g.topAnchor),
            collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
            hConstraint,
        ])
        
        collectionView.register(SingleLabelCollectionViewCell.self, forCellWithReuseIdentifier: "cell")
        collectionView.dataSource = self
        collectionView.delegate = self
        
        collectionView.backgroundColor = .systemBlue
        
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return myData.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let c = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! SingleLabelCollectionViewCell
        c.theLabel.text = myData[indexPath.item]
        return c
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        // tell the controller that a collection view cell was selected
        didSelectClosure?(indexPath.item)
    }

}

Second Row Table Cell

class SecondRowTableViewCell: UITableViewCell, UICollectionViewDelegate, UICollectionViewDataSource {
    
    var collectionView: UICollectionView!
    
    var activeData: [String] = []
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        
        let fl = UICollectionViewFlowLayout()
        fl.scrollDirection = .horizontal
        fl.minimumInteritemSpacing = 8
        fl.minimumLineSpacing = 8
        fl.estimatedItemSize = CGSize(width: 80.0, height: 60)
        
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: fl)
        
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        
        contentView.addSubview(collectionView)
        
        let g = contentView.layoutMarginsGuide
        
        // to avoid auto-layout warnings
        let hConstraint = collectionView.heightAnchor.constraint(equalToConstant: 60.0)
        hConstraint.priority = .defaultHigh
        
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: g.topAnchor),
            collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
            hConstraint,
        ])
        
        collectionView.register(SingleLabelCollectionViewCell.self, forCellWithReuseIdentifier: "cell")
        collectionView.dataSource = self
        collectionView.delegate = self
        
        collectionView.backgroundColor = .systemRed
        
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return activeData.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let c = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! SingleLabelCollectionViewCell
        c.theLabel.text = activeData[indexPath.item]
        return c
    }
    
}

Table View Controller

class MyTableViewController: UITableViewController {
    
    let numberData: [String] = [
        "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15",
    ]
    let letterData: [String] = [
        "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O",
    ]
    let wordData: [String] = [
        "First", "Second", "Third", "Fourth", "Fifth", "Sixth",
    ]

    var dataType: Int = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.register(FirstRowTableViewCell.self, forCellReuseIdentifier: "firstRowCell")
        tableView.register(SecondRowTableViewCell.self, forCellReuseIdentifier: "secondRowCell")
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 2
    }
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if indexPath.row == 0 {
            let c = tableView.dequeueReusableCell(withIdentifier: "firstRowCell", for: indexPath) as! FirstRowTableViewCell

            // set the closure so the first row can tell us one of its
            //  collection view cells was selected
            c.didSelectClosure = { [weak self] i in
                guard let self = self else { return }
                // only reload if different cell was selected
                if i != self.dataType {
                    self.dataType = i
                    self.tableView.reloadRows(at: [IndexPath(row: 1, section: 0)], with: .automatic)
                }
            }
            return c
        }

        let c = tableView.dequeueReusableCell(withIdentifier: "secondRowCell", for: indexPath) as! SecondRowTableViewCell
        switch dataType {
        case 1:
            c.activeData = numberData
        case 2:
            c.activeData = wordData
        default:
            c.activeData = letterData
        }
        c.collectionView.reloadData()
        return c
    }
    
}

Collection View Cell (used by both rows)

// simple single-label collection view cell
class SingleLabelCollectionViewCell: UICollectionViewCell {
    let theLabel = UILabel()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        
        theLabel.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(theLabel)
        
        let g = contentView.layoutMarginsGuide
        
        NSLayoutConstraint.activate([
            theLabel.topAnchor.constraint(equalTo: g.topAnchor),
            theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            theLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            theLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor),
        ])
        
        theLabel.backgroundColor = .yellow
        
        contentView.layer.borderColor = UIColor.green.cgColor
        contentView.layer.borderWidth = 1
    }
    
}

The result:

enter image description here

tap on "Numbers" and we see:

enter image description here

tap on "Words" and we see:

enter image description here

Upvotes: 1

Shehata Gamal
Shehata Gamal

Reputation: 100503

This line

self.delegate = ExistingInterestsCell() as? TableViewCellDelegate

refers to a table cell with nil outlets , that's why it's crashes , you need to access a real presented one

Edit: Add a reference to your tableView inside both cell classes, and using the indexPath and the table get the needed cell and update it's collection

if let cell = table.cellForRow(at:Indexpath(row:0,section:0)) as? ExistingInterestsCell {
  // update model
  cell.collectionView.reloadData()
}

Upvotes: 0

Related Questions