How to dequeue UITableViewCells that conform to a defined protocol - ios

I'd like to make sure that my tableview only contains cells that conform to a said protocol. I simplified the implementation to illustrate the specific problem.
protocol ACommonLookAndFeel {
func configureMyLookAndFeel()
}
CellA: UITableViewCell, ACommonLookAndFeel
CellB: UITableViewCell, ACommonLookAndFeel
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell: UITableViewCell
if indexPath.row == 0 {
cell = tableView.dequeueReusableCellWithIdentifier("CellA", forIndexPath: indexPath) as! CellA
if let myCell = cell as? CellA {
myCell.configureMyLookAndFeel() // we need to call this for each cell
}
} else if indexPath.row == 1 {
cell = tableView.dequeueReusableCellWithIdentifier("CellB", forIndexPath: indexPath) as! CellB
if let myCell = cell as? CellB {
myCell.configureMyLookAndFeel() // we need to call this for each cell
}
}
return cell
}
The above code is working except that there is repeated code and I need to do the casting each time to access the configureMyLookAndFeel() method. As I want all my cells to be configured for the look and feel, I tried the code below instead but hit the compile error
Error: Cannot convert return expression of type 'protocol' to return type 'UITableViewCell'
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell: protocol <ACommonLookAndFeel>
if indexPath.row == 0 {
cell = tableView.dequeueReusableCellWithIdentifier("CellA", forIndexPath: indexPath) as! CellA
} else if indexPath.row == 1 {
cell = tableView.dequeueReusableCellWithIdentifier("CellB", forIndexPath: indexPath) as! CellB
}
cell.configureMyLookAndFeel() // works
return cell // Compiler Error !
}
Is there a way to fix this compiler error?
Ideally I wouldn't have like to avoid repeating the call to dequeueCell and the casting to CellA or CellB. I know what cell I need to cast it to based on the cellReuseIdentifier which is the same as my cell class name. Is there a way?
Thanks!

Try with this:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell: UITableViewCell
if indexPath.row == 0 {
cell = tableView.dequeueReusableCellWithIdentifier("CellA", forIndexPath: indexPath) // There is no need to cast here
} else if indexPath.row == 1 {
cell = tableView.dequeueReusableCellWithIdentifier("CellB", forIndexPath: indexPath) // There is no need to cast here
}
// The method will be called for all cells that conform to ACommonLookAndFeel.
// This is also safe, so no crash will occur if you dequeue a cell that
// doesn't conform to ACommonLookAndFeel. Depending on the behavior
// you want to achieve, you may want to use a ! instead of ? to force
// a crash in case of issues while developing your app.
(cell as? ACommonLookAndFeel)?.configureMyLookAndFeel()
// You have to return a UITableViewCell, not a ACommonLookAndFeel
return cell
}

Related

Content views are not displayed on the UITableViewCell

