When I traverse all the cell in a tableView, In Objective-C:
for (int i = 0; i < [_tableView numberOfRowsInSection:0]; i++) {
UITableViewCell *cell = [_tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
cell.textLabel.text = #"change text";
}
It worked, but in swift I code :
for index in 0...tableView.numberOfRowsInSection(0) {
let cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: index, inSection: 0))!
cell.textLabel?.text = "\(index)"
}
It crashed, and throws an unwrap nil error. It seems that can only get the visible cells in Swift and can get all cells in Objective-C. How to explain this?
The behaviour is the same. Its just that in Objective C using a nil object does not crash whereas in Swift it crashes.
You can verify this by checking if the cell is nil or not in Objective-C and putting a log. In Swift to avoid the crash use optional binding instead.
E.g. if let cell = tableView.cellFor....
cellForRowAtIndexPath will only return the cell if it's currently visible, that's why your code causes a crash. To loop through all visible cells, just use
for cell in tableView.visibleCells {
// do something
}
Related
I'm having a huge issue with some I code I've converted to Swift. When calling self.tableView.endUpdates() my table view stays empty and I get the following error:
Assertion failure in -[UITableView _endCellAnimationsWithContext:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3512.60.7/UITableView.m:1501
Weirdly enough when the same code was written in Objective-C this error is not showing up.
The table view datasource and delegate ar still in Objective-C and used by many other view controllers in the project.
Here is the code in Objective-c:
self.dataSource.itemArray = [self.userList copy];
NSMutableArray *indexPaths = [NSMutableArray new];
for (NSUInteger i = (offset); i < (end); i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
[indexPaths addObject:indexPath];
}
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:[indexPaths copy]
withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
And this is the same code but then in swift:
self.dataSource.itemArray = self.userList.copy() as! [User]
var indexPaths = [NSIndexPath]()
for i in offset...end {
let indexPath = NSIndexPath(forRow: i, inSection: 0)
indexPaths.append(indexPath)
}
self.tableView.beginUpdates()
self.tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: .Fade)
self.tableView.endUpdates()
The tableView:numberOfRowsInSection: is called correctly and return the same in both case. Also the cell are correctly registered.
Is there a know issue or is ia just that I can't extent an Objective-C base class in Swift.
In this piece of code:
for i in offset...end {
let indexPath = NSIndexPath(forRow: i, inSection: 0)
indexPaths.append(indexPath)
}
The last indexPath which will be added is NSIndexPath(forRow: end, inSection: 0), that means that you are adding one extra row to your table view which is not expected.
Probably you want to change your loop to:
for i in offset..<end {
let indexPath = NSIndexPath(forRow: i, inSection: 0)
indexPaths.append(indexPath)
}
In my case
myTableView.reloadSections([sectionIndex], with: .none)
did work and removed the error. It may be due to any button or textfield tag getting wrong index after row deletion.
This table view delegate code to detect the last table cell doesn't work.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if (indexPath.row == self.tableView(tableView, numberOfRowsInSection: 0) - 1) {
...
}
}
The "last row" as determined by the code moves up and down the list as I scroll. How do I fix this?
I think the number of the sections for the tableView is more than 1,so you need to take the section into calculation(you should make sure that numberOfSections and numberOfRows is greater than 0):
let numberOfSections = self.tableView.numberOfSections
let numberOfRows = self.tableView.numberOfRowsInSection(numberOfSections-1)
if indexPath.row == numberOfRows - 1 && indexPath.section == numberOfSections - 1{
...
}
Try the below code
if (indexPath == [NSIndexPath indexPathForRow:(numberOfRowsInLastSection - 1) inSection:(numberOfSections - 1)])
{
...
}
In some cases, your last section might be empty, so you want to look for the section last section which contains a cell. This can be done by looping over each section, and in each section loop over each cell. While doing this, keep track of the last indexPath of the cell you encountered, and when the loop is done, you can retrieve the last cell using the indexPath.
Something like this should work:
NSIndexPath *lastCellIndexPath;
for (NSInteger sectionIndex = self.tableView.numberOfSections - 1; sectionIndex >= 0; sectionIndex--) {
if ([self.tableView numberOfRowsInSection:sectionIndex] > 0) {
NSInteger section = sectionIndex;
NSInteger row = [self.tableView numberOfRowsInSection:sectionIndex] - 1;
lastCellIndexPath = [NSIndexPath indexPathForRow:row inSection:section];
break;
}
}
// Retrieve your cell with the indexPath
...
My cell contains a UIView that draws inside drawRect and the output is different for the starting and ending table cells. I've found that if I do the following...
cell!.contLine.setNeedsDisplay()
...then the drawing is correct. (contLine is the embedded UIView). If I call setNeedsDisplay on the cell then it doesn't work. So I'm having to break encapsulation here.
As a result of Ossie's suggestion, I've also changed the detection code so that it updates the state of the cell every time. Otherwise, a reused cell's state can be stale.
cell!.isEnd = (indexPath.row == (self.tableView(tableView, numberOfRowsInSection: 0) - 1))
I have a method in Objective-C that I've used to uncheck all cells in a UITableView:
- (void)resetCheckedCells {
for (NSUInteger section = 0, sectionCount = self.tableView.numberOfSections; section < sectionCount; ++section) {
for (NSUInteger row = 0, rowCount = [self.tableView numberOfRowsInSection:section]; row < rowCount; ++row) {
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:row inSection:section]];
cell.accessoryType = UITableViewCellAccessoryNone;
cell.accessoryView = nil;
}
}
}
In Swift, I think I need to use enumeration to accomplish this. I'm stumped as to how to get the values I need. Here's a "physics for poets" sketch of what I'm trying to do:
func resetCheckedCells() {
// TODO: figure this out?
for (section, tableView) in tableView.enumerate() {
for (row, tableView) in tableView {
let cell = UITableView
cell.accessoryType = .None
}
}
}
This doesn't work, but it's illustrative of what I'm trying to accomplish. What am I missing?
UPDATE
There was a very simple, but non-apparent (to me), way to do this involving cellForRowAtIndexPath and a global array...
var myStuffToSave = [NSManagedObject]()
... that's instantiated with the UITableViewController loads. I'm posting this update in hopes that someone else might find it helpful.
My UITableViewController is initially populated with NSManagedObjects. My didSelectRowAtIndexPath does two things:
1) adds/removes NSManagedObjects from a global myStuffToSave array
2) toggles cell.accessoryType for the cell between .Checkmark and .None
That when cellForRowAtIndexPath is called, I compare items from myStuffToSave with what's in the tableView.
Here's a snippet of my cellForRowAtIndexPath:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
// I set the cells' accessory types to .None when they're drawn
// ** SO RELOADING THE tableView NUKES THE CHECKMARKS WITH THE FOLLOWING LINE... **
cell.accessoryType = .None
// boilerplate cell configuration
// Set checkmarks
// ** ...IF THE ARRAY IS EMPTY
if self.myStuffToSave.count > 0 {
// enumerate myStuffToSave...
for (indexOfMyStuffToSave, thingToSave) in stuffToSave.enumerate() {
// if the object in the array of stuff to save matches the object in the index of the tableview
if stuffInMyTableView[indexPath.row].hashValue == stuffToSave[indexOfMyStuffToSave].hashValue {
// then set its accessoryView to checkmark
cell.accessoryType = .Checkmark
}
}
}
return cell
}
So removing everything from myStuffToSave and reloading the tableView will reset all the checked cells. This is what my resetCheckedCells method looks like at the end:
func resetCheckedCells() {
// remove everything from myStuffToSave
self.myStuffToSave.removeAll()
// and reload tableView where the accessoryType is set to .None by default
self.tableView.reloadData()
}
Thanks to #TannerNelson for pointing me towards a solution.
This seems like a strange way to use UITableView.
You should look at the UITableViewDataSource protocol and implement your code using that.
The main function you will need to implement is tableView:cellForRowAtIndexPath. In this function, you dequeue and return a cell.
Then when you need to update cells to be checked or unchecked, you can just call reloadAtIndexPaths: and pass the visible index paths.
This gist has a nice UITableView extension for reloading only visible cells using self.tableView.reloadVisibleCells()
https://gist.github.com/tannernelson/6d140c5ce2a701e4b710
So I am trying to write some code that scrolls a collection view to a certain index, then pulls a reference to the cell and does some logic. However I've noticed if that cell wasn't presently visible prior to the scroll, the cellForItemAtIndexPath call will return nil, causing the rest of my logic to fail.
[_myView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:index
inSection:0]
atScrollPosition:UICollectionViewScrollPositionTop
animated:NO];
//Tried with and without this line, thinking maybe this would trigger a redraw
[_myView reloadData];
//returns nil if cell was off-screen before scroll
UICollectionViewCell *cell =
[_myView cellForItemAtIndexPath:
[NSIndexPath indexPathForItem:index inSection:0]];
Is there some other method I have to call to cause the cellForItemAtIndexPath to return something for a cell that suddenly came into view as a result of the scroll immediately preceding it?
I figured out a solution for now. If I call [myView layoutIfNeeded] right after my call to reloadData, but before my attempt to retrieve the cell everything works fine. Right now all the cells are cached so access is fast, but I am scared this might give me bad performance if I have to load from the web or an internal database, but we'll see.
If you want to access the cell and have it visible on screen:
NSIndexPath *indexPath = ...;
[collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredVertically | UICollectionViewScrollPositionCenteredHorizontally animated:NO];
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
if(cell == nil) {
[collectionView layoutIfNeeded];
cell = [collectionView cellForItemAtIndexPath:indexPath];
}
if(cell == nil) {
[collectionView reloadData];
[collectionView layoutIfNeeded];
cell = [collectionView cellForItemAtIndexPath:indexPath];
}
I very rarely enter the second if statement, but having two fallbacks like this works very well.
Based on your comments, it sounds like what you're ultimately after is the cell's frame. The way to do that without relying on existence of the cell is to ask the collection view's layout:
NSIndexPath *indexPath = ...;
UICollectionViewLayoutAttributes *pose = [self.collectionView.collectionViewLayout layoutAttributesForItemAtIndexPath:indexPath];
CGRect frame = pose.frame;
hfossli's solution worked for me, so I've provided a Swift version below. In my case, I was unable to get a reference to a custom UICollectionViewCell, probably because it was not visible. I tried many things, but just as Shaybc said, this was the sole working solution.
Swift 3:
func getCell(_ indexPath: IndexPath) -> CustCell? {
cView.scrollToItem(at: indexPath, at: UICollectionViewScrollPosition.centeredHorizontally, animated: false)
var cell = cView.cellForItem(at: indexPath) as? CustCell
if cell == nil {
cView.layoutIfNeeded()
cell = cView.cellForItem(at: indexPath) as? CustCell
}
if cell == nil {
cView.reloadData()
cView.layoutIfNeeded()
cell = cView.cellForItem(at: indexPath) as? CustCell
}
return cell
}
usage:
let indexPath = IndexPath(item: index, section: 0)
cView.scrollToItem(at: indexPath, at: UICollectionViewScrollPosition(), animated: true)
if let cell = getCell(indexPath) {
cell. <<do what you need here>>
}
I have n sections (known amount) and X rows in each section (unknown amount. Each row has a UITextField. When the user taps the "Done" button I want to iterate through each cell and do some conditional tests with the UITextField. If the tests pass data from each cell is written to a database. If not, then a UIAlert is shown. What is the best way to loop through the rows and if there is a more elegant solution to this please do advise.
If you only want to iterate through the visible cells, then use
NSArray *cells = [tableView visibleCells];
If you want all cells of the table view, then use this:
NSMutableArray *cells = [[NSMutableArray alloc] init];
for (NSInteger j = 0; j < [tableView numberOfSections]; ++j)
{
for (NSInteger i = 0; i < [tableView numberOfRowsInSection:j]; ++i)
{
[cells addObject:[tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:j]]];
}
}
Now you can iterate through all cells:
(CustomTableViewCell is a class, which contains the property textField of the type UITextField)
for (CustomTableViewCell *cell in cells)
{
UITextField *textField = [cell textField];
NSLog(#"%#"; [textField text]);
}
Here is a nice swift implementation that works for me.
func animateCells() {
for cell in tableView.visibleCells() as! [UITableViewCell] {
//do someting with the cell here.
}
}
Accepted answer in swift for people who do not know ObjC (like me).
for section in 0 ..< sectionCount {
let rowCount = tableView.numberOfRowsInSection(section)
var list = [TableViewCell]()
for row in 0 ..< rowCount {
let cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: row, inSection: section)) as! YourCell
list.append(cell)
}
}
for xcode 9 use this - (similar to #2ank3th but the code is changed for swift 4):
let totalSection = tableView.numberOfSections
for section in 0..<totalSection
{
print("section \(section)")
let totalRows = tableView.numberOfRows(inSection: section)
for row in 0..<totalRows
{
print("row \(row)")
let cell = tableView.cellForRow(at: IndexPath(row: row, section: section))
if let label = cell?.viewWithTag(2) as? UILabel
{
label.text = "Section = \(section), Row = \(row)"
}
}
}
for (UIView *view in TableView.subviews) {
for (tableviewCell *cell in view.subviews) {
//do
}
}
Since iOS may recycle tableView cells which are off-screen, you have to handle tableView one cell at a time:
NSIndexPath *indexPath;
CustomTableViewCell *cell;
NSInteger sectionCount = [tableView numberOfSections];
for (NSInteger section = 0; section < sectionCount; section++) {
NSInteger rowCount = [tableView numberOfRowsInSection:section];
for (NSInteger row = 0; row < rowCount; row++) {
indexPath = [NSIndexPath indexPathForRow:row inSection:section];
cell = [tableView cellForRowAtIndexPath:indexPath];
NSLog(#"Section %# row %#: %#", #(section), #(row), cell.textField.text);
}
}
You can collect an NSArray of all cells beforehands ONLY, when the whole list is visible. In such case, use [tableView visibleCells] to be safe.
quick and dirty:
for (UIView *view in self.tableView.subviews){
for (id subview in view.subviews){
if ([subview isKindOfClass:[UITableViewCell class]]){
UITableViewCell *cell = subview;
// do something with your cell
}
}
}
Here's a completely different way of thinking about looping through UITableView rows...here's an example of changing the text that might populate your UITextView by looping through your array, essentially meaning your tableView cell data.
All cells are populated with data from some kind of model. A very common model would be using an NSObject and NSMutableArray of those objects. If you were in didSelectRowAtIndexPath, you would then want to do something like this to affect the row you're selecting after modifying the array above:
for(YourObject *cellRow in yourArray)
{
if(![cellRow.someString isEqualToString:#""])
{
cellRow.someString = #"";
}
//...tons of options for conditions related to your data
}
YourObject *obj = [yourArray objectAtIndex:indexPath.row];
obj.someString = #"selected";
[yourArray insertObject:views atIndex:indexPath.row];
[yourArray removeObjectAtIndex:indexPath.row];
[yourTable reloadData];
This code would remove all the UITextField's text in every row except the one you selected, leaving the text "selected" in the tapped cell's UITextField as long as you're using obj.someString to populate the field's text in cellForRowAtIndexPath or willDisplayRowAtIndexPath using YourObject and yourArray.
This type of "looping" doesn't require any conditions of visible cells vs non visible cells. If you have multiple sections populated by an array of dictionaries, you could use the same logic by using a condition on a key value. Maybe you want to toggle a cells imageView, you could change the string representing the image name. Tons of options to loop through the data in your tableView without using any delegated UITableView properties.
swift 5:
guard let cells = self.creditCardTableView.visibleCells as? [CreditCardLoanCell] else {
return
}
cells.forEach { cell in
cell.delegate = self
}
I would like to add my two cents to the matter even though this post is old. I created an array of type UITableViewCell and appended each new cell to it before returning it in cellForRowAt. See code below:
var cellArray = [UITableViewCell]()
//UITableView code
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell
//set up cell information
cellArray.append(cell)
return cell
}
Then if you need any information from each cell (i.e., UITextFields) in your Done button, you can iterate through the array like so in the desired context:
for cell in cellArray {
let myCell = cell as! Cell
//do stuff
}
Hope this helps anyone in the future