Double-sectioned expandable tableview - iphone

I am currently facing a big issue in my coding, but I can't find any solution.
Just as you can see here, I would like to create an expandable tableview with:
categories split into 2 sections
subcategories
I mean, if you click on "2A", the "2A-1", "2A-2" etc. list is expanded. If you click on "2B", the "2B-1", "2B-2" list is, and so on.
How do you think I could manage it?

I've written a solution of this nature in a few products. The code to accomplish this is a bit extensive, so I will give you a high level overview.
Create each "row" as a section. Obviously, override viewForHeaderInSection and so forth in order to make each section header actually look like a row.
Have your view controller hold an array of which sections are expanded (non expanded are collapsed)
In the - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section check to see if the section is expanded, if it is, then return the number of subitems, otherwise, return 0.
hence, the cellForRowAtIndexPath always returns just the subItems, and the viewForHeaderInSection always returns the parent.
When the user clicks on the header cell, toggle the section expanded flag, and reloadSections:withRowAnimation: to get a nice animated transition
one note, since prior to IOS6, section headers were ALWAYS recreated and NEVER cached, the performance was not great. With IOS6, this issue is solved as it recycles header cells too.

For expandable cells you can use VPPDropDown class, i used it myself, it's good :)

Related

Can a UITableView index jump to a row instead of a section?