This is a part of my storyboard:
this is my running app:
This is my part of codes:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
if indexPath.row == 0 {
return super.tableView(tableView, cellForRowAt: indexPath)
} else {
tableView.register(SubTextFieldCell.self, forCellReuseIdentifier: "SubTextFieldCell")
let cell = tableView.dequeueReusableCell(withIdentifier: "SubTextFieldCell", for: indexPath) as! SubTextFieldCell
// cell.deleteButton.isEnabled = true
// cell.subTextfield.text = "OK"
print("indexPath.row: \(indexPath.row)")
return cell
}
...
I have already connected the button and the textfield in various places and I can guarantee that this part is not wrong, but when I click the Add button in the first row, I only get a cell without any content.
If I use code like this cell.deleteButton..., Xcode will report an error:
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an
Optional value
Then I tried to use the viewWithTag method to see if show the content, but I still get the same error as before.
This is the first time I have encountered this kind of error. I have no error with similar code and methods in my other programs.
When you configure custom cells inside a storyboard file, you don't need to call register(_:forCellReuseIdentifier:) because the storyboard should have done that for you.
The reason deleteButton is nil is because by re-registering the cell class as you did, you overwrote what the storyboard registered for you. All cells created by dequeueing with that reuse identifier will have no connection to the storyboard and simply be empty.
Assuming all the #IBOutlets and reuse identifiers and things are set up (which you said you did), then simply dequeue the cell with the reuse identifier set up in storyboard.
Dequeue Cell Example:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
if indexPath.row == 0 {
return super.tableView(tableView, cellForRowAt: indexPath)
} else {
// Registering again is unnecessary, because the storyboard should have already done that.
// tableView.register(SubTextFieldCell.self, forCellReuseIdentifier: "SubTextFieldCell")
let cell = tableView.dequeueReusableCell(withIdentifier: "SubTextFieldCell") as! SubTextFieldCell
cell.deleteButton.isEnabled = true
cell.subTextfield.text = "OK"
return cell
}
} else {
...
}
}
Note:
Even in cases where you do need to register a class with a table view, you should only have to do this once. (For example, during viewDidLoad)
Even in those times, you should not call it every time you dequeue a cell. You're just making your app work harder.
Connecting views to cells in Storyboard
Set a subclass to the table view
Set a subclass to the first prototype cell
Set a reuse identifier to the prototype cell
Make sure subview (UIButton, etc.) is connected to property with #IBOutlet (subclass code shown below)
Example UITableViewController subclass:
class MyTableViewController: UITableViewController {
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 cell = tableView.dequeueReusableCell(withIdentifier: "MyFirstCell", for: indexPath) as! MyFirstTableViewCell
// Configure cell if needed
cell.myButton.setTitle("New Button Text", for: .normal)
cell.myButton.tintColor = .green
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "MySecondCell", for: indexPath) as! MySecondTableViewCell
// Configure cell if needed
cell.myTextField.backgroundColor = .red
return cell
}
}
}
Example UITableViewCell subclass:
class MyFirstTableViewCell: UITableViewCell {
#IBOutlet weak var myButton: UIButton!
}
Result:

UITableView lags when scrolling with multiple cell types

I know there are a lot of questions regarding this topic but bear with me cause I didn't find any solution to my specific problem.
First of all, my entire app is in code (no storyboard). I am creating a tableView (where I get data from a backend) with multiple cell types. The cells are in constant order meaning that the first cell will always be the same type as any other first cell (not sure if you get my point but technically the cell types are constant to a specific indexPath.
So the problem is I have methods that dequeues the specific cell for each indexPath. In one of my cells, I have a MapboxStatic map, which returns an image of a map from Mapbox's servers. Let's say the cell is off the screen at initial run time, if I scroll fast enough, the tableView will hold off for a second or so to load the image returned (I tried loading it asynchronously on the main queue still with no changes).
Now my other problem is with the UberRides button, it also lags when scrolling (same issue).
It's worth mentioning that if I scroll through the tableView once (with the lag), and I scroll up and back down it does not lag anymore but I guess it's because the dequeued cell is still in memory.
If you need any other details please leave a comment.
[EDIT] Code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 1 {
return reserveTableViewCell(at: indexPath)
} else if indexPath.section == 2 {
return dateTimeTableViewCell(at: indexPath)
} else if indexPath.section == 3 {
return descriptionTableViewCell(at: indexPath)
} else if indexPath.section == 4 {
if indexPath.row == 0 {
return mapTableViewCell(at: indexPath)
} else {
return uberTableViewCell(at: indexPath)
}
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: defaultCell, for: indexPath)
return cell
}
}
private func reserveTableViewCell(at indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reserveCell, for: indexPath) as! ReserveTableViewCell
return cell
}
private func descriptionTableViewCell(at indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: descriptionCell, for: indexPath) as! DescriptionTableViewCell
return cell
}
private func dateTimeTableViewCell(at indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: dateTimeCell, for: indexPath) as! DateTimeTableViewCell
return cell
}
private func mapTableViewCell(at indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: mapCell) as! MapTableViewCell
cell.detailController = self
return cell
}
private func uberTableViewCell(at indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: uberCell, for: indexPath) as! UberTableViewCell
return cell
}
Best
Well, creating your cells is slow. It's hard to give you any suggestions now knowing what takes so much time. Use time profiler to determine the problematic code.
If you do not need overlays, you could try to use MGLMapSnapshotter. Here is an example of how a MGLMapSnapshot can be used with a table view.

