Does indexpath.row take care of iterating an array? - ios

I'm working through a UITableView tutorial and I've become curious about array iteration as I implement the UITableViewDataSource methods. When we call indexPath.row, is this calling a for loop for us behind the scenes? I'm asking because months back when I was learning to use data from a webservice (I've forgotten the exact steps of how I did it precisely) but I believe I needed to iterate over the array in order to present the information in the console.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Create an instance of UITableViewCell, with default appearance
let cell = UITableViewCell.init(style: .value1, reuseIdentifier: "UITableViewCell")
// Set the text on the cell with the description of the item instance that is at the nth index of the allItems array, where n = row this cell will appear in on the tableview
let item = itemStore.allItems[indexPath.row]
cell.textLabel?.text = item.name
cell.detailTextLabel?.text = "$\(item.valueInDollars)"
return cell
}
Am I correct in thinking that the indexPath.row method is iterating over the array for us behind the scenes?
let item = itemStore.allItems[indexPath.row]

No, calling indexPath.row does not iterate through all rows for you. It's just a variable sent to cellForRowAt that you can access and use to create your cell. It contains information about which row in which section the function cellForRowAt was called for.
However, cellForRowAt is actually called every time a cell is going to be visible on your tableVIew. Imagine you have a dataSource with 100 items and it is possible to only see 10 cells at a time. When the tableView gets initially loaded, cellForRowAt will get called 10 times. Then, if you scroll your tableView to show 3 more cells, cellForRowAt will get called 3 more times.

First of all the tutorial seems to be pretty bad. Table view cells are supposed to be reused
let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
The workflow to display table view cells is:
The framework calls numberOfRowsInSection (and also numberOfSections)
For each section/row pair in the range of the visible cells it calls cellForRowAt and passes the index path in the second parameter.

No. The indexPath is just a struct with a section and a row. You use those to directly look up the information from your array. No iteration is happening.
cellForRowAt is only called for the cells that are on screen, so the order they are called depends on which direction you are scrolling.
In fact, the UITableView doesn't even know if you are using an array, or generating the information on the fly. What you do with the indexPath row and section is up to you.

Check the documentation of UITableView and cellForRowAtIndexpath functions.
An index path locating a row in tableView.
IndexPath will be a structure, which helps you to get the specific location from a nested array. In your case of the table view, rows inside the section.
cellForRow will return a cell for particular index path(in the specified section and row)
So indexPath.section will give the section number for which the cell is going to be modified and returned to the data source. And Inside the section, and indexPath.row will be the corresponding row inside that section.
index path documentation
tableView-CellForRowAt: IndexPath
UITableView DataSource Documentation
[update]
If you want to add multiple sections, add another dataSource numberOfSections, and return the number of sections count from it.
I can link to a create a table view tutorial from Apple, but it's a long document, you can skip the starting and read the To display a section in your table view.
If you want, you can use a 2D array for keeping sections and rows. And in numberOfSections method return array.count, and in numberOfRows:inSection, return array[section].count
More examples,
example
example
thanks,
J

Related

Difference between tableView.cellForRow(at:) and tableView.dataSource?tableView(tableView:cellForRowAt:)

I'm unit testing on a tableView whether it renders a cell.
And I found that tableView.cellForRow(at:) returns nil, while tableView.dataSource?tableView(tableView:cellForRowAt:) returns the right cell.
Here's my unit test code.
it("renders one option text") {
let indexPath = IndexPath(row: 0, section: 0)
let cell = sut.tableView.dataSource?.tableView(sut.tableView, cellForRowAt: indexPath)
let cell2 = sut.tableView.cellForRow(at: indexPath)
expect(cell?.textLabel?.text).toEventually(equal("A1")) // test suceeded
expect(cell2?.textLabel?.text).toEventually(equal("A1")) // test failed
}
So I'm curious about the difference of the two methods.
Apple's document says that tableView.cellForRow(at:) returns nil if the cell is not visible, so I'v understood that tableView.cellForRow(at:) returns nil when it's under unit testing,
but I'm not sure the time order of the two methods being called and when tableView.cellForRow(at:) get the right value(cell).
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
This method is used to generate or dequeue the cells as required by tableView. This is not the UITableView member method. Instead, it is a protocol method and another object, which will be a data source, will implement and return the value. So it will always return a cell whether we are unit testing or while debugging the app.
tableView.cellForRow(at:)
This method is not the generator method. It is a member method of UITableView as a utility method for eg. for getting selected row we use tableView.selectedRow. So it is supposed to return cell for any indexPath.
As we know UITableView doesn't create cells equal to rows drawn. Suppose you wanted to draw 100 rows then UITableView only create few extra cells apart from cells which are visible. So if you pass any indexPath which is not among the visible rows then practically that cell doesn't exist. Because tableview is waiting for you to scroll and reuse the unused cells. So whether you are doing unit testing or working on app it will always show nil for cells which are not visible.
tableView.dataSource?tableView(tableView:cellForRowAt:) will always dequeue a new cell. It isn't the one on display unless tableView is the one that called it.

