Please note, this blog entry is from a previous course. You might want to check out the current one.
Allow users to reorder their itinerary for their vacation. To do this, you might want to think about creating a top-level Entity (Itinerary) in your schema and using an “ordered to-many relationship” to store the places in the itinerary. An “ordered to-many relationship” appears in your code as an NSOrderedSet (instead of an NSSet). The table view that shows the places in the itinerary will have to be rewritten to display this NSOrderedSet of places (it won’t be able to be an NSFetchedResultsController-based table view) and you will have to figure out how to use UITableView API to edit an NSMutableOrderedSet. Warning: while this is not that difficult to implement coding- wise, it requires quite a bit of investigation to figure out. Another approach is to add an attribute in your schema that determines the order (but this can be a little bit clunky). The former approach will probably lead to more learning opportunities.
Following the instructions from above create a new Entity called “Itinerary” for the Core Data Model with a ordered one-to-many relationship called “places” on the Itinerary side and “itinerary” on the place side. Recreate all NSManagedObject subclasses and add a category for the Itinerary class.
The Itinerary entity needs only a single object
+ (Itinerary *)singleItineraryInManagedObjectContext:(NSManagedObjectContext *)context { Itinerary *itinerary; NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Itinerary"]; NSError *error; NSArray *matches = [context executeFetchRequest:request error:&error]; if (!matches || ([matches count] > 1 || error)) { NSLog(@"Error in singleItineraryInManagedObjectContext: %@ %@", matches, error); } else if ([matches count] == 0) { itinerary = [NSEntityDescription insertNewObjectForEntityForName:@"Itinerary" inManagedObjectContext:context]; } else { itinerary = [matches lastObject]; } return itinerary; }
which is used when a place is set
+ (Place *)placeFromFlickrInfo:(NSDictionary *)flickrInfo inManagedObjectContext:(NSManagedObjectContext *)context { .... } else if (![matches count]) { .... place.itinerary = [Itinerary singleItineraryInManagedObjectContext:context]; } else { place = [matches lastObject]; if (!place.itinerary) place.itinerary = [Itinerary singleItineraryInManagedObjectContext:context]; } return place; }
It is not possible to use the NSFetchResultsController to populate the “new” itinerary table. Thus its class will be a subclass of UITableViewController instead of CoreDataTableViewController and needs a new property as data model:
// ItineraryTableViewController.h @interface ItineraryTableViewController : UITableViewController @property (nonatomic, strong) Itinerary *itinerary; // ItineraryTableViewController.m @synthesize itinerary = _itinerary;
Instead of setting up the fetched results controller, the itinerary is loaded:
- (void)viewDidLoad { [super viewDidLoad]; [VacationHelper openVacation:self.vacation usingBlock:^(BOOL success) { self.itinerary = [Itinerary singleItineraryInManagedObjectContext: [VacationHelper sharedVacation:self.vacation].database.managedObjectContext]; self.navigationItem.rightBarButtonItem = self.editButtonItem; [self.tableView reloadData]; }]; }
Notice, that in the code above also a edit button is put into the navigation bar to enable the rearranging of the table cells.
Where before the fetched-results controller populated the the cells, this has now to be done using the itinerary model:
- (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 { .... Place *place = [self.itinerary.places objectAtIndex:indexPath.row]; }
And when a row is selected the code changes slightly, too:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { .... Place *place = [self.itinerary.places objectAtIndex:indexPath.row]; .... }
The edit button added above is by default wired to the following function
- (void)setEditing:(BOOL)editing animated:(BOOL)animated { [super setEditing:editing animated:animated]; [self.tableView setEditing:editing animated:YES]; }
Be default the editing style is set to delete and indents the rows when in editing mode, which are not really needed for this task and should be changed:
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { return UITableViewCellEditingStyleNone; } - (BOOL)tableView:(UITableView *)tableView shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath *)indexPath { return NO; }
For the actual rearranging create a mutual copy of the places set, change the position of the selected place and write the set back.
- (void) tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath { Place *place = [self.itinerary.places objectAtIndex:sourceIndexPath.row]; NSMutableOrderedSet *places = [self.itinerary.places mutableCopy]; [places removeObject:place]; [places insertObject:place atIndex:destinationIndexPath.row]; self.itinerary.places = places; self.placesHaveBeenRearranged = YES; }
Note that for the functionality the generated subclass provides two functions removePlacesObject: and insertObject:inPlacesAtIndex:. But both seem to try to manipulate the unmutable NSOrderedSets and thus crash the application if used (status Xcode 4.3 newer version might fix this problem).
In addition use a new property placesHaveBeenRearranged to know if the database should be saved when the done button is pressed:
- (void)setEditing:(BOOL)editing animated:(BOOL)animated { ... if (!editing && self.placesHaveBeenRearranged) { VacationHelper *vh = [VacationHelper sharedVacation:self.vacation]; [vh.database saveToURL:vh.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL]; self.placesHaveBeenRearranged = NO; } }
The complete code for this task is available at github.