ios swift making table list keep getting error about nill

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellReuseIdentifier") as! CustomTableViewCell
let text = data[indexPath.row]
cell.label.text = text
return cell
}
above on is the code that I saw to follow.
my code is as below
I don't know why it getting nil value on
tableView.dequeueReusableCell(withIdentifier: "locCell")
my storyboard is as below
I added identifier like below(you can see it on bottom-right section of pic
You need to register the cell for reuse.
tableView.register(LocationTableCell.self, forCellReuseIdentifier: "locCell")
Or enter your reuse identifier in the storyboard by selecting your cell and then entering the reuse identifier in the properties to the right.
Simply because tableView.dequeueReusableCell(withIdentifier: "cell") by default is nil.
It is the same case for any optional when trying to print it out, example:
let optionalString: String? = ""
print(optionalString)
leads to get:
So, by declaring a constant as:
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
since dequeueReusableCell(withIdentifier:) returns an optional UITableViewCell instance, the type of cell would be UITableViewCell? (optional UITableViewCell), that's why you are seeing this error.
How to get rid of it?
Assuming that you have set the cell right identifier for your cell:
Well, in case of having your custom cell, you could cast it as:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as? MyCustomCell else {
// something goes wrong
return UITableViewCell()
}
print(cell) // it would be fine for now
// ...
return cell
}
And if you don't have a custom cell, all you have to do is to remove the as? MyCustomCell down casting.
Replace
let cell = tableView.dequeueReusableCell(withIdentifier: "locCell")
With this code:
let cell = tableView.dequeueReusableCell(withIdentifier: "locCell", for: indexPath)
func tableView (_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "cellReuseIdentifier", for: indexPath) as! CustomTableViewCell
cell.label.text = data[indexPath.row].name
return cell
}
NOTE: In your storyboard set tableView Delegate, DataSource and set cell ID cellReuseIdentifier.

dequeueReusableCellWithIdentifier:forIndexPath: VS dequeueReusableCellWithIdentifier:

