Reputation: 537
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
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:
tap on "Numbers" and we see:
tap on "Words" and we see:
Upvotes: 1
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