I have a UITableView in a ViewController with a custom UITableClass implemented. The table displays different songs that the user can play. The table is populated by a method that pulls data from a server. This method is called in ViewDidLoad.
The user can also tag songs as a 'favorite'. I'd like the user to be able to view all of their 'favorite' tracks in a new `UITableView'. This table should be exactly the same, only with a different data source (only favorited tracks from the server).
How should I implement this? Should I create another method that loads new data to the table with only 'favorited' tracks? Should initialize a new UITableView with the same class and somehow set a different data source or a new ViewController? If so, how?
There will be a slight difference between the two ViewControllers that contain the UITableViews. The original ViewController with all of the tracks will have a button that either changes the datasource or initializes a new UITableView (depending on how it's implemented). The 'favorited' ViewController will have a back button.
I would create a segmented control that has options for "Favorites | All" and when it is switched a BOOL called favoritesOnly or something like that is switched from YES to NO or vice versa. My songs would be kept in an NSArray of NSDictionarys called songsArray and I would use this as my DataSource methods:
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if(favoritesOnly)
{
NSInteger count = 0;
for(int n=0; n<[songsArray count]; n++)
if([[[songsArray objectAtIndex:n] objectForKey:#"Favorite"] isEqualToString:#"YES"])
count++;
return count;
}
else
{
return [songsArray count];
}
}
and then for the cells:
-(UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
{
UITableViewCell *theCell = [tableView dequeueReusableCellWithIdentifier:#"Proto Cell"];
if(favoritesOnly)
{
NSInteger count = -1;
for(int n=0; n<[songsArray count]; n++)
{
if([[[songsArray objectAtIndex:n] objectForKey:#"Favorite"] isEqualToString:#"YES"])
{
count++;
if(count==[indexPath row])
{
//Configure the Cell using [songsArray objectAtIndex:n]
return theCell;
}
}
}
//If you got here there was an error; Error cell?
return theCell;
}
else
{
//Configure cell using [songsArray objectAtIndex:[indexPath row]]
return theCell;
}
}
This way you are using the same set of Data and the same UITableView, you are just using your control to properly delegate how the DataSource displays the information on the UITableView
Now, if you are using CoreData and and NSFetchedResultsController, this is all much much easier.
You can always go the route of just updating a central playlist UITableView, in which case you would just swap the data in your dataSource (in this case, perhaps NSMutableArray *playlist?) and then call [UITableView reloadData]. In this scheme, you avoid the overhead of having multiple views and the trouble of passing data around.
If you're planning to create additional functionality for your favorited song list, a secondary, customized UIViewController might be in order. In this sense, it can be re-used if you decide to have additional song lists. This would be a good solution if you intend to let them do anything additional that you wouldn't want cluttering your main interface with your list(s) such as editing title, song order, etc.
If these two views would be more or less identical, you can just set up a new UIViewController, either pass the new data via property or load it in your init, and then push it onto the view stack. So long as your app is navigation-based, the back button will appear on its own once you push your secondary UIViewController onto the stack. That isn't functionality you need to add on your own. The perks of this include code-reusability, which is a good skill to have as a UI designer and an engineer.
If you just want a read-only view, you can also look into a UIPopoverController with the data that would dismiss once they click away. This solution is not robust in the least and shouldn't be used if you intend the user to be doing anything more than tapping an entry, or if you expect your datasets to get big.
When planning your UI and flow, just make sure you think of what directions you might take it in the future. As mentioned in another answer, how you store your data makes a difference, as well as how you intend to make calls to your server (button clicks? after a set time?)
how do you store your data from the server? if you use CoreData (or MagicalRecord, which I can recommend) that having a fetched results controller with a different argument would be the only change you need.....
Ah, link to MagicalRecord: MagicalRecord
when you are tagging song as a favorite send one flag as a favorite to the web-service and then call another web service in the favorite view controller and make new table view with the same custom class and view the new source coming from server... and if you are storing in sq-lite or using core data make one column extra as favorite and call it in favorite view controller and load it with different data-source.
I would have a refresh method, and a button that switches between "show all" and "show favorites".
Basically, if the button is clicked, switch to the opposite group of objects, and update the text on the button accordingly. The table will always load an array called "tableDataArray" in my example, and you'll get row counts and such from the length of it.
Like...
-(IBAction)refresh {
if ([faveButton.text isEqualToString:#"Show All"]){
tableDataArray = favoriteArray;
[faveButton setText:#"Show Favorites"];
}
else {
tableDataArray = allSongsArray;
[faveButton setText:#"Show All"];
}
[tableView reloadData];
}
Simplest way to do it would be to have two instances of the same view controller. Each instance will have its own data source, one with all the songs, another with only the favorites.
Related
I have a table view. I want my app to work like the "Contacts" app on the iPhone. When you select a person in the app, it goes to a view that has the users name and phone number. and it doesnt matter which cell is selected, it always goes to the same view, but with different data. How can i achieve the same thing? I want one view controller and i want each cell to load certain data onto the presented view controller! And i would like to use NSUserDefaults (if possible). Any Help would be great, Thank You!
P.S. i know this is kind of a broad question, but im unsure where to find an answer i have searched and searched. Thanks again!
I want one view controller and i want each cell to load certain data
onto the presented view controller!
No problem. User taps on cell, your table's delegate gets a -tableView:didSelectRowAtIndexPath: message. Let's say the table's delegate and data source are both the view controller that manages the table (because that's a pretty common setup). So the view controller takes a look at the index path for the cell, grabs the associated data from wherever it keeps its data, instantiates the detail view controller with the data connected to the tapped cell, and pushes it.
The part that you're probably missing is some sort of model that stores the data displayed in the table. The view controller for the table needs to know about the model because it's the data source for the table. It already needs to know where to find enough data to configure each cell in the table. If you use that same model to store the detail data, you'll be able to use it to configure the detail view controller.
Example: Let's say you have a table with just one section. A simple model for that table could be an array of dictionaries. When your table view controller needs to populate a cell, it uses the row of the index path as an index into the array to get a dictionary, and it uses that to set up the cell. Exactly the same thing happens when the user taps a cell: you use the row from the index path to get the right dictionary from the array, and you use that to set up the detail view controller. You could even just pass the whole dictionary to the detail view controller and let the detail view controller get what it needs. That way the table view controller doesn't have to worry about exactly which details the detail view controller displays.
You create an array of names (strings) and display those names in your table view.
Then once a name was selected you save it in NSUserDefaults as a string.
- (void) tableView: (UITableView *) tableView didSelectRowAtIndexPath: (NSIndexPath *) indexPath
{
NSString *selectedString = [namesArray objectAtIndex:[indexPath row]];
NSUserDefaults *infoSaved = [NSUserDefaults standardUserDefaults];
[infoSaved setObject:selectedString forKey:#"theName"];
[infoSaved synchronize];
[self performSegueWithIdentifier:#"segueToMoreInfo" sender:self];
}
Then at your second view controller you load the information from an NSDictionary that contains NSDictionary items.
-(void) loadData {
NSUserDefaults *infoSaved = [NSUserDefaults standardUserDefaults];
NSString *theName = [infoSaved objectForKey:#"theName"];
NSDictionary *currentNameToDisplay = [itemsDictionary objectForKey:theName];
// then you can set your labels with information in that NSDictionary item
[nameLabel setString:[currentNameToDisplay objectForKey:#"userName"];
[ageLabel setString:[currentNameToDisplay objectForKey:#"userAge];
}
That would be a simple basic structure.
You can save the main Names NSDictionary that contains the items into a plist file or even better save the dictionary in NSUserDefaults.
Hope that helps.
I would like to know if there is any sneaky way of getting a UITableViewCell to appear at the beginning of a UITableView no matter what the array that is going to populate the tableview contains?
I am having issues where I would like to have a "select all" cell at the top of the tableview but currently having issues trying to adjust the arrays I have going into the tableView:cellForRowAtIndexPath:
Not sure of your end goal, but there are many ways some easier (sneaky ways) than others depending on your needs. It sounds like your items are fairly static, so you can just insert them before you display or update the table row.
say you have a datasource called self.items and you did something to get data. Maybe in your app you are round tripping to some data source based upon input, like sort selectors or from the search bar delegate.
try something like this in the area where you load your datasource.
NSString *myCustomObject = #"Jump To Songs";
self.items = [self getGetMovieList];
[self.items insertObject:myCustomObject atIndex:0];
[self.tableView reloadData];
Another easy way would be to add sections to your table and just make the first section your navigation items.
Note: you may need to handle the actions in didSelectRowAtIndexPath......
there you go, not so sneaky, but pretty simple.
be well
You can make two types of UITableViewCell, then return the "select all" one if indexPath.row == 0.
On the other hand, how about just make a UIView for the "select all" functionality, and set it as the tableHeaderView of your table view?
I have a UITableView in my MainViewController. When a user taps a cell,
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
selectedRow = indexPath;
....
[self performSegueWithIdentifier:#"OtherViewControllerSegue" sender:self];
}
and they are taken to another UIViewController (let's call it OtherViewController). In OtherViewController, the name for the selected cell is set. When OtherViewController is dismissed, it updates the cell in MainViewController with the new name:
[[[mainvc.myTableView cellForRowAtIndexPath:mainvc.selectedRow] textLabel] setText:namecell.textField.text];
[self.navigationController popViewControllerAnimated:YES];
This all works fine until I have more cells than will fit on the screen. If there are more cells than will fit on the screen (8 for iPhone or 16 for iPad), then this will also set the name for every eighth or sixteenth cell respectively. Any ideas on what I am doing wrong?
Update:
- (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [pointsTableView dequeueReusableCellWithIdentifier:#"myTableCell"];
return cell;
}
This is due to cell-reuse and you are mixing up your model with your view (in the MVC context).
A table-cell is a transient thing, once it goes off the screen it is reused (instead of creating new cells) when another cell is needed. This is what the dequeueReusableCellWithIdentifier: method does.
This means you can't store data in there and expect it to still be valid later on. In this example you are trying to store the name in the table cell. The reason to set a property (like the label text) on any view object is purely for display, not for storage. So to solve this problem you should maintain a list of objects in your model (this could be in separate classes or in an array in your mainvc object for example). Then in cellForRowAtIndexPath: you should set the label text every time - even when there should be no label you need to set it to nil or an empty string because the cells are re-used it might contain something from the last time it was used.
Update:
Instead of calling cellForRowAtIndexPath: yourself and setting its text, you should set the text in your model using a method or property in your controller and then tell the table view to reload that cell. The code might look something like this:
// This code is in where you want to set the text from
[mainvc setText:someText forIndexPath:indexPath];
.. and in your main view controller:
- (void)setText(NSString*)newText forIndexPath:(NSIndexPath*)indexPath
{
// Store the text in your model here...
...
// If the view is loaded, the table view should reload the cell.
if(self.isViewLoaded)
{
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
The table view will then call cellForRowAtIndexPath: where the text will be set correctly. This may seem a little convoluted at first, but when you get used to using the Model-View-Controller design pattern you will find that keeping the jobs of each MVC component separate like this will mean your code is tidier, easier to understand, has less bugs, is easier to update/extend, etc.
You're trying to store data (the new name) in a view (the cell's label). What's probably happening is that when you re-use cells in the data source's cellForRowAtIndexPath method, some of them are ones that have had this text set for them and it's still there.
The better idea is to make your changes in whatever array you use as cell information and then reload the table view to make the changes visible.
As I suppose, you shouldn't call cellForRowAtIndexPath by yourself. It can be called to create cell, not to change it.
You can update your table by passing needed string to the first view via delegate, for example. And on the event (user sets the name) you can update all table and set needed names to cells.
Hard to say exactly what the problem is, but one possible solution might be this:
Make sure that in your cellForRowAtIndexPath you are initializing the cells like this:
// Create the Cell
static NSString *recordCell = #"pickerTableCell";
cell = [tableView dequeueReusableCellWithIdentifier:recordCell];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:recordCell];
}
I know this is primarily a memory solution, but might gelp here too.
Also, look through your code and check how you are determining which cell is renamed. You could be accidentally calling the rename on more than one cell without realizing it
I am trying to delete a row from UITableview outside the delegate method. I am calling a method when I click a button inside a table cell and trying to delete the row inside that method.
Here is the function I am using
UIButton *btn = (UIButton*)sender;
int tag = btn.tag;
UITableViewCell *buttonCell = (UITableViewCell*)[[btn superview] superview];
NSIndexPath *indexPath = [self.msgTbl indexPathForCell:buttonCell];
[deleg.rmessages removeObjectAtIndex:buttonRow];
[self.msgTbl deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationTop];NSInteger buttonRow = indexPath.row;
[self.msgTbl reloadData];
Using this one or two rows get deleted but after that its crashing giving exception
Number of rows before and after deletion must be same
How can I do this in ios?
Thanks
Your problem is that the data that is being taken to populate your table isn't consistent with the table after deleting the cell. Make sure your dataSource methods provide the correct data after doing this (for example, if it is an array of objects you are using to populate the table, you must remove the object from the array as well)
The root issue is that this method:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
Must return the correct number of rows, and it isn't. When you remove the item from deleg.rmessages, is this the same object that is being used to supply the return value of the above method? (Something like [deleg.rmessages count]?)
Also, in my experience that exception often gives you more details, in particular:
How many items it had before
How many were added/deleted
How many it expects to have vs. how many it does have after the reload
Do you see anything like this being mentioned? If so, it would be worth including in your question.
Sidenote:
It's a bad idea to rely on:
UITableViewCell *buttonCell = (UITableViewCell*)[[btn superview] superview];
To return the UITableViewCell. You appear to assign the tag of the button to a local variable, but never use it. (Maybe this would be a good place to store the index of the UITableViewCell, and then subclass the cell to maintain an ivar to the button?) This is only part of the problem.
I have a tableview where the user can make sections of people using a slider. So each section can have any number of people. I want to save the state of that tableview and then reload it when they come back.
I figured that since I'm using core data I can give each person a row and section attribute. So I'm able to save that but I don't know the best way to use those values to fill the tableview when it reappears.
I don't think that NSUserDefaults would work the best because I have many groups that can be broken into sections. I've been struggling with the best way to do this for a few days now and I'm still not sure what way to go.
More (per mihir mehta):
// Set core data values
int sec = 0;
int row = 0;
for (NSArray *section in groupsArray) {
for (People *person in section) {
[person setSubgroupSection:[NSNumber numberWithInt:sec]];
[person setSubgroupRow:[NSNumber numberWithInt:row]];
row++;
}
sec++;
row = 0; // new section so restart the row count
}
If you are already familiar with CoreData then perhaps you should stick with the plan you describe. The way I see it you should make some kind of TableViewInfoManagedObject:NSManagedObject. This TableViewInfoManagedObject should have members like #dynamic numberOfSections for example that describe what you need for your table view to work.
If you use CoreData to manage the people already consider using relationships to map numberOfSections to numberOfGroups or whatever you have in your People:NSManagedObject.
Also you need to consider when the appropriate time to "save" your state, which seems to be completely determined by the slider. In that case you may want to implement an IBAction for valueChanged.
EDIT 0: Based on the snippet you have provided it seems like at the end of the loop you would have the requisite info you need. The final value of sec should correspond to the UITableViewDataSource delegate method -(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView and I am not really sure why you are setting the row number of the People object unless you are trying to achieve some sort order, which should be accomplished anyway by an NSSortDescriptor. So tableView:numberOfRowsInSection should return something like [[peopleinSection: section] count] and your tableView:cellForRowAtIndexPath should be set up so that it returns a cell like cell.textLabel.text = [[[peopleInSection:indexPath.section] objectAtIndex:indexPath.row] getPersonName]. Makes sense?
How about creating a class (subclass of NSObject) for each object that you need to save, and in that class you can add properties for each object. Then, you can use NSKeyedArchiver/Unarchiver to save and load your objects back to reuse.
Make a function that will take row and Section as argument and Returns that particular person by searching the array .... Got my point ?