I have implemented a UITableView with only one giant section and now I need to implement an index to this UITableView (something like the contacts apps) however my index does not represent sections it represents rows. And as far as I know you can only jump to a section and not to a row with the index in tableview.
I don't want to add sections because then I'll have to add a section for each row, which would be kinda stupid.
So my question is: Is there any way to implement an index to a UITableView such that it when I tap on any part of the index it takes me to the relative row instead of section in the tableview?
I would probably endup writing a hack for this thing which a really wanna avoid and do it the way it should be done (if there is any such way) so any help would be greatly appreciated. Cheers!
Just implement the sections and set their height to 0 in tableView:heightForHeaderInSection:
It's not such a big hack. Just change rows for sections with one row. Move what you have in tableView:numberOfRowsInSection: to numberOfSectionsInTableView:. You have to slightly adapt cellForRowAtIndexPath: to check the section you are in instead of the row and there you are.
Use this method:
- (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated;

how do I calculate heightForRowAtIndexPath when the cell is not even constructed yet?

Question - How does one best calculate the height for a row in the "heightForRowAtIndexPath" method of a UITableViewController, given that:
I'm using a custom subclassed UITableViewCell & the actually size of the subview (e.g. UILabels) is calculated at runtime & dependant on things such as if the user changed the font size
the cell's aren't actually prepared it seems prior to a "heightForRowAtIndexPath", so you can't rely on calling your specific custom cell instant to query it
Only thing I can think of for the moment is to:
1. In your custom UITableViewCell subclass create a method that calculates the heights of each subview (e.g. UILabel) that is in the UITableViewCell subclass - then use this within the cell subclass when it is creating instances
2. Also in the custom subclass create a class method that runs through all the UILabels, calling the above-mentioned method, to sum up the heights and therefore work out the total row height. It would have to get the data passed to it (e.g. text in each of the UILabels)
3. In the UITableViewController "heightForRowAtIndexPath" then you have to call the "calRowHeight" type method from (2) above, passing it the label text data. So effectively call a class method on your custom cell subclass which knows how to work out the total row height, but it's using the same logic that the cell needs too...
Is there an easier way than this I'm missing?
When a UITableView is created and whenever you send it a reloadData message, the datasource is sent one heightForRowAtIndexPath message for each cell. So if your table has 30 cells, that message gets sent 30 times.
Say only six of those 30 cells are visible on screen. In that case, when created and when you send it a reloadData message, the UITableView will send one cellForRowAtIndexPath message per visible row, i.e. that message gets sent six times.
Why do Apple implement it like this? Part of the reason is that it's almost always cheaper to calculate the height of a row than it is to build and populate a whole cell. And given that in many tables the height of every cell will be identical, it is often vastly cheaper. And part of the reason is because iOS needs to know the size of the whole table: this allows it to create the scroll bars and set it up on a scroll view etc.
If your row heights vary in size because they hold varying amounts of text, you can use one of the sizeWithFont: methods on the relevant string to do the calculations. This is quicker than building a view and then measuring the result. Note, that if you change the height of a cell, you will need to either reload the whole table (with reloadData - this will ask the delegate for every height, but only ask for visible cells) OR selectively reload the rows where the size has changed.
Additional material
If I understand the follow up question in the comment, the following may help:
If you are implementing an editing mode, then it's not uncommon to need to change the height of your table rows. For example, you may have text in your table rows and when they cells become narrower - to make space for the delete circles on the right - you may want some of the cells to become taller to accommodate the text. The basic approach here is to:
Make sure the tableView:heightForRowAtIndexPath: method knows whether your are in editing mode or not. (It can ask the tableView using isEditing.) And then get the method to return the right height, depending on whether you are in editing mode or not.
In your setEditing:animated: method in the UITableViewController (or a UIViewController, whichever you are using - there are some differences depending what you use, so it's worth checking the documentation carefully) send a reloadData message to the tableView after you have changed its state. This will force the tableView to grab the heights of every row and it will refetch the cells for the visible rows. The tableView handles making cells narrower when you enter editing mode, but if you want to do more work on the layout, do it in tableView:cellForRowAtIndex:. As noted above, the general strategy is to find a means of calculating the height that is quick. With text sizeWithFont: (and its variants) can do it. If you have images etc., then you can grab their dimensions and do some sums.
In addition to those steps you may also want to scroll the tableView a bit after switching modes. If the heights of your rows are different, then you will end up in the wrong position in the table after switching mode. An approach I have taken here is to use performSelector:withObject:afterDelay after I've reloaded the table to call a method that does the scroll adjusting. You need to use the delay, to allow time for the tableView to collect the new heights and the new table cells. (There may be a smarter way of doing this.) I do some sums to make the scroll adjustment based on the difference between the origin.y of the tableView:cellForRowAtIndexPath: of the cell first visible row on screen before and after the reload. So, for e.g., to get the position before the pre-load, something a bit like this.
CGPoint offset = [[self tableView] contentOffset];
NSIndexPath* indexPath = [[self tableView] indexPathForRowAtPoint:CGPointMake(0,offset.y)];
CGFloat preCellOffset = [[[self tableView] cellForRowAtIndexPath:indexPath] origin].y;
What I have done in the past, which I am not sure is the most efficient, is from within my heightForRowAtIndexPath method call cellForRowAtIndexPath then I ask the view for that cell its height. I have done similar things for header and footer heights. This way if I change the cell, the header, or the footer, I don't have to remember to go and update the corresponding height method.

heightForRowAtIndexPath for only one section?

This is the problem I'm facing right now:
I've got a lot of UITableViews with two sections each (only one is displayed at any time, on demand). The first section has got 3 cells, which might need to be resized. Because of that, I'm using heightForRowAtIndexPath.
The second section might have up to 3.000 cells, all using the default height of 44 points.
My heightForRowAtIndexPath determines whether it is treating a cell from section 1 or from section 2 and then either measures (section 1) or immediately returns the default value (44 section 2).
By using this method, large table views take a little while to be displayed, since heightForRowAtIndexPath is a performance issue in cases like this (more than about 1.000 cells).
My question is:
Is there any way to resize just the 3 cells presented in the first section? Any way to maybe force heightForRowAtIndexPath to be called just for indexPath.section == 0?
In case it makes any difference, I'm using a grouped table view.
Thanks.
heightForRowAtIndexPath is going to be called if it is implemented in your delegate. If you're only looking to change the values in the first section, then just use a simple if statement. This could look like:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
// Test to see what section you're in
if ([indexPath section] == 0) {
// return the height for the first section
} else {
// return the height for everything other than the first section
}
}
Cheers.
Why not just use 2 table views--one for your top section, and one for the second section?
You could either give each UITableView a different delegate, or if they share the delegate, you could return immediately from heightForRowAtIndexPath for the bottom UITableView.
I'd probably give them each a different delegate, to avoid a lot of switching in the delegate methods to figure out which section you're in.

Loading table sections when using headers

I cant seem to wrap my head around this. I have googled, and overstacked for hours now looking for examples that i can relate to. What I have is two arrays.
The name of my first NSMutableArray is "showDates".
I have 3 objects in here.
Object 0: "Today, May 20th"
Object 1: "Tomorrow, May 21st"
Object 2: "Saturday, May 22nd"
Then I have my second NSMutableArray named "showTimes"
I have about 15 objects in there with strings in each object. ( i hope that makes sense? )
Each object is structured like this:
Object 0:
showID #"98022"
eventID #"833"
showTime #"1:30pm"
showDate #"Today, May 20th"
auditorium #"9"
venue #"2991"
Object 1:
showID #"98222"
eventID #"813"
showTime #"2:30pm"
showDate #"Tomorrow, May 21st"
auditorium #"9"
venue #"2991"
Etc, etc, ....
I have the headers working great in my tableView, but I cant seem to figure out how to add the objects in my "showTimes" array under the correct header. Any help would be greatly appreciated.
In your table view's dataSource class, you need to implement the method tableView:cellForRowAtIndexPath: to provide the cell for each row in the table. Using
[indexPath section]
where indexPath is the second parameter passed into the method, you can determine which section the cell that was requested belongs to (you need to also implement numberOfRowsInSection: to inform the table view of how many cells it should request in each section). Hope that helps.
Details
First, you need to implement the numberOfRowsInSection: method in your TableView's data source.
This method takes the row of an NSIndexPath (which is an integer) as an argument. NSIndexPath, when used in TableViews, has the properties section and row. All you need to do is get the value of the section property and return the number of rows you'd like to appear in that section. For example:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
switch(section)
{
//This would represent your first section, "Today..."
case 0: //implement logic to find how many data objects need to be represented as rows for this section
break;
//second section, "Tomorrow"
case 1: //logic
break;
//third section
case 2: //logic
break;
}
}
Now that you've accomplished that, it's time to implement cellForRowAtIndexPath:. Unlike the previous method, this one takes a whole NSIndexPath as an argument, usually called indexPath in the docs. Simply find the section of the path using the property accessor indexPath.section, and the row within the section with indexPath.row.
Important Note
Having said that, all this might be a bit easier if you rearrange the information currently contained in the ShowTimes array. If the array isn't sorted, then (for example) the logic in each case I showed for numberOfRowsInSection: will require you to traverse the entire array and test each element to see if it belongs in the table section being requested. I would recommend splitting the ShowTimes array into three separate arrays (one for each section) before the table is displayed, so that all you need is a simple count call to the appropriate array in each case. The implementation for cellForRowAtIndexPath: would be simplified as well.
Summary
Think of it this way. Methods like numberOfSectionsInTableView: and numberOfRowsInSection: decide the layout of rows in your table long before a single cell is even loaded - before your data is even considered. As each cell is about to be displayed, cellForRowAtIndexPath: decides what that cell will contain based on the NSIndexPath of the cell, or more specifically, based on the section and row properties of that IndexPath.
In short, the only thing linking a cell to its content is the IndexPath, and cellForRowAtIndexPath: decides how the link is made.
For further reading, take a look at this page from the TableView Programming Guide. Specifically, the section entitled "Populating the Table View With Data".

