Populating two grouped tableviews from the same PLIST - iphone

In my app, there is a portion that holds a static contacts directory. I would like the directory to appear indexed (alphabetically) and the detail view will need to be grouped also. (I am somewhat trying to replicate the look and feel of the Contacts app.) I have it working, just no index and a detail page that is just a view with a collection of buttons.
For some reason, I cannot get the a-ha moment when dealing with the table view.
Does anyone have any examples of how I can do this? Even better, what is the absolute best book to show how to work with UITableViews (especially when grouping them) using a PLIST as a source?
Apples documentation and other searches have gotten me some good information, but feels far from comprehensive enough to fully "get it".

First of if youre using pLists as the source then they wont stay in any form of order, well not the order there in the pList anyway. A way around this is to have an array within your pList which then has your elements. To us data from a plist you might want to do something like this if you had a plist populated with NSDictionarys:
NSDictionary* dict = [NSDictionary initwithContentsOfFile:#"ApList.plist"];
//Get all of the elements in the dictionary
NSArray *array = [dict allKeys};
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 2;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return [array count];
}
- (UITableViewCell *)tableView:(UITableView *)table cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier;
MyIdentifier = #"Cell";
UITableViewCell *cell = (UITableViewCell *)[table dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MyIdentifier] autorelease];
[cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator];
}
//Create a new dict based of the indexpath of the table cell, we need the array otherwise we wouldnt be able to get the key value for the dict.
NSDictionary* currentDict = [NSDictionary valueForKey:[array objectAtIndex:indexPath.row]];
//Then once you;ve got the dict create a string from a String held in that dict, in this case the key for the String is LABEL.
NSString *title = [currentDict valueForKey:#"LABEL"];
cell.textLabel.text = title;
return cell;
}
Hope this helps

Related

Populate a tableView doesn't update

