NSFetchedResultsController and NSOrderedSet relationships - iphone

I am having an issue (understanding issue to be honest) with NSFetchedResultsController and the new NSOrderedSet relationships available in iOS 5.
I have the following data-model (ok, my real one is not drawer's and sock's!) but this serves as a simple example:
Drawer and Sock are both NSManagedObjects in a Core Data model/store. On Drawer the socks relationship is a ordered to-many relationship to Sock. The idea being that the socks are in the drawer in a specific order. On Sock the drawer relationship is the inverse of the socks relationship.
In a UIViewController I am drawing a UITableView based on these entities. I am feeding the table using a NSFetchedResultsController.
- (NSFetchedResultsController *)fetchedResultsController1 {
if (_fetchedResultsController1 != nil) {
return _fetchedResultsController1;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Sock" inManagedObjectContext:[NSManagedObjectContext MR_defaultContext]];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"drawer.socks" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
self.fetchedResultsController1 = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:[NSManagedObjectContext MR_defaultContext] sectionNameKeyPath:nil cacheName:#"SocksCache"];
self.fetchedResultsController1.delegate = self;
return _fetchedResultsController1;
}
When I run this, I get the following errror: *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'to-many key not allowed here'
This makes sense to me, as the relationship is a NSOrderedSet and not a single entity to compare against for sorting purposes.
What I want to achieve is for the Socks to appear in the UITableView in the order specified in the socks relationship. I don't really want to have a sort order but NSFetchedResultsController, which is a great component is insisting there has to be one. How can I tell it to use the socks order on the Drawer entity. I don't want the table to show Drawer entities at all.
Note: I am using this within an iOS5 application only, so ordered relationships are available.
Anyone that can offer me any direction, would be greatly appreciated. Thanks for your time.
Edit: So the tableview that display's socks does so for just one drawer. I just want the table view to honor the order that the socks relationship contains. I'm not sure what to set the sort criteria to make sure that happens.

Damien,
You should just make your NSFetchRequest use the array form of the ordered set. It will operate fine. Your controller needs an attribute to sort on. Hence, you'll need to specify that too.
Andrew

I found this thread while looking for an answer to the exact question posed by the OP.
I never found any examples of presenting such data in a tableView without adding the additional sort field. Of course, adding the sort field pretty much eliminates any benefit in using the ordered relationship. So given that I got this working, I thought it might be helpful to others with the same question if I posted my code here. It turned out to be quite simple (much simpler than using an additional sort field) and with apparently good performance. What some folks may not have realized (that includes me, initially) is that the type (NSOrderedSet) of the "ordered, to-many" relationship attribute has a method for getting objectAtIndex, and that NSMUtableOrderedSet has methods for inserting, and removing objectAtIndex.
I avoided using the NSFetchedResultsController, as some of the posters suggested. I've used no array, no additional attribute for sorting, and no predicate. My code deals with a tableView wherein there is one itinerary entity and many place entities, with itinerary.places being the "ordered, to-many" relationship field. I've enabled editing/reordering, but no cell deletions. The method moveRowAtIndexPath shows how I've updated the database with the reordering, although for good encapsulation, I should probably move the database reordering to my category file for the place managed object. Here's the entire TableViewController.m:
//
// ItineraryTVC.m
// Vacations
//
// Created by Peter Polash on 8/31/12.
// Copyright (c) 2012 Peter Polash. All rights reserved.
//
#import "ItineraryTVC.h"
#import "AppDelegate.h"
#import "Place+PlaceCat.h"
#import "PhotosInVacationPlaceTVC.h"
#interface ItineraryTVC ()
#end
#implementation ItineraryTVC
#define DBG_ITIN YES
#synthesize itinerary ;
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.navigationItem.rightBarButtonItem = self.editButtonItem ;
}
- (void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated] ;
UIManagedDocument *doc = UIAppDelegate.vacationDoc;
[doc.managedObjectContext performBlock:^
{ // do this in the context's thread (should be the same as the main thread, but this made it work)
// get the single itinerary for this document
self.itinerary = [Itinerary setupItinerary: doc ] ;
[self.tableView reloadData] ;
}];
}
- (void)viewDidUnload
{
[super viewDidUnload];
// Release any retained subviews of the main view.
// e.g. self.myOutlet = nil;
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown );
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
return [self.itinerary.places count ];
}
- (UITableViewCell *) tableView: (UITableView *) tableView
cellForRowAtIndexPath: (NSIndexPath *) indexPath
{
static NSString *CellIdentifier = #"Itinerary Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: CellIdentifier ];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault reuseIdentifier: CellIdentifier];
}
Place *place = [self.itinerary.places objectAtIndex:indexPath.row];
cell.textLabel.text = place.name;
cell.detailTextLabel.text = [NSString stringWithFormat:#"%d photos", [place.photos count]];
return cell;
}
#pragma mark - Table view delegate
- (BOOL) tableView: (UITableView *) tableView
canMoveRowAtIndexPath:( NSIndexPath *) indexPath
{
return YES;
}
-(BOOL) tableView: (UITableView *) tableView
canEditRowAtIndexPath: (NSIndexPath *) indexPath
{
return YES ;
}
-(void) tableView: (UITableView *) tableView
moveRowAtIndexPath: (NSIndexPath *) sourceIndexPath
toIndexPath: (NSIndexPath *) destinationIndexPath
{
UIManagedDocument * doc = UIAppDelegate.vacationDoc ;
[doc.managedObjectContext performBlock:^
{ // perform in the context's thread
// itinerary.places is the "ordered, to-many" relationship attribitute pointing to all places in itinerary
NSMutableOrderedSet * places = [ self.itinerary.places mutableCopy ] ;
Place *place = [ places objectAtIndex: sourceIndexPath.row] ;
[places removeObjectAtIndex: sourceIndexPath.row ] ;
[places insertObject: place atIndex: destinationIndexPath.row ] ;
self.itinerary.places = places ;
[doc saveToURL: doc.fileURL forSaveOperation: UIDocumentSaveForOverwriting completionHandler: ^(BOOL success) {
if ( !success ) NSLog(#"Error saving file after reorder, startPos=%d, endPos=%d", sourceIndexPath.row, destinationIndexPath.row) ;
}];
}];
}
- (UITableViewCellEditingStyle) tableView: (UITableView *) tableView
editingStyleForRowAtIndexPath: (NSIndexPath *) indexPath
{
return ( UITableViewCellEditingStyleNone ) ;
}
- (void) prepareForSegue:(UIStoryboardSegue *) segue sender: (id) sender
{
NSIndexPath *indexPath = [self.tableView indexPathForCell: sender] ;
PhotosInVacationPlaceTVC * photosInVacationPlaceTVC = segue.destinationViewController ;
Place *place = [self.itinerary.places objectAtIndex:indexPath.row ];
photosInVacationPlaceTVC.vacationPlace = place ;
photosInVacationPlaceTVC.navigationItem.title = place.name ;
UIBarButtonItem *backButton =
[[UIBarButtonItem alloc] initWithTitle:#"Back" style:UIBarButtonItemStylePlain target:nil action:nil];
self.navigationItem.backBarButtonItem = backButton;
}
#end

You can give Sock an index attribute and order the socks by them:
sock01.index = [sock01.drawer.socks indexOfObject:sock01];

To add some more clarity to the answer from adonoho (thanks mate) which helped me work it out - rather than trying to specify the to-many relationship as any sorting key which I also couldn't get working, specify the belongs-to relationship in a predicate to select which entities you want in the fetched results controller, and specify the belongs-to relationship as the sort key.
This satisfies the primary aim of having the results in a NSFetchedResultsController with all that goodness, and respects the order in the to-many relationship.
In this particular example (fetching socks by drawer):
// socks belong-to a single drawer, sort by drawer specified sock order
fetchRequest.sortDescriptors = #[[[NSSortDescriptor alloc] initWithKey:#"drawer" ascending:YES]];
// drawer has-many socks, select the socks in the given drawer
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"drawer = %#", drawer];
Essentially, this uses a given drawer to specify which socks should go into the NSFetchedResultsController, and the ordering as specified by the to-many relationship.
Looking at the generated SQL (annotated from my example using different entity names) via a core data SQL debug:
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, ...fields..., t0.ZDRAWER, t0.Z_FOK_DRAWER FROM ZSOCKS t0 WHERE t0.ZDRAWER = ? ORDER BY t0.Z_FOK_DRAWER
you can see the SQL ordering using the Z_FOK_DRAWER column, which Core Data uses for the position of that sock.

As far as I understand this functionality, it allows to have ordered Socks in every Drawer. As Apple writes in documentation:
You should use them only if a relationship has intrinsic ordering that
is critical to its own representation—such as the steps in a recipe.
This means you can't fetch all Socks using sorted relation. Sorted Socks will be available only in every Drawer object.

As I just answered here, I prefer just adding a new property to my NSManagedObject via a category.
Just add a method:
- (NSUInteger)indexInDrawerSocks
{
NSUInteger index = [self.drawer.socks indexOfObject:self];
return index;
}
Then in your NSFetchedResultsController, use sort descriptor:
fetchRequest.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"indexInDrawerSocks" ascending:YES]];