Must I keep my own UITableView "isSelected" array for multiselect?

I am trying to use a UITableview with multiple selection on and a check mark accessory view for selected rows. This is mostly working if I turn on and off the accessory view in tableView:didSelectRow.
However, I tried to build a selectAll method, and I found that the array of selected cells was being cleared after I had spun through all the cells and selected them if I then call reloadData().
I suspect reloading the table clears selection. I don't know of any other way to have all the cells drawn after I set the selected flag and accessory view.
I am wondering if I need to keep my own array of selected rows. Has anyone else built something like this? Its seems like a common scenario.
Any tips or sample code appreciated.
Take an Array and add the indexPath of each selected cell into it and put a condition in cellForRowAt... that if the Array contains that particular indexPath, set it as selected.
There are two approaches you can take. One is to track the selected row numbers. To do this, you can use an NSMutableIndexSet or its Swift counterpart IndexSet.
Essentially, when a row is selected, add it to the set. When you deselect it, remove it from the set. In cellForRowAtIndexPath you can use containsIndex to determine if a check mark should be shown or not.
Since you explicitly mention an issue with selection when you reload the table, it is worth considering the issue with storing row numbers (whether in a set or an array), and that is that row numbers can change.
Say I have selected rows 4,7 and 9 and these values are stored in the index set. When I reload the data, new data may have been inserted after the "old" row 8, so now I should be selecting rows 4,7 and 10, but I will be selecting 4,7 and 9 still.
A solution to this is to store some sort of unique identifier for the data that should be selected. This will depend on your data, but say you have a string that is unique for each item. You can store this string in a NSMutableSet or Swift Set, which again makes it easy to check if a given item is selected using contains
you need add some functionality in cellForRowAtIndexPath method like this ang your view controller code like this
let we take one example of photo gallery application
class CreateEvent: UIViewController,UITableViewDataSource,UITableViewDelegate {
var yourArray : [Photo] = [Photo]()
//MARK: - Content TableView Methods
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CustomCell", forIndexPath: indexPath)
let objPhoto = yourArray[indexPath.row]
if objPhoto.isPhotoSelected == true
{
cell.accessoryType = .Checkmark
}
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
{
let objPhoto = yourArray[indexPath.row]
objPhoto.isPhotoSelected = true
let cell = tableView.cellForRowAtIndexPath(indexPath)
cell.accessoryType = .Checkmark
}
//MARK: Action Method
func selectAllPhoto()
{
for objPhoto in yourArray
{
objPhoto.isPhotoSelected = true
}
tableView.reloadData()
}
}
and one more thing you need to create your custom object like
class Photo: NSObject {
var photoName:String = ""
var isPhotoSelected = false
}
hope this will help you
The best approach for multiple selection is
Take a model object, in that take all your attributes and one extra boolean attribute (like isSelected) to hold the selection.
In case of selecting a row
Fetch the relevant object from the array
Then update the isSelected boolean (like isSelected = !isSelected) and reload table.
In case of select all case
Just loop through the array.
Fetch the model object from array.
make the isSelected = true.
After completion of loop, reload the table.

Swift - UITableViewCell with custom class and prototype repeating after 4th cell