I have read this question and think that I understand the difference between the two methods until I find a strange example:
Set table view cell's style be Basic, Identifier be Cell in Storyboard, code as below:
import UIKit
class TableViewController: UITableViewController {
var items: [String]!
override func viewDidLoad() {
super.viewDidLoad()
items = ["first", "second", "third"]
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// either works fine
let cell = tableView.dequeueReusableCellWithIdentifier("Cell")! // let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
cell.textLabel?.text = items[indexPath.row]
return cell
}
}
Very simple, but when I change the tableView:cellForRowAtIndexPath: method to 1, 2, 3, 4 cases respectively:
Case 1:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
cell.textLabel?.text = items[indexPath.row]
return cell
}
Case 2:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
cell = tableView.dequeueReusableCellWithIdentifier("Cell")!
cell.textLabel?.text = items[indexPath.row]
return cell
}
Case 3:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("Cell")!
cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
cell.textLabel?.text = items[indexPath.row]
return cell
}
Case 4:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("Cell")!
cell = tableView.dequeueReusableCellWithIdentifier("Cell")!
cell.textLabel?.text = items[indexPath.row]
return cell
}
Case 1, 2 (doesn't work):
Case 3, 4 (works fine):
How to explain? I think it really helps to understand these two methods from another perspective, any opinion is welcome.
In each case, you are dequeueing two cells for each row. In cases 1 and 2, you call the ("Cell", forIndexPath: indexPath) version first. In this case the table view ends up with two cells for each row, one completely overlapping and obscuring the other. You can see this in the view inspector since you can amend the angle of view to see behind:
(I amended the cellForRowAtIndexPath code like this:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("plainCell", forIndexPath: indexPath)
cell.textLabel!.text = "First cell for row \(indexPath.row)"
cell = tableView.dequeueReusableCellWithIdentifier("plainCell", forIndexPath: indexPath)
cell.textLabel!.text = "Second cell for row \(indexPath.row)"
print("Cell being returned is \(cell)")
return cell
}
to given different text labels to each cell.) In cases 3 and 4, where you call the ("Cell") version first, the table view has only one cell for each row.
Why the different behaviour? If you create a custom subclass of UITableViewCell and use that in your storyboard, you can then override various methods and add print() statements to see what's happening. In particular, awakeFromNib, didMoveToSuperView, and deinit. What transpires is that in cases 1 and 2, the first cell is created (awakeFromNib) and immediately added (didMoveToSuperView) to a superview, presumably the table view or one of its subviews. In cases 3 and 4, the first cell is created but is not added to a superview. Instead some time later, the cell is deallocated (deinit).
(Note that if the second cell is dequeued using the ("Cell", forIndexPath: indexPath) version, it too is added immediately to a superview. However, if the second cell is dequeued using the ("Cell") version, it is only added to a superview after the cellForRowAtIndexPath method has returned.)
So the key difference is that the ("Cell", forIndexPath: indexPath) version results in the cell being added immediately to the table view, before even the cellForRowAtIndexPath has completed. This is hinted at in the question/answer to which you refer, since it indicates that the dequeued cell will be correctly sized.
Once added to the superview, the first cell cannot be deallocated since there is still a strong reference to it from its superview. If the cells are dequeued with the ("Cell") version, they are not added to the superview, there is consequently no strong reference to them once the cell variable is reassigned, and they are consequently deallocated.
Hope all that makes sense.
dequeueReusableCellWithIdentifier: doesn't give you guarantees: cells could be nil, so you have to check if your cell is nil and handle it properly or your app will crash.
dequeueReusableCellWithIdentifier:forIndexPath:, on the other hand, does check this for you (it always return a cell).
For your particular case (Swift), this means you can safely force-unwrap the cell with dequeueReusableCellWithIdentifier:forIndexPath:, while you'll have to use the if let syntax with the second one.
Example codes (in Objective-C, I don't use Swift)
dequeueReusableCellWithIdentifier:forIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" atIndexPath:indexPath];
// Here we know the cell is not nil (....atIndexPath: ensures it)
cell.textLabel.text = items[indexPath.row];
return cell;
}
dequeueReusableCellWithIdentifier:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
// You asked for a cell, but you don't know if it is nil or not
// In Swift, here the cell should be a conditional
// First, check if the cell is nil
if ( cell == nil ) {
// Cell is nil. To avoid crashes, we instantiate an actual cell
// With Swift conditional should be something similar
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"Cell"];
}
// Here you're sure the cell is not nil
// If condicional, you probably will write cell?.textLabel?.text = items[indexPath.row];
cell.textLabel.text = items[indexPath.row];
// Finally, you return the cell which you're 100% sure it's not nil
return cell;
}

2 custom UITableViewCell in one UITableView Swift

I am trying to implement a custom table view which has different types of cells: type A and type B. All of my cells should be of type A, except for one that will be of type B. Whenever the users selects a cell, this one changes to type B.
My code is the following one:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let row = indexPath.row
if (row == typeBCellIndex) {
// Get Type B cell
let cell = tableView.dequeueReusableCellWithIdentifier(typeBCellIdentifier, forIndexPath: indexPath) as! TypeBTableViewCell
return cell
} else {
// Get Type A cell
let cell = tableView.dequeueReusableCellWithIdentifier(typeACellIdentifier, forIndexPath: indexPath) as! TypeATableViewCell
cell.detailLabel.text = "I am a type A cell"
return cell
}
}
The variable typeBCellIndex is initialised in 0, and this code gives an error when I add a new cell and try to dequeue the cell at index 1.
In Objective-C, as this links indicates, I would check if the cell is nil, and if not create a new instance of the corresponding cell. However, I am not sure if this concept applies to Swift, and in case it does, I don't know how to do it.
declare a variable cellindex before viewdidload method and initialize to 3 or any number
and in tableview design two different cells and assign unique identifier for each
code for tableview is
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell : UITableViewCell!
var row = indexPath.row
if(row == cellindex){
cell = tableView.dequeueReusableCellWithIdentifier("Cell2", forIndexPath: indexPath) as! UITableViewCell
}else{
cell = tableView.dequeueReusableCellWithIdentifier("Cell1", forIndexPath: indexPath) as! UITableViewCell
}
return cell
}
the cellindex row will be cell2 and other cells are cell1

Resources