Related

Accessing String from NSIndex

I currently have a UITableView that is populated by a .plist full of exercises. What I want to be able to do is access individual exercises within the table by storing each exercise that is clicked on into an array, that will later be used to populate a seperate UITableView.
How exactly do I get access to these individual cells so that I can store them into this array. Here is what I have so far:
-(IBAction) saveWorkout {
NSMutableArray *workout = [NSMutableArray arrayWithCapacity:10];
[workout addObject: ] // I'm assuming this is where I add a cell to an array (or atleast the cell's string).
}
Any help?
Without delving too much into the actual code part of your question, calling -cellForRowAtIndexPath to retrieve a title is (can be) extremely expensive, especially if it is called multiple times. Use -didSelectRowAtIndexPath: to get the index of the title within your datasource array, then add that object to your list. Call -saveWorkout when you are finished/reach a certain limit.
A same might look like:
-(void)didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
//other code and such...
//get the index of the object in our original array and add the corresponding object to the new array.
[customWorkoutArray addObject:[workoutArray objectAtIndex:indexPath.row]];
}
To restate #CodaFi in code:
#property (strong, nonatomic) NSMutableArray *selectedElements;
#synthesize selectedElements=_selectedElements;
- (NSMutableArray *)selectedElements {
if (!_selectedElements) {
_selectedElements = [NSMutableArray array];
}
return _selectedElements;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
id element = [self.myModel objectAtIndex:indexPath.row];
// this is the key: this array will now be the basis for your subsequent table of selected items
[self.selectedElements addObject:element];
// do something to your model here that indicates it's been selected
// have your cellForRow... method interrogate this key and draw something special
[element setValue:[NSNumber numberWithInt:1] forKey:#"selected"];
// reload that row so it will look different
[tableView beginUpdates];
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
-(void)didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
You can get the index here by indexPath.row
}

Array nil (even when addObject:) [duplicate]

This question already has answers here:
Closed 11 years ago.
Possible Duplicate:
addObject: to array not working (array still nil)
EVERYTHING UPDATED
This app is a table view with a tab bar controller. I am logging the count of the array: arrayOfFavourites and even though i add an object is continues to have a nil value, my relating code, all objects shown are allocated and initialized in the code (previous or present) some are instances and some are properties:
ListViewController.m:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"TOUCHED CELL!");
// Push the web view controller onto the navigation stack - this implicitly
// creates the web view controller's view the first time through
[[self navigationController] pushViewController:webViewController animated:YES];
// Grab the selected item
entry = [[channel items] objectAtIndex:[indexPath row]];
if (!entry) {
NSLog(#"!entry");
}
// Construct a URL with the link string of the item
NSURL *url = [NSURL URLWithString:[entry link]];
// Construct a request object with that URL
NSURLRequest *req = [NSURLRequest requestWithURL:url];
// Load the request into the web view
[[webViewController webView] loadRequest:req];
// Take the cell we pressed
// IMPORTANT PART
CELL = [tableView cellForRowAtIndexPath:indexPath];
[webViewController setItem:entry];
webViewController = nil;
webViewController = [[WebViewController alloc] init];
[entry release];
}
WebViewController.m:
You shake to favorite a cell
-(void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
cellToPassOn = nil;
NSLog(#"Favouriting"); // YES I KNOW SPELLING
// This is pretty simple, what we do is we take the cell we touched and take its title and link
// then put it inside an array in the Favourites class
Favourites *fav = [[Favourites alloc] init];
ListViewController *list = [[ListViewController alloc] init];
[self setCellToPassOn: [list CELL]];
if (!item) {
NSLog(#"NILLED ITEM");
}
[[fav arrayOfFavourites] addObject:[item autorelease]];
[fav setCell: cellToPassOn];
}
Favourites.m:
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSLog(#"ROWS NO.");
NSLog(#"%i", [arrayOfFavourites count]);
return [arrayOfFavourites count];
}
Favourites:
A CLASS, ON TAB BAR CONTROLLER
WEBVIEWCONROLLER:
CONTROLLER FOR DIFFERENT WEB VIEWS
LISTVIEWCONTROLLER:
DATA PROVIDER
Actually what happens is when i shake i reload the table view data and i add an object to an array (array of favorites) the count is one.... GOOD! But then when i shake again (in a different article, my app has different webViews when i press different cells.) It is still 1... weird.... if i go to favorites class that array remains one.. ok... so as you can see i am returning the arrayOfFavourites count to numberOfRowsInSection (which is 1) but no cells appear and the cellForRowAtIndexPath is never called (using an NSLOG) why is this happening I AM VERY ANNOYED!
In your Favourites.m numberOfRowsInSection function, looks like you should do this:
if(arrayOfFavourites == NULL)
{
arrayOfFavourites = [[NSMutableArray alloc] init];
}
Because you're reinitializing (and likely leaking) in every single call to numberOfRowsInSection (which gets called each time the table needs to know how many rows it must display -- i.e. very often).
You create and destroy a new Favourites object every time you go through -motionBegan:withEvent:, and you create a new array every time you go through -tableView:numberOfRowsInSection:. If you want data to persist beyond those events, you need to keep the objects around.
Let review this part of your code :
-(void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event {
cellToPassOn = nil;
NSLog(#"Favouriting"); // YES I KNOW SPELLING
// This is pretty simple, what we do is we take the cell we touched and take its title and link
// then put it inside an array in the Favourites class
// HERE you are creating a NEW fav
Favourites *fav = [[Favourites alloc] init];
// HERE you are creating a NEW list
ListViewController *list = [[ListViewController alloc] init];
// SO HERE what is "CELL" doing, returning some constant or static object?
[self setCellToPassOn: [list CELL]];
// HERE what is item and where does it come from
if (!item) {
NSLog(#"NILLED ITEM");
}
// Here you take an array of an object you just created and autoreleasing the item
// this is not the regular way to handle memory management in Cocoa,
// depending on what you are doing to item else where you could get item == deallocated pretty soon
[[fav arrayOfFavourites] addObject:[item autorelease]];
[fav setCell: cellToPassOn];
HERE you are releasing fav
[fav release];
HERE fav don't exist anymore as well as the array to which you've added something to it.
[list release];
item = nil;
}
Unless I'm reading your code incorrectly I have the feeling that you are trying to have some persistence with "volatile" object.
You need to make a #property for those object to survive longer that one method call.
Every time you create a new object it's new and have no knowledge of the precedent one.
And if we look at this code :
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// HERE everytime the tableview is asking for the numberOfRowsInSection, you create a new array
// And that new empty array is replacing your old one.
// That is your "weird" thing, it's doing what you are asking it to do, set it back to a new empty array.
arrayOfFavourites = [[NSMutableArray alloc] init];
NSLog(#"ROWS NO.");
NSLog(#"%i", [arrayOfFavourites count]);
return [arrayOfFavourites count];
}

Can you edit core data on ListViewController

I have a Core Data application set up with a ListViewController, a DetailViewController and an EditingViewController, where most of the editing occurs. On the ListViewController, I have a graphic of a checkbox, and I can toggle two images by selecting the row in
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
Is it possible to edit the Core Data record from the ListViewController so that I can make my selection persistent? I cannot work out the syntax to select the record, edit, and save the value - which will simply toggle from true to false.
jon
Thanks for the prompt response! Additional information after Answer 1. Your assumption is correct. I am modeling this application on the CoreData Books sample. I am not using a button, but rather using two images to create a checked and unchecked checkbox. I added a boolean "check" to my entity, recreated the header file, and added the header file to my ListViewController. Here is a simplified version of the datamodel header file.
#interface Patient : NSManagedObject
{
}
#property (nonatomic, retain) NSString * location;
#property (nonatomic, retain) NSNumber * check;
#property (nonatomic, retain) NSString * lastName;
#end
And here is my modification of your code:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
NSManagedObject *entityObject = [self.fetchedResultsController objectAtIndexPath:indexPath];
if (![entityObject.check boolValue]) {
entityObject.check = [NSNumber numberWithBool:YES];
cell.imageView.image = [UIImage imageNamed:#"check.png"];
}
else {
entityObject.check= [NSNumber numberWithBool:NO];
cell.imageView.image = [UIImage imageNamed:#"uncheck.png"];
}
However, this gives me the error "Request for member 'check' in something not a structure or union". I verified that the new attribute is a boolean and that the header file is imported to the ListViewController. Any thoughts?
Lastly, does this code eliminate the need to save entityObject.check to the database? Thanks again.
It would be nice to get a few more details about what your app does before we can provide a more informed recommendation. I'll make a few assumptions anyway to see if it helps...
Firstly, when dealing with BOOL properties in Coredata, it is important to remember that they are actually saved as NSNumbers so keep that in mind when you need to test their values and make sure you are using the right methods.
Assuming the data in your ListViewController is being populated by CoreData and your data model defines an entity "Entity" with an attribute of type BOOL called "favourite", here's what you can try:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
NSManagedObject *entityObject = [self.fetchedResultsController objectAtIndexPath:indexPath];
if (![entityObject.favourite boolValue]) {
entityObject.favourite = [NSNumber numberWithBool:YES];
[self.yourToggleButton setSelected:YES]; // this assumes your checkbox is a UIButton and you want its view to be updated to checked when your BOOL == YES
}
else {
entityObject.favourite = [NSNumber numberWithBool:NO];
[self.watchListButton setSelected:NO];
}
The above assumes you have a fetchedResultsController taking care of the data fetching from your CoreData database too. If not I strongly recommend you check out one the tutorials from Apple (CoreData Books and CoreData Recipes) to understand how they work as it will make your life much easier - NSFetchedResultsController make it pretty straight forward to keep your data syncronised between different views which is something you are going to have to consider specially because you are allowing different attributes to be edited in different view controllers.
I hope this helps but feel free to ask any additional questions if you need to.
Cheers,
Rog
I came up with a slightly different solution, but Rog's post definitely got me on the right track. I had some trouble with the boolean syntax, so I changed the entity attribute to a string and used "N" and "Y" instead. I also found that I needed to save the value to make it persistent. I appreciate the help.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// Establish connections
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
Patient *patient = [self.fetchedResultsController objectAtIndexPath:indexPath];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
// Toggle images and check value with each row tap
if ([patient.check isEqualToString: #"N"] == YES) {
cell.imageView.image = [UIImage imageNamed:#"check.png"];
patient.check = #"Y";
}
else {
cell.imageView.image = [UIImage imageNamed:#"uncheck.png"];
patient.check = #"N";
}
// Save check value
NSError *error = nil;
if (![context save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}

UITableView Core Data reordering

I know this question has been asked before, and I took a look at the answer to this question. However, I'm still confused as to how to implement reordering with a UITableView in a Core Data project. What I know is that I need to have a "displayOrder" attribute in my Entity to track the order of items, and I need to enumerate through all the objects in the fetched results and set their displayOrder attribute.
In the given code in the question I linked to, the table view delegate method calls a method like this [self FF_fetchResults];, and the code for that method is not given so its hard to tell what exactly it is.
Is there any sample code that demonstrates this? That would be simpler to look at than sharing large chunks of code.
Thanks
I'm not sure which part you are having trouble with (based on the comments)... but here is my suggestion. The displayOrder is just a simple attribute on a NSManagedObject class. If you can save a managed object, then you will be able finish this feature. Lets first take a simple NSManagedObject:
#interface RowObj : NSManagedObject
{
}
#property (nonatomic, retain) NSString *rowDescription;
#property (nonatomic, retain) NSNumber *displayOrder;
Next, we need to have local copy of the data being displayed in the tableview. I have read through the comments you have made and I'm not really sure if you are using the FetchedResultsController or not. My suggestion would be to start simple and just use a normal tableviewcontroller where you update the row data whenever a user changes the display order... then save the order when the user is done editing.
The interface for this tableviewcontoller would look like this:
#interface MyTableViewController : UITableViewController {
NSMutableArray *myTableViewData;
}
#property(nonatomic,retain) NSMutableArray *myTableViewData;
#end
Next, we need to load the the table view data in the viewWillAppear method:
- (void)viewWillAppear:(BOOL)animated {
myTableViewData = [helper getRowObjects]; // insert method call here to get the data
self.navigationItem.leftBarButtonItem = [self editButtonItem];
}
There are 2 things going on here... (I'll explain the editButtonItem later) the first is that we need to get our data from CoreData. When I have to do this I have some sort of helper(call it what you want) object do the work. A typical find method would look like this:
- (NSMutableArray*) getRowObjects{
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"RowObj" inManagedObjectContext:[self managedObjectContext]];
[request setEntity:entity];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"displayOrder" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[request setSortDescriptors:sortDescriptors];
[sortDescriptors release];
[sortDescriptor release];
NSError *error;
NSMutableArray *mutableFetchResults = [[managedObjectContext executeFetchRequest:request error:&error] mutableCopy];
if (mutableFetchResults == nil) {
// Handle the error.
}
[request release];
return mutableFetchResults;
}
Now that you have your data, you can now wait for the user to edit the table. That is where the [self editButtonItem] comes into play. This is a built in feature that returns a bar button item that toggles its title and associated state between Edit and Done. When the user hits that button, it will invoke the setEditing:animated: method:
To update the display order you need to override the setEditing method on the UITableViewController class. It should look something like this:
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
[super setEditing:editing animated:animated];
[myTableView setEditing:editing animated:animated];
if(!editing) {
int i = 0;
for(RowObj *row in myTableViewData) {
row.displayOrder = [NSNumber numberWithInt:i++];
}
[helper saveManagedObjectContext]; // basically calls [managedObjectContext save:&error];
}
}
We don't have to do anything when the user is in edit mode... we only want to save once they have pressed the "Done" button. When a user drags a row in your table you can update your display order by overriding the canMoveRowAtIndexPath and moveRowAtIndexPath methods:
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath {
return true;
}
(void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath {
RowObj *row = [myTableViewData objectAtIndex:sourceIndexPath.row];
[myTableViewData removeObjectAtIndex:sourceIndexPath.row];
[myTableViewData insertObject:row atIndex:destinationIndexPath.row];
}
Again, the reason I don't update the displayOrder value here is because the user is still in edit mode... we don't know if the user is done editing AND they could even cancel what they've done by not hitting the "Done" button.
EDIT
If you want to delete a row you need to override tableView:commitEditingStyle:forRowAtIndexPath and do something like this:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete the managed object at the given index path.
RowObj *row = [myTableViewData objectAtIndex:indexPath.row];
[helper deleteRow:row];
// Update the array and table view.
[myTableViewData removeObjectAtIndex:indexPath.row];
[myTableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:YES];
}
}

Dynamically Changing UITableView's Contents

I have an NSURL object which gets data from my site, based on a variable entered by the user into the search bar.
I split this data into an NSArray.
Once I have done that I wish to display the data in a UITableView.
My question is this. Is it possible to load the data into a UITableView dynamically?
i.e. Program loads, no data so UITableView is empty, then the user searches for one variable. Gets some data and the contents is loaded into the UITableView. Searches for a new variable, old data is cleared from UITableView and the new data is added?
I'm currently trying to do this using interface builder, but fear that I may have to make my interface pragmatically, so that I could destroy and re-create the UITableView, but i'm, not sure.
Thanks for any help.
Sure the method reloadData on UITableView will do the trick
Fear not, subclassing UITableView is very easy. In xCode simply choose new file, choose "Cocoa Touch Classes", "Objective-c class" and in the "Subclass of" dropdown pick "UITableView". xCode will add a UITableViewController subclass complete with stubs to build on.
I filled in a very simple example that draws the table data from an array and is displayed from the Application Delegate. As you suggested sending a reloadData message to the UITableView will refresh the displayed data.
As you probably found out, using InterfaceBuilder for this job is a lot harder than doing it programatically.
Cheers, niels
//
// MyTableViewController.m
// TableView
//
// Created by Niels Castle on 7/15/09.
// Copyright 2009 Castle Andersen ApS. All rights reserved.
//
#import "MyTableViewController.h"
#implementation MyTableViewController
// Initializer do custom initialisation here
- (id)initWithStyle:(UITableViewStyle)style {
if (self = [super initWithStyle:style]) {
// This is the source of my data. The simplest source possible,
// an NSMutableArray, of name. This would be the data from your web site
array = [[NSMutableArray alloc]
initWithObjects:#"Niels", #"Camilla", #"William", nil];
}
return self;
}
// How many section do we want in our table
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
// Customize the number of rows in the table view
// Simply the number of elements in our array of names
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
return [array count];
}
// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// Reuse cells
static NSString *id = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:id];
if (cell == nil) {
cell = [[[UITableViewCell alloc]
initWithStyle: UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier] autorelease];
}
// Simplest possible cell - displaying a name from our name array
[[cell textLabel] setText: [array objectAtIndex:[indexPath row]]];
return cell;
}
- (void)dealloc {
[super dealloc];
[array release];
}
#end
//
// TableViewAppDelegate.m
// TableView
//
// Created by Niels Castle on 7/15/09.
// Copyright Castle Andersen ApS 2009. All rights reserved.
//
#import "TableViewAppDelegate.h"
#import "MyTableViewController.h"
#implementation TableViewAppDelegate
#synthesize window;
- (void)applicationDidFinishLaunching:(UIApplication *)application {
MyTableViewController *twc = [[MyTableViewController alloc]
initWithStyle: UITableViewStylePlain];
[window addSubview: [twc view]];
[window makeKeyAndVisible];
}
- (void)dealloc {
[window release];
[super dealloc];
}
#end
It's a bit complicated, but my solution that is working very reliably is following:
(assume that you have an array as bunch of arrays, that each represents a section and contains items that are in fact rows in table).
This example fits at the situation, when we load some data from server (e.g. JSON), and the result can be very different in number of sections and/or rows.
void function, you can omit it
-(void)addToPropertiesTable {
//fullTableData is above mentioned two dimensional array
int sectionsCount = _fullTableData.count;
int count = 0;
NSMutableArray *insertIndexPaths = [NSMutableArray array];
NSMutableArray *deleteIndexPaths = [NSMutableArray array];
for(int j = 0; j < sectionsCount; j++) {
NSMutableArray *currentAdverts = [[NSMutableArray alloc] init];
[currentAdverts addObjectsFromArray:[_fullTableData objectAtIndex:j]];
count = [currentAdverts count];
int currentRowsInSection = [self.propertiesTable numberOfRowsInSection:j];
if(currentRowsInSection > 0) {
//if any data in current tableView, lets get rid of them first
for (int i = [self.propertiesTable numberOfRowsInSection:j] - 1; i >=0 ; i--)
{
[deleteIndexPaths addObject:[NSIndexPath indexPathForRow:i inSection:j]];
}
}
for (NSUInteger item = 0; item < count; item++) {
[insertIndexPaths addObject:[NSIndexPath indexPathForRow:item inSection:j]];
}
}
[self.propertiesTable beginUpdates];
//we delete old rows - whether we need them or not
[self.propertiesTable deleteRowsAtIndexPaths:deleteIndexPaths
withRowAnimation:UITableViewRowAnimationFade];
if([self.propertiesTable numberOfSections]) {
//if any sections, we remove them
NSIndexSet *nsIndexSetToDelete = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0,[self.propertiesTable numberOfSections])];
[self.propertiesTable deleteSections:nsIndexSetToDelete withRowAnimation:UITableViewRowAnimationAutomatic];
}
//here we have to set new sections, whether they have changed or not
NSIndexSet *nsIndexSetToInsert = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0,sectionsCount)];
[self.propertiesTable insertSections:nsIndexSetToInsert withRowAnimation:UITableViewRowAnimationAutomatic];
//finally we insert rows
[self.propertiesTable insertRowsAtIndexPaths:insertIndexPaths
withRowAnimation:UITableViewRowAnimationFade];
[self.propertiesTable endUpdates];
//now we see the change in UI
[self.propertiesTable reloadData];
}