In my application, I have a UITableView which dynamically creates new cells as the user clicks on an "add" button. My cells have several fields that are intended to take user input. However, after creating a fourth cell, the cell contains duplicates of the input added in the first cell. For example, say each cell had a textfield
FirstCell.textfield.text = 0 <--- manually assigned
SecondCell.textfield.text = 1 <--- ..
ThirdCell.textfield.text = 2 <---- ..
FourthCell.textfield.text = 0 <--- automatically assigned
FifthCell.textfield.text = 1 <--- automatically assigned
After some digging, I believe this is due to the cells being dequeued using a reuse identifier and the cells being reused. How can I create multiple cells from the same prototype, but do not automatically hold the manually assigned values from the previous cell?
Update:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("MyCustomCell", forIndexPath: indexPath) as! CustonUITableViewCellClass
cellB.delegate = self
return cell
}
I tried assigning each cell's UI element in this function according to indexPath.row, but it doesn't seem to be working. It'd be working fine until I start scrolling after adding 4 rows, the cell in the first row would return indexPath.row = 4 and all the UI elements in the first row would be assigned to the value inputted on the fourth row.
As you said, your cells are being reused and that's why you are experiencing this weird behavior. You need to hold on to the assigned values for each cell and clean/set each cell every time it is reused in cellForRowAtIndexPath.
Since you have a textField you could use the delegate to respond to it's text being changed and save that value in an array, then every time a cell is reused look for that value and set it.

iOS tableview cell is empty at random

Screenshot of weird behavior
The screenshot tells is quite well. I have a tableview with dynamic custom cells. I added a println for one of the contents of the cell to check, if the labels are set. I can see in the debug log, that each cell has its content. Still, on the device there are empty cells at random, which means, the row, where no content appears, is changing a lot. Even just scrolling up and down makes the second row disappear, but the third row is filled. Scrolling again turns this around again. If I close the app and start it again, every row is filled correctly.
Here is the code for the cell generation:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Return a count picker cell
if countPickerTableRow == indexPath.row {
...
}
// Return a normal wish list entry cell
else {
let article = wishListEntries[indexPath.row]!
let cell = tableView.dequeueReusableCellWithIdentifier("ArticleCell", forIndexPath: indexPath) as! WOSArticleCell
// Correct the order in case a count picker cell was inserted
var row = indexPath.row
if countPickerTableRow != -1 && indexPath.row > countPickerTableRow {
row--
}
cell.setThePreviewImage(UIImage(data: article.thumbnail))
cell.setArticleName(article.name)
cell.setArticleDescription(article.text)
cell.setArticleNumber(article.number)
cell.setArticleCount(article.count as Int)
cell.setOrderInTable(row)
cell.setTableViewController(self)
cell.setNeedsDisplay()
cell.setInputAccessoryView(numberToolbar) // do it for every relevant textfield if there are more than one
println(String(indexPath.row) + " " + cell.nameLabel.text!)
return cell
}
}
In the custom cell class there is nothing special. Just a few outlets to the labels.
Here is a screen of the storyboard:
Storyboard
Can anyone please help me finding out what is going on here? I can't find the reason why the debug log can output the contents of a cell, but the device is not able to render them.
You should change the logic of your code. If the PickerCell comes up just call reloadData() and reload everything in the tableview. If the amount of rows you have is small this won’t be an issue and it’s not an expensive operation as you are not doing any heavy calculating during display.
If you need to update only a single cell because of changes you made in the PickerCell then you should be calling reloadRowsAtIndexPaths:withRowAnimation: with the indexPath of the cell to be updated.
Your issue is with your subclass WOSArticleCell. Have you implemented prepareForUse()? If you have, are you setting any properties to nil?
UITableViewCell Class Reference
Discussion
If a UITableViewCell object is reusable—that is, it has a reuse
identifier—this method is invoked just before the object is returned
from the UITableView method dequeueReusableCellWithIdentifier:. For
performance reasons, you should only reset attributes of the cell that
are not related to content, for example, alpha, editing, and selection
state. The table view's delegate in tableView:cellForRowAtIndexPath:
should always reset all content when reusing a cell. If the cell
object does not have an associated reuse identifier, this method is
not called. If you override this method, you must be sure to invoke
the superclass implementation.

Using indexPath vs. indexPath.row in tableView.deselectRowAtIndexPath

