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
Related
I am new in iOS development and currently working on UITableView. I want to find last visible cells on the screen of device and cells that are at the bottom of the screen must be of blue color, which should fade to green as the cell is scrolled to the top of the screen.
I have gone through these links
Link1
Link2
But could not get success. Can anyone please provide idea how to detect last cells & cell fade animation?
Get last visible cell:
if let lastCell = tableView.visibleCells.last {
// do something with lastCell
}
In Swift 3.0, you can used this tableview method.
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
{
let intTotalrow = tableView.numberOfRows(inSection:indexPath.section)//first get total rows in that section by current indexPath.
//get last last row of tablview
if indexPath.row == intTotalrow - 1{
// call for last display
}
}
#shallowThought solution will only work if cells are already presented.
But, if you want to know the last cell when cells aren't presented yet and are going to be presented, we can create an extension for UITableView as follow:
func isLastVisibleCell(at indexPath: IndexPath) -> Bool {
guard let lastIndexPath = indexPathsForVisibleRows?.last else {
return false
}
return lastIndexPath == indexPath
}
This way, you can check tableView.isLastVisibleCell(...) multiple times until you have reached actual visible cell.
Try this code, It will work
Initially Declare
int firstIndexRow;
int lastIndexRow;
Write below code inside of ViewDidLoad()
[myTable reloadData]; //Reload because get visible last cell index row
firstIndexRow = 0;
lastIndexRow = (int)[self.myTable.indexPathsForVisibleRows lastObject].row;
NSLog(#"first : %d",firstIndexRow);
NSLog(#"Bottom : %d",lastIndexRow);
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
NSIndexPath *firstVisibleIndexPath = [[self.myTable indexPathsForVisibleRows] objectAtIndex:0];
NSIndexPath *lastObject = [self.myTable.indexPathsForVisibleRows lastObject];
firstIndexRow = (int)firstVisibleIndexPath.row;
lastIndexRow = (int)lastObject.row;
NSLog(#"first : %d",firstIndexRow);
NSLog(#"Bottom : %d",lastIndexRow);
[myTable reloadData];
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [myTable dequeueReusableCellWithIdentifier:#"cell"];
if (indexPath.row == firstIndexRow) {
cell.textLabel.textColor = [UIColor blueColor];
}else if (indexPath.row == lastIndexRow) {
cell.textLabel.textColor = [UIColor greenColor];
}else{
cell.textLabel.textColor = [UIColor grayColor];
}
cell.textLabel.text =[namesArray objectAtIndex:indexPath.row];
return cell;
}
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
}
I'm trying to add some subviews to my UITableViewCell. The number of subviews is based on my data. When I scroll down the subviews disappears and does not show any more. Adding them to the NIB is no option because I only now the number of subviews at runtime and they are different for each cell.
What is the right way to add an unknown number of subviews to a UITableViewCell at runtime?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *simpleTableIdentifier = #"DetailCell";
DetailCellTableViewCell *cell = (DetailCellTableViewCell *)[tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];
if (cell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"DetailCell" owner:self options:nil];
cell = [nib objectAtIndex:0];
NSInteger count = [self getMaxSubviews];
NSInteger y=100;
for (int i=0; i<count;i++)
{
UITextField *dataS = [[UITextField alloc] init];
dataS.frame=CGRectMake(277, y, 60, 17);
y=y+17;
dataS.tag=i+1337;
dataS.backgroundColor=[UIColor redColor];
[cell addSubview:dataS];
}
}
if (!useOrigCellFromNib) // Here I can use the original Nib created by IB
{
NSString *data = #"Some String";
[cell.data setText:data];
}
else // Use added subviews!
{
for (int i=0;i<arrS.count;i++)
{
NSManagedObject *s = [arrS objectAtIndex:i];
UITextView *dataS =[cell viewWithTag:i+1337];
dataS.text=[NSString stringWithFormat:#"%ld foo", (long)i];
[cell.data setHidden:YES];
}
}
return cell;
}
Like Igor mentioned when reusing cell you have to remove waht ever you add previousely and re-create subviews.
May be you can not use "loadFromNib" and Subclass 'UITableViewCell' class and create your cell there.
This is a example in swift but logic is same for ObjC too
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let stuffArray = array[indexPath.row]
var cell = tableView.dequeueReusableCellWithIdentifier("Cell")
if cell == nil {
cell = MyCustomCell(initWithDaraArray:stuffArray) // create cell based on array data dynamically
} else { // even if you have cell you need to refresh it for new data
cell.refreshDataForDataInArray(stuffArray) // here remove all subviews and create new ones
}
return cell
}
and cell heights can be adjusted by
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let stuffArray = array[indexPath.row]
return calculatedHeight(stuffArray)
}
1 you should reuse cells, call tableView.dequeueReusableCellWithIdentifier("CellId")
2 after you get the reused cell, you should delete all previously added custom subviews
3 after that you can add new subviews
about " I scroll down the subviews disappears and does not show any more"
I don't see any "cell" variable before
if (cell == nil)
So Probably you do not paste the reuse code here, in this case cells after scrolling will not be nil and the code under the if (cell == nil) will not be called...
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