8

I have a UITableView and have seen this effect and would like to implement it for our the followind data:

menu_header
  menu_subheader
    * item
    * item
  menu_subheader
    * item
    * item  
    * item

Basically, I would like to show just the header and subeaders and then when the user clicks one of the subheaders, it displays the items (preferably in an animation block) AND adjusts the other cells down or up appropriately. Like this:

enter image description here

Is there a prebuilt component that does this? Thinking about it, it seems like I would like to set these item cells to be hidden. I have seen this https://github.com/peterpaulis/StaticDataTableViewController but it looks like it doesn't work with dynamic data. It seems like this should be really simple. Any ideas on how to get this done? Ideally, I'd like it to be able to when you click it insert the data and then if you click another sub-header, close the other one and add to that sub-header.

timpone
  • 19,235
  • 36
  • 121
  • 211
  • I've done similar to this before, but I'm not sure what you mean by "subheader". Typically there are sections, the sections have header titles/views, and then there are cells in the section. It's really only 2-dimensions of information. When you talk about a header and subheader and cells, sounds like you mean 3-dimensions? – Mike Sep 07 '14 at 01:54
  • basically, it's a tree structure so there can be multiple levels of subheaders (basically coming out of a Rails acts_as_tree data structure). And when I say `headers`, I'm not thinking in terms of a UITableView header but that, unfortunately, is what we call it. Currently, I flatten this data structure and render all the items out and things can either be a `header` or an `item`. So what I'd like to do is have a tableview that just shows the headers and then, once you click, it will show the items for that header. – timpone Sep 07 '14 at 02:02

5 Answers5

6

To implement "folding" in a table view you have two options:

  • Control the number of cells in a section based on a folded/unfolded property per section. When folding or unfolding, use the insert or deleteRowsAtIndexPaths:withRowAnimation: methods on the tableView.
  • Control the height of the cells using the delegate method. Return zero for folded sections based on a folded/unfolded property per section. When folding or unfolding, call beginUpdates followed immediately by endUpdates to re compute the heights and animate to the new layout.

I've created a simple implementation of the second option in this GitHub repo. Please let me know if you have other questions about it.

jrturton
  • 118,105
  • 32
  • 252
  • 268
  • your second strategy sounds like what I was trying to do conceptually but hmm.... practically was another story. Do you have a link to some sample code for doing that? I googled for a couple of hours and got some really wonky results. As a first iteration, I would like to have only a single `section` be visible but honestly, a `toggle` button for each menu_header would be ideal – timpone Sep 07 '14 at 07:44
  • Is your problem in hiding and showing the rows, or _controlling_ the hide/show? – jrturton Sep 07 '14 at 08:55
  • thx, this is really helpful. I am trying to integrate with some of our data today. Since, there can be multiple levels of `menuHeaders` (similar to a Rails acts_as_tree), using sections will be problematic as it doesn't support sub-sections. Thinking something like checking in `cellForRowAtIndexPath` with `[rowInTable isKindOfClass:[CTVMenuHeader class]]`. I'll try to get something working later today – timpone Sep 08 '14 at 16:37
  • I don't think checking the types of cell is going to cut it. You'd be better off with some sort of data mapping from your levels to a standard index path. You may not realise but NSIndexPath can contain an arbitrary number of indexes, the section and row properties are just convenience methods. – jrturton Sep 08 '14 at 17:44
  • thx - I started working on it by forking yours and seeing if I could get model 2 (or yours working) - conceptually, it's the easiest for me without messing with indexPath. Have a lot of other things to do today so haven't hit the toggle show / hide for height but it is located here if you want to take a look https://github.com/trestles/collapsingTableView It is handling nested data on rendering but not the unhiding / rehiding. If you have a chance to look, great but fully understand if not. – timpone Sep 08 '14 at 19:24
  • 1
    so I updated github repo just now and think it's ALMOST there.... at github.com/trestles/collapsingTableView I've left your parts as comments and the model part (esp getOne is a big ugly) but getting closer – timpone Sep 09 '14 at 01:05
  • Yeah, looks like you're nearly there! Checking the model class is definitely better than checking the cell class, which is what I thought you were talking about. Do you need any more help with it? It seems that the hardest part is converting the (linear) index path into the appropriate tree node, but that's not too difficult. – jrturton Sep 09 '14 at 12:19
  • Changing the height of cells to "hide" them is not optimal as the cells will still be instantiated and configured, just not visible. – Rivera Sep 11 '14 at 04:42
  • 1
    @rivera it's not optimal, no, but depending on how much data is being displayed, it's often a _lot_ simpler with no discernible difference in performance or memory use. Note that my first suggestion _was_ to add and remove cells. – jrturton Sep 11 '14 at 05:24
  • How could you automatically collapse all open sections with your implementation @jturton? – aframe Nov 24 '14 at 13:37
  • @aframe you'd set all of the model properties that indicate open section(s) to NO or nil or however you'd done it – jrturton Nov 24 '14 at 13:47