I noticed in that to disable the highlight feature of a table cell you would yse the following method:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
...
tableView.deselectRowAtIndexPath(indexPath, animated: false)
}
What I don't understand is what's going on in the first argument indexPath. In the instructional book I've been reading just about every other method has been using indexPath.row for selecting individual cells:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "Cell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as CustomTableViewCell
cell.nameLabel.text = restaurantNames[indexPath.row]
cell.typeLabel.text = restaurantTypes[indexPath.row]
cell.locationLabel.text = restaurantLocations[indexPath.row]
}
Just out of curiosity I tried passing indexPath.row as an argument but got the error, 'NSNumber' is not a subtype of 'NSIndexPath'. In this particular case what is the main difference between using indexPath and indexPath.row.
From the NSIndexPath class reference:
The NSIndexPath class represents the path to a specific node in a tree
of nested array collections. This path is known as an index path.
Table views (and collection views) use NSIndexPath to index their items
because they are nested structures (sections and rows). The first index is the section of the tableview and the second index
the row within a section.
indexPath.section and indexPath.row are just a "convenience" properties, you always have
indexPath.section == indexPath.indexAtPosition(0)
indexPath.row == indexPath.indexAtPosition(1)
They are documented in NSIndexPath UIKit Additions.
So NSIndexPath is an Objective-C class and is used by all table view
functions. The section and row property are NSInteger numbers.
(Therefore you cannot pass indexPath.row if an NSIndexPath is expected.)
For a table view without sections (UITableViewStyle.Plain) the
section is always zero, and indexPath.row is the row number of
an item (in the one and only section). In that case you can use
indexPath.row to index an array acting as table view data source:
cell.nameLabel.text = restaurantNames[indexPath.row]
For a table view with sections (UITableViewStyle.Grouped) you have
to use both indexPath.section and indexPath.row (as in this
example: What would be a good data structure for UITableView in grouped mode).
A better way to disable the selection of particular table view rows is
to override the willSelectRowAtIndexPath delegate method:
override func tableView(tableView: UITableView, willSelectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath? {
// return `indexPath` if selecting the row is permitted
// return `nil` otherwise
}
In addition, to disable the appearance of the cell highlight on touch-down, you can also set
cell.selectionStyle = .None
when creating the cell. All this is (for example) discussed in
UITableview: How to Disable Selection for Some Rows but Not Others.
Your question and confusion stems from the fact that Apple decided to use somewhat imprecise method name/signature
tableView.deselectRowAtIndexPath(indexPath, animated: false)
The "Row" word in the method signature implies that row is going to be deselected, therefore one would expect a row as a method parameter. But a table view can have more than one section. But, in case it doesn't have more sections, the whole table view is considered one section.
So to deselect a row (a cell really) in table view, we need to tell both which section and which row (even in case of having only one section). And this is bundled together in the indexPath object you pass as a parameter.
So the before-mentioned method might instead have a signature
tableView.deselectCellAtIndexPath(indexPath, animated: false)
which would be more consistent, or another option, albeit a bit less semantically solid (in terms of parameters matching the signature)
tableView.deselectRowInSectionAtIndexPath(indexPath, animated: false)
Now the other case - when you implement the delegate method
tableview:cellForRowAtIndexPath
here again (Apple, Apple...) the signature is not completely precise. They could have chose a more consistent method signature, for example
tableview:cellForIndexPath:
but Apple decided to stress the fact that we are dealing with rows. This is an 8 year old API that was never the most shining work of Apple in UIKit, so it's understandable. I don't have statistical data, but I believe having a single section (the whole tableview is one section) is the dominant approach in apps that use a table view, so you don't deal with the sections explicitly, and work only with the row value inside the delegate method. The row often serves as index to retrieve some model object or other data to be put into cell.
Nevertheless the section value is still there. You can check this anytime, the passed indexPath will show the section value for you when you inspect it.
NSIndexPath is an object comprised of two NSIntegers. It's original use was for paths to nodes in a tree, where the two integers represented indexes and length. But a different (and perhaps more frequent) use for this object is to refer to elements of a UITable. In that situation, there are two NSIntegers in each NSIndexPath: indexPath.row and indexPath.section. These conveniently identify the section of a UITableView and it's row. (Absent from the original definition of NSIndexPath, row and section are category extensions to it).
Check out tables with multiple sections and you'll see why an NSIndexPath (with two dimensions) is a more general way to reference a UITableView element than just a one-dimensional row number.
i see the perfect answer is from apple doc
https://developer.apple.com/documentation/uikit/uitableview/1614881-indexpath
if you print indexpath you may get [0,0] which is first item in the first section
I was confused between indexPath and indexPath.row until i understand that the concept is the same for the TableView and the CollectionView.
indexPath : [0, 2]
indexPath.section : 0
indexPath.row : 2
//
indexPath : the whole object (array)
indexPath.section : refers to the section (first item in the array)
indexPath.row : refers to the selected item in the section (second item in the array)
Sometimes, you may need to pass the whole object (indexPath).

Resources