In Swift with dequeueReusableCell API we don't have control over creating of a new instance of TableViewCell. But what if I need to pass some initial parameters to my custom cell? Setting parameters after dequeue will require a check if they have been already set and seem to be uglier than it was in Objective-C, where it was possible to create custom initializer for a cell.
Here is a code example of what I mean.
Objective-C, assuming that I don't register a class for the specified identifier:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString* reuseIdentifier = #"MyReuseIdentifier";
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier];
if (!cell)
{
cell = [[MyTableViewCell alloc] initWithCustomParameters:...]; // pass my parameters here
}
return cell;
}
Swift:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyReuseIdentifier")
if let cell = cell as? MyTableViewCell {
// set my initial parameters here
if (cell.customProperty == nil) {
cell.customProperty = customValue
}
}
}
Do I miss something or it's how it supposed to work in Swift?
In swift or objective-c dequeueReusableCell will return a cell if there is an available 1 or will create another if there isn't , btw what you do in objc can be done in swift it's the same
Always before UITVCells will reuse inside your Cell class will prepareForReuse() called. You can use this method to reset all content like imageView.image = nil.
Use the initial from UITVCell init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) to know if the cell was created.
If you want to know this infomations inside your tableView class, use func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) delegate method.
PS: Dont forget to call super.
The working approach is basically the same as Objective-C: Do NOT register cell for "MyReuseIdentifier" and use dequeueReusableCell(withIdentifier: )
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "MyReuseIdentifier")
if cell == nil {
cell = MyTableViewCell.initWithCustomParameters(...)
}
return cell
}
Related
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:
I have a UIView in a custom view that I built using xib. I need to display a UITableView in the said view. At first I thought about placing a container and embedding a UITableViewController in it. Turns out I cannot place containers in a xib file, or atleast there's no way of doing it from the IB as it doesn't show up in views section at the lower right.
I can create a UITableView programmatically and add it as a subview of the view. It shows up as expected but I cannot seem to be able to add cells in it. I also tried creating a well behaving UITableViewController in association with a storyboard view, instantiate that controller as follows:
let storyboard = (UIStoryboard(name: "Main", bundle: nil))
let vc = storyboard.instantiateViewControllerWithIdentifier("tableViewController") as! TestTableViewController
and then tried accessing the UITableView's outlet which was nil. Then I read somewhere that I should do a vc.loadView() because as the name suggests, it loads the view and my IBOutlet would not be nil. This worked. The outlet was on longer nil. But, when I add the table in the container view as a subview, it still shows no cells. There are only separator lines but no content. I've run out of ideas!
EDIT
I do not have any UITableViewCell implementations as the tables are static.
Good approach is to use UITableview inside your custom view:
if you are adding tableview programmatically then register your cell using Nib or UITableview subclass like
tableView.registerNib(UINib(nibName: "UITableViewCellSubclass", bundle: nil), forCellReuseIdentifier: "UITableViewCellSubclass")
for if you are creating UITableviewCell using Xib.
tableView.registerClass(UITableViewCellSubclass.self, forCellReuseIdentifier: "UITableViewCellSubclass") // using code.
tableView.delegate = self
tableView.dataSource = self
and then use 2 required delegates.
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return tableView.dequeueReusableCellWithIdentifier("UITableViewCellSubclass", forIndexPath: indexPath) as! UITableViewCellSubclass
}
hope i answered your question.
Objective c
You need delegates in your viewcontroller, if you have a viewcontroller, put delegate of table :
UIViewController
UITableViewDelegate
UITableViewDataSource
And you can use your functions
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1; //count of section
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [catagorry count]; //count number of row from counting array hear cataGorry is An Array
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *MyIdentifier = #"MyIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:MyIdentifier] autorelease];
}
// Here we use the provided setImageWithURL: method to load the web image
// Ensure you use a placeholder image otherwise cells will be initialized with no image
[cell.imageView setImageWithURL:[NSURL URLWithString:#"http://example.com/image.jpg"]
placeholderImage:[UIImage imageNamed:#"placeholder"]];
cell.textLabel.text = #"My Text";
return cell;
}
Swift :
delegate:
UIViewController
UITableViewDataSource
UITableViewDelegate
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
Swift
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(textCellIdentifier, forIndexPath: indexPath)
let row = indexPath.row
cell.textLabel?.text = swiftBlogs[row]
return cell
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(textCellIdentifier, forIndexPath: indexPath)
let row = indexPath.row
cell.textLabel?.text = swiftBlogs[row]
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return swiftBlogs.count
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
Look more More inf
I am coming to you for a little issue I have, that might actually not be that complicated but that i have struggled about for now about an hour...
I defined a TableViewCell in a TableViewController. This TableViewCell has three UIImage and two textLabels. I implemented the following methods :
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return fruits.count
}
(which in my sense defines the number of rows)
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "friendListCell")
cellOutlet.nameTextLabel.text = fruits[indexPath.row]
return cell!
}
On this second method, the "cellOutlet" is an instance of the class that defines my cell, in which I modify one of the textLabel's text.
"fruits" is a simple array of Strings that are supposed to be displayed in every cell of my app one by one.
Compilation goes fine, but when accessing the page in question, the app crashes saying
unexpectedly found nil while unwrapping an Optional value
My question is the following :
Would you have any idea of what is done wrong in term of accessing my textLabel's text ?
Thanks in advance !
If you're using a custom TableViewCell class you have to
Set the class of the cell in Interface Builder to the custom class
Cast the cell accordingly in cellForRow... (assuming the name is MyTableViewCell)
let cell = tableView.dequeueReusableCell(withIdentifier: "friendListCell" for:indexPath) as! MyTableViewCell
Try to call
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "friendListCell")
cell.nameTextLabel.text = fruits[indexPath.row]
return cell!
}
and also check "cell" should not be nil at the time of calling cell.nameTextLabel.text = fruits[indexPath.row]
Even if your custom UITableViewCell class (e.g. FruitsCell) has some labels, the standard UITableViewCell doesn't (apart from one, which is its 'label' property). In your code, the 'cell' property is of type UITableViewCell, which has no cellOutlet or nameTextLabel property to access.
If you set up your custom cell in IB, be sure to connect all your labels from IB with the code in your Swift-File, e.g. FruitsCell.swift, that contains the FruitsCell class.
Then cast the 'cell' in your ViewControllers delegate method as e.g. FruitCell instead of UITableViewCell. Only then you can access the labels of that class that you set up in IB.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// casting to your Class
let cell = tableView.dequeueReusableCell(withIdentifier: "friendListCell") as! FruitCell
// accessing that Class' properties
cell.nameTextLabel.text = fruits[indexPath.row]
// returning cell of type FruitCell
return cell!
}
I have a custom UITableViewCell with a UILabel called CellLabel. I want to be able to access this label at a certain tableView section e.g. section 3 and provide it a string to display without the use of an array and arrayName[indexPath.section].
You can catch it in cellForRowAtIndexPath override of a TableView
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:UITableViewCell = tableView.dequeueReusableCellWithIdentifier("YourCustomCellRegisterName") as YourCustomCell
if(indexPath.section == 3){
cell.CellLabel.text = "foo"
}
return cell
}
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;
}