2

You have to remove and insert cells into your table view instead of hacking their height.

We use a controller for table views that lets you "hide/show" cells while actually handling the removal/reinsertion of rows in the table view.

The way "Toggle Details" works on the Demo is pretty similar to what you are trying to achieve:

- (IBAction)toggleDetails:(id)sender
{
    // Hide if all hiddeable rows are hidden, show all otherwise
    [self.topSection setObjectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 3)]
                                  hidden:(![self.topSection isObjectAtIndexHidden:1] &&
                                          ![self.topSection isObjectAtIndexHidden:2] &&
                                          ![self.topSection isObjectAtIndexHidden:3])];
}

Changing the height of cells to "hide" them is not optimal as the delegate will be asked for their sizes many times and cells will still be instantiated and configured yet not visible.


The sample library keeps the row data on memory (one object per row) while reusing cells. This should be ok for most projects, but maybe in your case not only all objects shouldn't be on memory but also you shouldn't fetch all of them at once.

Rivera
  • 10,792
  • 3
  • 58
  • 102
  • hmm... why do you think manipulating the height is hacking it? originally, I was going to do inserting / deleting but manipulating height seems cleaner.... we will have some big tables (a couple of thousand rows) so when I get around to implementing I'll be curious to see which approach works better. – timpone Sep 12 '14 at 02:30
  • A "couple of thousand" rows? Height might not be the way to go then, but I'd also suggest that a single table isn't right either, folding or no. It's going to be hard to meaningfully interact with that. – jrturton Sep 12 '14 at 04:08
  • I'd say 2000 would be the top-end. On our first customer we have one that is about 500 and thinking there might be larger. The performance is good - maybe a 1-2s delay on loading the ViewController with the table view. As it mimics a real-world document (approximately 20 pages), there isn't much I can do about splitting it up. But the 500 item list is part of the motivation for creating a more navigable structure. I'll look into your excample more and really appreciate feedback. – timpone Sep 13 '14 at 02:45
1

Make your header will be Tableview Section and sub header will be Row... And in didSelectRow delegate method insert rows, that will be your items.

user716937
  • 38
  • 9
1

There is a sample code from Apple that can help you in get this result.
The main difference is that in the Apple sample code is the header that triggers the action and shows the relative subviews, but this is not a blocking issue.
You can use normal cells to achieve that, by inserting and deleting rows while selecting one of them.
What is important is that you need to to remap datasource information or pair the info with another collection to get the state of that cell: opened or closed and subheader or item to identify them and choose the right action while selecting it.
Also important is to keep consistency between your data model(data source) and the number of cells, if you do using batch operation to ad insert and remove cells would not be a problem. If you don't you are going to see a lot of exceptions.
Take a look also here.

Andrea
  • 26,120
  • 10
  • 85
  • 131
1

I actually wanted to just add a comment, but reputation issues...

Anyway, my personal favorite way of expanding/collapsing UITableView sections is described in this post: https://stackoverflow.com/a/1941766/2440562

If I am understanding the issue correctly, the menu_headers and menu_subheaders would always be visible and only the items would be shown/hidden.

So here it is my idea (let's see if I can explain it well enough): You probably have an idea how many menu_subheaders you would have for each menu_header (static count or the number of elements of an array), so you can add one section for each menu_header (which would actually contain only one row or header) and in-between those you can add the expandable sections (menu_subheaders), which can be managed as shown in the answer I mentioned above. And as you want to collapse the previously expanded menu_subheader when tapping on another, you could just reset its boolean value and reload both with the reloadSections method. You would have to do some calculating for the placement of the menu_headers and menu_subheaders, but basically you wouldn't have to deal with cell heights and row insertions and deletions (that actually is my favorite part).

Here it is a quick code snippet of the calculations I've mentioned (not tested, totally improvised):

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    // Return the number of sections.
    return <number_of_menu_headers> + <number_of_menu_subheaders>;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (indexPath.section == 0) {
        // handle first menu_header
    } else if (indexPath.section < 1 + <number_of_menu_subheaders1>) {
        if (indexPath.row == 0) {
            // handle the menu_subheader header row
        } else {
            // handle the rest of the items
        }       
    } else if (indexPath.section == 1 + <number_of_menu_subheaders1>) {
        // handle second menu_header
    } else if (indexPath.section < 2 + <number_of_menu_subheaders1> + <number_of_menu_subheaders2>) {
        if (indexPath.row == 0) {
            // handle the menu_subheader header row for the current menu_subheader
        } else {
            // handle the rest of the items for the current menu_subheader
        }       
    } etc...
}

Again, just an idea...

Community
  • 1
  • 1
Tihi
  • 91
  • 4