5

Using Swift, how can I iterate over all the UITableCells given a section id (eg: all cells in section 2)?

I only see this method: tableView.cellForRowAtIndexPath, which returns 1 cell given the absolute index, so it doesn't help.

Is there an elegant and easy way?

Note: I want to set the AccesoryType to None for all of the cells in a section, programatically, say: after a button is clicked, or after something happends (what happends is not relevant for the question)

I have the reference for the UITableView and the index of the section.

sports
  • 6,889
  • 11
  • 61
  • 121
  • To everyone: Given that people keep adding newer answers to this question, showing how to do this technique in newer ways (e.g. Swift 3, then Swift 4), please read all my comments under [isPha's answer](https://stackoverflow.com/a/32626614/199364). Modifying UITableCells directly is almost always a bad habit, that will eventually cause some obscure bug when iOS reuses cells or rebuilds the view from the model (have you tested a phone call interrupting your app? Have you tested on small phones with your largest data set?). If at all possible, instead make change in the **data source**. – ToolmakerSteve Jan 21 '19 at 19:23

7 Answers7

9

You misunderstand how table views work. When you want to change the configuration of cells, you do not modify the cells directly. Instead, you change the data (model) for those cells, and then tell your table view to reload the changed cells.

This is fundamental, and if you are trying to do it another way, it won't work correctly.

You said "I need the array of cells before modifying them…" Same thing applies. You should not store state data in cells. As soon as a user makes a change to a cell you should collect the changes and save it to the model. Cells can scroll off-screen and their settings can be discarded at any time.

@LordZsolt was asking you to show your code because from the questions you're asking it's pretty clear you are going about things the wrong way.

EDIT:

If you are convinced that you need to iterate through the cells in a section then you can ask the table view for the number of rows in the target section, then you can loop from 0 to rows-1, asking the table view for each cell in turn using the UITableView cellForRowAtIndexPath method (which is different than the similarly-named data source method.) That method will give you cells that are currently visible on the screen. You can then make changes to those cells.

Note that this will only give you the cells that are currently on-screen. If there are other cells in your target section that are currently not visible those cells don't currently exist, and if the user scrolls, some of those cells might be created. For this reason you will need to save some sort of state information to your model so that when you set up cells from the target section in your datasource tableView:cellForRowAtIndexPath: method you can set them up correctly.

Community
  • 1
  • 1
Duncan C
  • 115,063
  • 19
  • 151
  • 241
  • You can use a UITableView with static data or dynamic data. Also: your post doesnt answer the question. – sports May 17 '15 at 00:15
  • 1
    Yeah well, the answer is "don't do that." SO doesn't lend itself to "Your question doesn't make sense" type answers, which is what my answer is. – Duncan C May 17 '15 at 00:20
  • My question is valid enough for itself ("as is"). Its a technical question. – sports May 17 '15 at 00:22
  • 2
    This does not provide an answer to the question. To critique or request clarification from an author, leave a comment below their post. – Schemetrical May 17 '15 at 01:00
  • 4
    @Schemetrical: Answers that explain why an approach is invalid or unwise, *and that then explain at least somewhat of what to do instead*, are perfectly legitimate. This is such an answer. – Nathan Tuggy May 17 '15 at 02:20
  • @NathanTuggy, this is not "such an answer", because Duncan C doesn't know the context. This is a technical question. You cant start throwing OPINIONS about "the approach" when the question is such as, I repeat: "how to iterate over all the UITableCells from a given section". – sports May 17 '15 at 05:35
  • @Sports, see my edit. I outlined what you need to do in order to get the on-screen cells for a given section. – Duncan C May 20 '15 at 14:26
  • @sports - but Duncan C is correct: the Apple approved way to alter table cells, even for the simple case you describe is *alter the model and then tell the table to update*. Clearly that isn't the answer you are looking for, but it is considered one form of valid answer in Stack Overflow. Q & A's aren't just for the original person asking the question. The larger value is for future readers. Here, those readers need to be warned that this is not a road to go down, unless they are very sure. – ToolmakerSteve Apr 07 '17 at 04:32
  • 1
    To finish out this discussion, my answer is not an opinion, it is well documented by Apple, and also borne out through painful experience. If you grab on-screen cells and change them without updating the data model to reflect the change, then when you scroll the table view and scroll back, the cells that you've changed manually lose their changes, and other cells that recycle your changed cells are likely to be set up incorrectly as well. – Duncan C Apr 07 '17 at 14:18
  • I edited my answer to give you the information you asked for, but the simple fact is that you can't iterate through all the cells in a section unless those cells all happen to be on-screen. If a cell is not currently on-screen then you'll get `nil` in response to the table view's `cellForRow(at:)` method. – Duncan C Feb 16 '18 at 01:25
  • Q: "How do I fly my airplane twice as far as it's maximum cruising range". A: "You can't. You will run out of fuel and have to make an emergency, unpowered landing long before you reach your destination." – Duncan C Feb 16 '18 at 01:27
6

Im using same way of iterating all table view cells , but this code worked for only visible cells , so I'v just add one line allows iterating all table view cells wether visible they are or not

   //get section of interest i.e: first section (0)
       for (var row = 0; row < tableView.numberOfRowsInSection(0); row++)
       {

        var indexPath = NSIndexPath(forRow: row, inSection: 0)

            println("row")
            println(row)
    //following line of code is for invisible cells
        tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.Top, animated: false)


    //get cell for current row as my custom cell i.e :roomCell

            var cell :roomCell = tableView.cellForRowAtIndexPath(indexPath) as roomCell
      }