Here is my problem. I have a table view that is filled depending on the content of an array. Up to now, everything is fine. This array is used to store some maps name coming from a server. I have a refresh button so that when I click on it, it will update the array with the new maps name (refresh method).
When I click on the refresh button, the maps array is modified. Either some maps are added or deleted (if a user deleted a map or created a new map for example). However, this situation does not reflect on the screen.
Let's say I have 5 maps (named 1,2,3,4,5). If I delete one (let's say the map 3) and call refresh, the maps array (mapsModel->mapsNameList) will contain 4 maps and the content of this array is proper. However, on the iphone screen, I would see, in the table view, (1,2,4,5,5). I don't know why it doesn't remove a row if it is not in the maps array anymore.
I get the same problem if I try to add a map (let's say a map 0), I would get (0,1,2,3,4) and the number 5 would not be there.
If I restart the application, then all the maps appear properly...
Here is my code, if some variables name aren't clear of obvious, please let me know !
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
int numberOfRows = [[MapsModel sharedMapsModel] numberOfMapsInSection:((UITabBarController*) self.parentViewController).tabBar.selectedItem.tag];
// Return the number of rows in the section. THIS FUNCTION WORKS
return numberOfRows;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
MapsModel* mapsModel = [MapsModel sharedMapsModel];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
[[cell textLabel] setText:[[mapsModel->mapsNameList objectAtIndex:((UITabBarController*) self.parentViewController).tabBar.selectedItem.tag] objectAtIndex:indexPath.row]];
return cell;
}
-(void) refresh {
[[MapsModel sharedMapsModel] populateMapsNameListWithMapState:MAP_STATE_ALL];
[self updateView];
}
-(void) updateView {
//Reorder the maps by alphabetical order
NSSortDescriptor *sortDescriptor;
MapsModel* mapsModel = [MapsModel sharedMapsModel];
sortDescriptor = [[NSSortDescriptor alloc] initWithKey:nil ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
[[mapsModel->mapsNameList objectAtIndex:self.tabBarItem.tag] sortUsingDescriptors:sortDescriptors];
//Update the table view
[self.tableView reloadData];
}
Are you sure that [[MapsModel sharedMapsModel] numberOfMapsInSection: returns correct value after update? Your table code has no problem, it seems that you should check your MapsModel.
Probably your mapsModel returns wrong values in cellForRowAtIndexPath: and numberOfRowsInSection:
I know the problem, I had a tab bar controller and three table views. However, I tried to use only one view controller for all the three table views... One view controller per table view resolved my problem !

How to get a UITableViewCell's subtitle show how many times that specific cell was tapped?

I have a uitableview that displays the values of an array. I would like to know if there is a way to update the subtitle of its table cells, based on how many times each cell was tapped.
Thank you!
First of all, you'll want to use a NSMutableArray so you can change its contents after instantiation. Here's a basic over view of what I just tried to achieve your intended results:
In your interface
#property (strong, nonatomic) NSMutableArray *tapCountArray;
In your implementation
- (void)viewDidLoad
{
[super viewDidLoad];
self.tapCountArray = [NSMutableArray new];
int numberOfRows = 20;
for (int i = 0; i < numberOfRows; i ++) {
[self.tapCountArray addObject:#(0)];
}
}
Then the important part!
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.tapCountArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
NSString *text = [self.tapCountArray[indexPath.row] stringValue];
[cell.textLabel setText:text];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[self.tapCountArray replaceObjectAtIndex:indexPath.row withObject:#([self.tapCountArray[indexPath.row] intValue] + 1)];
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
When each cell is tapped the number in its detailTextLabel will be incremented up by one.
You shouldn't create a new array or set, that could lead to problems if the two arrays get out of sync with each other. The way to do it, as you suggested in your comment, is to use dictionaries. The way you said you were doing that is probably not the way, however. You want an array of dictionaries where the values for one key would be whatever your main data is and the value for the other key would be the number of taps. For example, lets call the two keys main and sub, and your main data is a set of names. The array of dictionaries would look like this: ({main:#"Tom",sub:1}, {main:#"Dick", sub:0}, {main:#"Harry",sub:2}, .....). In the tableView:cellForRowAtIndexPath:indexPath method you would provide the data to the cells like this:
cell.textLabel.text = [[array objectAtIndex:indexPath.row] valueForKey:#"main"];
cell.detailTextLabel.text = [[array objectAtIndex:indexPath.row] valueForKey:#"sub"];
I think you can just set up another array of the same length as the one you have now. Then when your didSelectRowAtIndexPath is triggered, increment your indexPath.row entry of the new array and refresh that cell. If you don't expect to shuffle the table, you don't need a dictionary.
You can insert the object into an NSCountedSet, and on your cellForRowAtIndexPath method, you would take the model object for the cell and verify the number of times it has been inserted into the NSCountedSet instance.
Take a look at the NSCountedSet documentation: https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/NSCountedSet_Class/Reference/Reference.html

How can I store my UITableViewCells in a NSMutableArray?

Basically I'm making a list view that you can add things to the top of. The best way I can think of doing this is to store the UITableViewCells themselves in a NSMutableArray — Because I can simply pull them from the array them with all their data inside the object, and this list view will never be over 10 cells long.
Also note that I'm using Storyboards, hence the initWithCoder use.
The following code is what I'm trying, and it doesn't work:
// This is where my NSMutableArray is initialized:
- (id)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super initWithCoder:aDecoder]) {
if (!_CellsArray) {
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"TestCell"];
_CellsArray = [NSMutableArray arrayWithObject:cell];
}
}
return self;
}
//UITableView Delegate & DataSource Methods
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"TestCell"];
[_CellsArray insertObject:cell atIndex:0];
return [_CellsArray objectAtIndex:indexPath.row];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 10;
}
I realize I may be approaching this in the wrong way, that's why I'm here though :)
Thank you.
edit: fixed a type in the code (TimerCell -> UITableViewCell)
Let's look at the order things get called in and what happens.
Your view controller is unarchived, so your initWithCoder: method is called. This method creates a mutable array and puts one instance of TimerCell into it. Said instance is not further configured (unless you've overridden initWithStyle:reuseIdentifier: to do some configuration).
Your data source method tableView:numberOfRowsInSection: is called, and it tells the table view there are ten rows.
Thus, your tableView:cellForRowAtIndexPath: is called ten times. Each time, it creates a new instance of UITableViewCell and inserts it into your mutable array. (After ten calls, your mutable array contains one TimerCell at index 10 and ten UITableViewCells at indices 0-9.) It does nothing to configure the cell's contents or appearance, then it returns the cell at the specified row index. On the first call, you're asked for row 0, so the cell you just created and inserted at index 0 is returned. On the second call, you're asked for row 1, so the cell at index 1 in your array is returned -- since you just inserted a new cell at index 0, the cell you created on the last call has shifted to index 1, and you return it again. This continues with each call: you return the same unconfigured UITableViewCell ten times.
It looks like you're trying to out-think UIKit. This is almost never a good thing. (It's been said that premature optimization is the root of all evil.)
UITableView already has a mechanism for cell reuse; it's best to just keep track of your own cell content and let that mechanism do its thing. I took so long to type this that other answers have been written describing how to do that. Look to them, or to Apple's documentation or any third-party UITableView tutorial.
Why don't you just store the cell information in an array. Then in the -cellForRowAtIndexPath: method, just extract the data needed to change each cell.
Here is a simple example:
//Lets say you have an init like this that inits some cell information
- (id)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super initWithCoder:aDecoder]) {
cellArray = [NSArray alloc] initWithObjects:#"firstCell",#"secondCell",#"thirdCell",nil];
}
return self;
}
//then for each cell, just extract the information using the indexPath and change the cell that way
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// Configure the cell...
cell.textLabel.text = [cellArray objectAtIndex:indexPath.row];
return cell;
}
Table views don't store things. Rather, they just ask for the data they want to display, and you typically get that data from elsewhere (like an NSArray, or an NSFetchedResultsController). Just store the things you want into some data container, and let the table display them for you.
// Probably your data model is actually a member of your class, but for purposes of demonstration...
static NSArray* _myArray = [[NSArray alloc] initWithObjects:#"Bob", #"Sally", #"Joe", nil];
- (NSInteger) tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section
{
return [_myArray count];
}
- (UITableViewCell*) tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
static NSString* CellIdentifier = #"TestCell";
// Make a cell.
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if( cell == nil ) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
// Setup the cell with the right content.
NSString* aString = [_myArray objectAtIndex:[indexPath row]];
cell.textLabel = aString;
return cell;
}
Now if you want more stuff in the list, add it to your array, and you're done.
Edit: On another note, initWithCoder: isn't generally the best place to do initialization for a view controller. Reason being, at the point that it's called, there's a good chance that stuff isn't loaded yet (IBOutlets, for example). I tend to prefer viewDidLoad (don't forget to cleanup in viewDidUnload in that case), or awakeFromNib.

Many different UITableViewCells in one TableView

I have an NSDictionary that we will say is like this:
key: value:
name Bookshelf
movies Array containing: (Movie 1, Movie 2, Movie 3)
books Array containing: (Book 1, Book 2, Book 3)
music Array containing: (Music 1, Music 2, Music 3)
And so on. Now what I'm trying to do is create a tableView that displays all this information, but with different types of cells based on what the dictionary key is. For example, movies use cellForMovies books use cellForBooks music use cellForMusic, each has it's own layout.
Obviously, I am oversimplifying, but hopefully you understand what I'm trying to do.
I am able to accomplish this if I do sections and a different cell type for each section, but I don't want to do that. I want to be able to have a table like this, for example:
cellForBooks
cellForMovies
cellForMovies
cellForMusic
cellForBooks
and so on...Any ideas or resources you can point me to? I only care about iOS 5 support (storyboards).
Edit with solution:
A big thank you to everyone who helped, especially atticus who put the final piece together.
What ended up working was to change the structure of the data to be an array of dictionaries and then add a Key: type Value: book/movie/music to each entry. The data structure now looks like:
Array[0]: Dictionary: [Key: type Value: book], [Key: name Value: Physics];
Array[1]: Dictionary: [Key: type Value: music], [Key: name Value: Rock];
Then to configure the cells I did this:
NSString *CellIdentifier = #"Cell";
NSDictionary *object = [self.listOfStuff objectAtIndex:indexPath.row];
if ([[object objectForKey:#"type"] isEqualToString:#"book"]){
CellIdentifier = #"BookCell";
}else if ([[object objectForKey:#"type"] isEqualToString:#"music"]){
CellIdentifier = #"MusicCell";
}else if ([[object objectForKey:#"type"] isEqualToString:#"movie"]){
CellIdentifier = #"MovieCell";
}
each of these had a different tableviewcell in the storyboard. We figured out if you want to use any of the built-in cell features, like cell.textLabel.text you needed to do this:
cell.textLabel.text = [object objectForKey:#"name"];
cell.textLabel.backgroundColor = [UIColor clearColor];
cell.textLabel.opaque = NO;
Hope this helps someone else in the future.
In your - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath method you can return many types of cells according to indexPath.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row > 5) { // Your conditions here to choose between Books and Movies
BookCell *cell = [tableView dequeueReusableCellWithIdentifier:#"bookCell"];
if (!cell)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"bookCell"];
}
return cell;
} else {
MovieCell *cell = [tableView dequeueReusableCellWithIdentifier:#"movieCell"];
if (!cell)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"movieCell"];
}
return cell;
}
return nil;
}
A big thank you to everyone who helped, especially atticus who put the final piece together.
What ended up working was to change the structure of the data to be an array of dictionaries and then add a Key: type Value: book/movie/music to each entry. The data structure now looks like:
Array[0]: Dictionary: [Key: type Value: book], [Key: name Value: Physics];
Array[1]: Dictionary: [Key: type Value: music], [Key: name Value: Rock];
Then to configure the cells I did this:
NSString *CellIdentifier = #"Cell";
NSDictionary *object = [self.listOfStuff objectAtIndex:indexPath.row];
if ([[object objectForKey:#"type"] isEqualToString:#"book"]){
CellIdentifier = #"BookCell";
}else if ([[object objectForKey:#"type"] isEqualToString:#"music"]){
CellIdentifier = #"MusicCell";
}else if ([[object objectForKey:#"type"] isEqualToString:#"movie"]){
CellIdentifier = #"MovieCell";
}
each of these had a different tableviewcell in the storyboard. We figured out if you want to use any of the built-in cell features, like cell.textLabel.text you needed to do this:
cell.textLabel.text = [object objectForKey:#"name"];
cell.textLabel.backgroundColor = [UIColor clearColor];
cell.textLabel.opaque = NO;
Hope this helps someone else in the future.
In your storyboard, set your tableView to use Dynamic Prototypes. Then, create a cell for each kind of cell you're going to use. Customize those appropriately, and then give each one a unique Reuse Identifier in the inspector (e.g. MovieCell).
Then, in your tableview:cellForRowAtIndexPath: method, determine the appropriate kind of cell for the content at that index path. If the cell at indexPath should be a movie cell, you would create the cell using:
static NSString * MovieCellIdentifier = #"MovieCell"; // This must match the storyboard
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MovieCellIdentifier];
That will give you the cell you specified as "MovieCell" in your storyboard. If you need to customize it further, create a UITableViewCell subclass and specify that for the cell in Storyboard. Then, you can refer to the properties etc. here for customization by casting the returned cell:
MovieTableViewCell *movieCell = (MovieTableViewCell *)cell;
// customize
My opinion is that you should implement all your cell types in one cell. Which means you should have one UITableViewCell class and .xib for all of them. Then in cellForRowAtIndexPath, create the cell, then depending on the actual requirement, show/hide the subviews that makes up a book or a movie.
In such design, your cell will be somewhat heavyweight. However, it's still better than using several cell classes because this way cell reusing is more efficient ([UITableView dequeueReusableCellWithIdentifier]).
I believe UITableView is designed towards one type of cell. So if your layout varies, you can still put them in one place and use frame or show/hide to control cell behavior.
i think u should use the following as your cell identifier.
- (UITableViewCell *)tableView:(UITableView *)tmpTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
NSString *cellIdentifier = [NSString stringWithFormat:#"Cell%d%d",indexPath.section,indexPath.row];
UITableViewCell *cell = [tmpTableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (nil == cell) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier] autorelease];
// do ur stuff here
}
return cell;
}

Group Table View - loading data

Looking for help on how to properly load the data for a Grouped Table View. I am using following code to load the data which I think should prepare an array "details" for "cellForRowAtIndexPath". However when I run the program I get the same data for all groups and it happens to be the last row in my data (which is in an NSArray called "Users"). Obviously I am doing something wrong but not sure what... please help.
// Customize the number of rows in the table view.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSRange aRange;
aRange.length = 9; aRange.location = 16;
NSString *users = [self.Users objectAtIndex: section];
NSString *subtitle = [NSString stringWithString:[users substringWithRange:aRange]];
details = [[NSArray arrayWithObjects:subtitle, subtitle, subtitle, nil] retain];
return [details count];
}
The data loading routine is as follows:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
}
// Configure the cell.
UIImage *cellImage = [UIImage imageNamed:#"world.png"];
cell.imageView.image = cellImage;
cell.textLabel.text = [details objectAtIndex:indexPath.row];
return cell;
}
From what you've posted, details array is being reset every time the table views asks its data sources for the number of rows in a section. details is last set when -tableView:numberOfRowsInSection: is last called i.e. for the last section. Once set, the same array provides the data for cell.textLabel.text for all the sections. So you are getting incorrect data.
It would be appropriate to calculate and store all the details arrays in a different array, say detailsArray.
The data can then be accessed them like
[[detailsArray objectAtIndex:indexPath.section] objectAtIndex:indexPath.row]
Another problem with your code is that every time numberOfRowsInSection is called, data is being leaked as details is being reset without the earlier object being released.