Can I show/hide a certain cell in an UITableView depending on the state of another cell?

I have a UITableView with style "Grouped" which I use to set some options in my App. I'd like for one of the cells of this UITableView to only show up depending on whether another of this UITableView's cells is activated or not. If it's not, the first cell should show up (preferably with a smooth animation), if it is, the first cell should hide.
I tried returning nil in the appropriate -tableView:cellForRowAtIndexPath: to hide the cell, but that doesn't work and instead throws an exception.
I'm currently stuck and out of ideas how to solve this, so I hope some of you can point me in the right direction.
You should remove the data behind the hidden cells from the table view's data source.
For example, if you are using an array, when an action occurs that causes a cell to be hidden, you would remove the object for that row from the array. Then, as the table view's data source, the array will return one less total count and only return valid cells for every row in that count (no nil).
This approach may require maintaining a second array with all of the objects (including hidden).
To update the view, check out reloadRowsAtIndexPaths:withRowAnimation:.
Here's a handy post in which the author provides some source code for performing animations on the currently selected cell:
http://iphonedevelopment.blogspot.com/2010/01/navigation-based-core-data-application.html
He's using this in a NSFetchedResultsController context, but you can see how he's using various calls to add/remove cells & sections.
Now, in your case, you'll need to modify whatever array you're using to host the data used to generate the rows in your tableView when you "activate" your cell, then selectively use:
tableView:insertRowsAtIndexPaths:withRowAnimation:
tableView:deleteRowsAtIndexPaths:withRowAnimation:
tableView:insertSections:withRowAnimation:
tableView:deleteSections:withRowAnimation:
to adjust things accordingly (you can start with tableView:reloadData:, but it's inefficient).
I realize that the API can be a bit daunting, but take the time to read through it and understand what the various calls do. Understanding how the UITableView uses its datasource and delegate, as well as the chain of events that occur when cells are selected/deleted/etc., is important if you want to get things just right (and crash-free).
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:withRowAnimation:]; // or insertRowsAtIndexPaths:withAnimation:
[tableView endUpdates];
Before cellForRowAtIndexPath is called, numberOfRowsInSection is called. You should return the appropriate value of cells in the section there, so if you only want to show 1 cell, return one. The logic what cells are shown has to be implemented partially in both methods