* the idea is to scroll tableview to every row I'm receiving in the loop so, in every turn my current row is visible ->all table view rows are now visible :D

IsPha
  • 355
  • 4
  • 8
  • Ah, so many people with misguided answers, promoting an approach that will seem to work, but fail sometimes on some devices - a testing nightmare. Here is the problem: if user has scrolled down the list of rows, at some point the first row will be discarded. If user makes a change on the currently visible row, the change *might* be lost when this code starts back at the first row - there is no guarantee as to when cells are kept once you start scrolling through a long table. Worse, might be different on different devices or iOS versions. – ToolmakerSteve Apr 07 '17 at 04:11
  • Hi @ToolmakerSteve, for the test case you're mentioning, it's easy to keep changed indexes via like dictionary . The idea is that when you scroll the cellforrowatindexpath gets called. So keep an eye on changed indexes and take care of them while reloading table due to scrolling. If your test case is like : You have a long list of ratings and you rate while scrolling, will provide code for this case that I faced a lot , otherwise you can give more details on your test case if it is technically different.Thanks for your reply. – IsPha Apr 07 '17 at 07:33
  • Wait @ToolmakerSteve, is that actually true? Will the tableView/collectionView actually _discard_ the cells? Obviously it doesn't update them/render them, for performance reasons, but it was not my understanding that it would actually "discard" them. This would suggest that, rather than just "visible" and "invisible" cells, there's actually a third state... That seems odd. The real point, I suppose, is that we shouldn't be iterating "all cells" if that's what we want; we should be iterating the dataSource (otherwise, we're treating the view as a model). – jbm Jan 21 '19 at 17:10
  • @IsPha - Bottom line: don't do what you are trying to do. It will eventually bite you. Its a misuse of the architectural design. Don't use the array of cells as information storage. For example, if the model array has 1000 rows, do you *really* want to iterate through 1000 cells, forcing them all to be created and then thrown away? Do you really want to manage additional information to keep track of which ones changed? You want to rely on iOS *not* discarding them as they go offscreen? Its much better to use as intended. Each cell should have a backing model, which changes as cell changes.. – ToolmakerSteve Jan 21 '19 at 18:53
  • .. But the primary reason I am so strongly against the approach you use is that it cannot be guaranteed to be correct - its **not testable**. Here is how I got bit, that forever cured me of doing what you do: I had 4 cells. They were all visible. Worked great. I didn't bother with backing store. Then a change was made that grew all the cells by 1 text line. Still worked great. EXCEPT on one small phone, the containing rectangle got squeezed. One cell was now offscreen. Still seemed to work, except if you scrolled to bottom then back. Info lost. Obvious in hindsight, but was a PITA. – ToolmakerSteve Jan 21 '19 at 18:57
  • @mrwheet - there is no third state. What happens is that *the cell object gets reused for a different source array index*. It still exists, but it is showing different information!! As you say, the correct answer is to iterate the dataSource, not the view. – ToolmakerSteve Jan 21 '19 at 19:00
  • Note that if your page design includes a user option to "cancel" - to discard changes - this means you have to *copy* the model state on page entry, because you modify model state immediately on every UI change. When user "cancels" you keep the "before" copy. When user "accepts" you keep the changed copy. [Avoiding this extra coding is one reason it is so tempting to do what isPha proposes, and that I am strongly cautioning against. Unless you can guarantee cells are never reused nor discarded, nor even "recreated" by iOS from model; e.g. a phone call interrupts your app!] – ToolmakerSteve Jan 21 '19 at 19:10
  • This is really, really bad advice. If you scroll through the table view, updating all the cells, either the scrolling won't do anything, or it will cause some of the cells you've updated to scroll off and get recycled. in the second case, those cells that scroll away will loose their custom configurations. To say "you can solve that by keeping a dictionary of updated indexes and "keeping an eye on" those cells is a hack upon a hack, and a truly awful idea. Just update your data model and tell the affected cells to redraw themselves! – Duncan C Dec 02 '19 at 13:30
6

For Swift 4 I have been using something along the lines of the following and it seems to work pretty well.

for section in 0...self.tableView.numberOfSections - 1 {
            for row in 0...self.tableView.numberOfRows(inSection: section) - 1 {
                let cell = self.tableView.cellForRow(at: NSIndexPath(row: row, section: section) as IndexPath)

                print("Section: \(section)  Row: \(row)")

            }
        }
Steve
  • 1,827
  • 12
  • 24
5

To answer my own question: "how can I iterate over all the UITableCells given a section id?":

To iterate over all the UITableCells of a section section one must use two methods:

  • tableView.numberOfRowsInSection(section)
  • tableView.cellForRowAtIndexPath(NSIndexPath(forRow: row, inSection: section))

So the iteration goes like this:

// Iterate over all the rows of a section
for (var row = 0; row < tableView.numberOfRowsInSection(section); row++) {
    var cell:Cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: row, inSection: section))?

   // do something with the cell here.
}

At the end of my question, I also wrote a note: "Note: I want to set the AccesoryType to None for all of the cells in a section, programatically". Notice that this is a note, not the question.

I ended up doing that like this:

// Uncheck everything in section 'section'
for (var row = 0; row < tableView.numberOfRowsInSection(section); row++) {
    tableView.cellForRowAtIndexPath(NSIndexPath(forRow: row, inSection: section))?.accessoryType = UITableViewCellAccessoryType.None
}

If there is a more elegant solution, go ahead and post it.

Note: My table uses static data.

sports
  • 6,889
  • 11
  • 61
  • 121
  • 1
    A note for everyone: If someone asks how to use a "goto" in java, you dont answer: "you shouldnt use a goto in Java". Of course you can add that as a comment, but not as an answer. (By "comment" and "answer" I mean in the context of StackOverflow) – sports May 17 '15 at 00:18
  • 1
    great, I searched over 2 hours, thank you very much! – AlexWoe89 Jan 05 '16 at 21:49
  • @sports - yes and no. It would have been far better if people *started* by asking some version of *"what problem are you trying to solve, for which you think this is a useful technique"*. And *then* decided how to answer. However, *in this specific case*, the explanation of *why* you almost never should be attempting to do what you are asking, is too long to put into a comment. People are learning a technique that will seem to work fine - and then they'll hit a situation where it breaks, and have no idea why. You seem to know what you are doing; but the next person reading probably won't. – ToolmakerSteve Apr 07 '17 at 04:22
  • (sorry, in case it isn't clear, I was commenting on your comment "If someone asks how to use a goto in java ...") – ToolmakerSteve Apr 07 '17 at 04:37
2

Im using this way of iterating all table view cells , but this code worked for only visible cells , so I'v just add one line allows iterating all table view cells wether visible they are or not

  //get section of interest i.e: first section (0)
   for (var row = 0; row < tableView.numberOfRowsInSection(0); row++)
   {

    var indexPath = NSIndexPath(forRow: row, inSection: 0)

        println("row")
        println(row)
//following line of code is for invisible cells
    tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.Top, animated: false)


//get cell for current row as my custom cell i.e :roomCell

        var cell :roomCell = tableView.cellForRowAtIndexPath(indexPath) as roomCell
  }

* the idea is to scroll tableview to every row I'm receiving in the loop so, in every turn my current row is visible ->all table view rows are now visible

IsPha
  • 355
  • 4
  • 8
2

Swift 4

More "swifty", than previous answers. I'm sure this can be done strictly with functional programming. If i had 5 more minutes id do it with .reduce instead. ✌️

func cells(tableView:UITableView) -> [UITableViewCell]{
    var cells:[UITableViewCell] = []
    (0..<tableView.numberOfSections).indices.forEach { sectionIndex in
        (0..<tableView.numberOfRows(inSection: sectionIndex)).indices.forEach { rowIndex in
            if let cell:UITableViewCell = tableView.cellForRow(at: IndexPath(row: rowIndex, section: sectionIndex)) {
                cells.append(cell)
            }
        }
    }
    return cells
}
Sentry.co
  • 4,132
  • 38
  • 30
0

You can use reloadSections(_:withRowAnimation:) method of UITableView.

This will reload all the cells in the specified sections by calling cellForRowAtIndexPath(_:). Inside that method, you can do whatever you want to those cells.

In your case, you can apply your logic for setting the appropriate accessory type:

if (self.shouldHideAccessoryViewForCellInSection(indexPath.section)) {
    cell.accessoryType = UITableViewCellAccessoryTypeNone
}
Lord Zsolt
  • 6,086
  • 9
  • 41
  • 68
  • But I need the array of cells before modifying them... before using realodSections. – sports May 16 '15 at 22:49
  • Why do you need them? `I want to set the AccesoryType to None for all of the cells in a section` - that you can do inside the `cellForRowAtIndexPath` method. – Lord Zsolt May 16 '15 at 22:49
  • And what is the index for the cells in the given section? – sports May 16 '15 at 22:52
  • Excuse me? I really don't understand what you're asking. Do you know how a table view works? Post your code to pastebin and provide a link, it might be helpful. – Lord Zsolt May 16 '15 at 22:53
  • Quoting you: "that you can do inside the cellForRowAtIndexPath method". How? – sports May 16 '15 at 22:57
  • There is no need to post the code because the answer is pretty clear: How can I modify the AccesoryType of every cell under a given section? I dont want to touch the cells of other sections. You are not answering the question in your post. – sports May 16 '15 at 22:59
  • I AM answering the question in the post, you just don't know how table views work. – Lord Zsolt May 16 '15 at 23:01
  • 1
    You're right, OP needs to understand how `UITableView`s work. – Schemetrical May 17 '15 at 01:04