I use Core Data to keep track of entries in a simple to do list. I use a simple UIAlertView with UITextField for the user to add new entries. The entries are saved using NSManagedObject, but the latest entry isn't added to the tableview, even after I run [self.tableView reloadData];
This GIF show how it works now:
Header file:
#import <UIKit/UIKit.h>
#interface PakkelisteViewController : UITableViewController <UIAlertViewDelegate>
#end
Implementation file:
#import "PakkelisteViewController.h"
#import "AUFToDoItem.h"
#interface PakkelisteViewController ()
#property NSMutableArray *toDoItems;
-(IBAction)addNewToDoItem;
#end
#implementation PakkelisteViewController
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// defaultTodos = [[NSMutableArray alloc] initWithObjects:#"Badetøy", #"Skrivesaker", #"Lommepenger", #"Godt humør", nil];
self.toDoItems = [[NSMutableArray alloc] init];
[self loadInitialData];
// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
}
-(void)viewWillAppear:(BOOL)animated
{
// Fetch the devices from persistent data store
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"AUFToDoItem"];
self.toDoItems = [[managedObjectContext executeFetchRequest:fetchRequest error:nil] mutableCopy];
[self.tableView reloadData];
}
-(void)loadInitialData
{
}
-(IBAction)addNewToDoItem
{
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:#"Legg til ny" message:nil delegate:self cancelButtonTitle:#"Avbryt" otherButtonTitles:#"Legg til", nil];
[alertView setAlertViewStyle:UIAlertViewStylePlainTextInput];
[[alertView textFieldAtIndex:0] setPlaceholder:#"Rent undertøy"];
[[alertView textFieldAtIndex:0] setAutocapitalizationType:UITextAutocapitalizationTypeSentences];
[alertView show];
}
-(BOOL)alertViewShouldEnableFirstOtherButton:(UIAlertView *)alertView
{
NSString *inputText = [[alertView textFieldAtIndex:0] text];
if ([inputText length] > 0)
{
return YES;
}
else
{
return NO;
}
}
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex == 1)
{
NSManagedObjectContext *context = [self managedObjectContext];
// Create a new managed object
NSManagedObject *toDoItem = [NSEntityDescription insertNewObjectForEntityForName:#"AUFToDoItem" inManagedObjectContext:context];
[toDoItem setValue:[alertView textFieldAtIndex:0].text forKey:#"itemName"];
[toDoItem setValue:[NSDate date] forKey:#"creationDate"];
[toDoItem setValue:NO forKey:#"completed"];
NSError *error = nil;
// Save the object to persistent store
if (![context save:&error]) {
NSLog(#"Can't Save! %# %#", error, [error localizedDescription]);
}
[self dismissViewControllerAnimated:YES completion:nil];
[self.tableView reloadRowsAtIndexPaths:[self.tableView indexPathsForVisibleRows] withRowAnimation:UITableViewRowAnimationFade];
// [self.tableView reloadData];
}
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return self.toDoItems.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
}
NSManagedObject *toDoItem = [self.toDoItems objectAtIndex:indexPath.row];
cell.textLabel.text = [toDoItem valueForKey:#"itemName"];
if ((BOOL)[toDoItem valueForKey:#"completed"] == YES)
{
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
else
{
cell.accessoryType = UITableViewCellAccessoryNone;
}
return cell;
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
/*
[tableView deselectRowAtIndexPath:indexPath animated:NO];
AUFToDoItem *tappedItem = [self.toDoItems objectAtIndex:indexPath.row];
tappedItem.completed = !tappedItem.completed;
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
*/
}
-(NSManagedObjectContext *)managedObjectContext {
NSManagedObjectContext *context = nil;
id delegate = [[UIApplication sharedApplication] delegate];
if ([delegate performSelector:#selector(managedObjectContext)]) {
context = [delegate managedObjectContext];
}
return context;
}
// Override to support conditional editing of the table view.
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
// Return NO if you do not want the specified item to be editable.
return YES;
}
// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
NSManagedObjectContext *context = [self managedObjectContext];
if (editingStyle == UITableViewCellEditingStyleDelete) {
[context deleteObject:[self.toDoItems objectAtIndex:indexPath.row]];
NSError *error = nil;
if (![context save:&error])
{
NSLog(#"Can't delete! %# %#", error, [error localizedDescription]);
return;
}
[self.toDoItems removeObjectAtIndex:indexPath.row];
[self.tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
/*
// Override to support rearranging the table view.
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
{
}
*/
/*
// Override to support conditional rearranging of the table view.
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
{
// Return NO if you do not want the item to be re-orderable.
return YES;
}
*/
/*
#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
#end
You need to add the new toDoItem to your array as well. Try this :
// Create a new managed object
NSManagedObject *toDoItem = [NSEntityDescription insertNewObjectForEntityForName:#"AUFToDoItem" inManagedObjectContext:context];
[toDoItem setValue:[alertView textFieldAtIndex:0].text forKey:#"itemName"];
[toDoItem setValue:[NSDate date] forKey:#"creationDate"];
[toDoItem setValue:NO forKey:#"completed"];
[self.toDoItems addObject:toDoItem];
[self.tableView reloadData];
You should update your dataSource self.toDoItems as it still stays the same as off in viewDidLoad method. You're only saving it in CoreData, but not refreshing your dataSourse and [tableView reloadData] wont do anything
I have a simple todo app, where a user can touch a cell to check/uncheck a task. But I am not sure how to update Core Data setting the boolean value of completed to yes.
Here is my code:
//
// PakkelisteViewController.m
// Sommerleir
//
// Created by Ronny-André Bendiksen on 08.05.14.
// Copyright (c) 2014 Arbeidernes Ungdomsfylking. All rights reserved.
//
#import "PakkelisteViewController.h"
#import "AUFToDoItem.h"
#import "TodoCell.h"
#interface PakkelisteViewController ()
#property NSMutableArray *toDoItems;
-(IBAction)addNewToDoItem;
#end
#implementation PakkelisteViewController
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.toDoItems = [[NSMutableArray alloc] init];
[self loadInitialData];
// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
}
-(void)viewWillAppear:(BOOL)animated
{
// Fetch the devices from persistent data store
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"AUFToDoItem"];
self.toDoItems = [[managedObjectContext executeFetchRequest:fetchRequest error:nil] mutableCopy];
[self.tableView reloadData];
}
-(void)loadInitialData
{
// check if user has already been using this functionality
BOOL hasRunBefore = [[NSUserDefaults standardUserDefaults] boolForKey:#"PakkelisteHasRun"];
if (!hasRunBefore)
{
NSLog(#"Has not run");
[self.toDoItems addObjectsFromArray:#[#"Badetøy", #"Skrivesaker", #"Lommepenger", #"Godt humør"]];
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:#"PakkelisteHasRun"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
else
{
NSLog(#"Already run");
}
}
-(IBAction)addNewToDoItem
{
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:#"Legg til ny" message:nil delegate:self cancelButtonTitle:#"Avbryt" otherButtonTitles:#"Legg til", nil];
[alertView setAlertViewStyle:UIAlertViewStylePlainTextInput];
[[alertView textFieldAtIndex:0] setPlaceholder:#"Rent undertøy"];
[[alertView textFieldAtIndex:0] setAutocapitalizationType:UITextAutocapitalizationTypeSentences];
[alertView show];
}
-(BOOL)alertViewShouldEnableFirstOtherButton:(UIAlertView *)alertView
{
NSString *inputText = [[alertView textFieldAtIndex:0] text];
if ([inputText length] > 0)
{
return YES;
}
else
{
return NO;
}
}
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex == 1)
{
NSManagedObjectContext *context = [self managedObjectContext];
// Create a new managed object
NSManagedObject *toDoItem = [NSEntityDescription insertNewObjectForEntityForName:#"AUFToDoItem" inManagedObjectContext:context];
[toDoItem setValue:[alertView textFieldAtIndex:0].text forKey:#"itemName"];
[toDoItem setValue:[NSDate date] forKey:#"creationDate"];
[toDoItem setValue:[NSNumber numberWithBool:YES] forKey:#"completed"];
[self.toDoItems addObject:toDoItem];
NSError *error = nil;
// Save the object to persistent store
if (![context save:&error]) {
NSLog(#"Can't Save! %# %#", error, [error localizedDescription]);
}
[self dismissViewControllerAnimated:YES completion:nil];
[self.tableView reloadData];
}
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return self.toDoItems.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"Cell";
TodoCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil)
{
cell = [[TodoCell alloc] init];
}
NSManagedObject *toDoItem = [self.toDoItems objectAtIndex:indexPath.row];
cell.title.text = [toDoItem valueForKey:#"itemName"];
if ([[toDoItem valueForKey:#"completed"] boolValue] == 1)
{
cell.checkbox.image = [UIImage imageNamed:#"checkbox_on.png"];
}
else
{
cell.checkbox.image = [UIImage imageNamed:#"checkbox_on.png"];
}
return cell;
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
/*
[tableView deselectRowAtIndexPath:indexPath animated:NO];
AUFToDoItem *tappedItem = [self.toDoItems objectAtIndex:indexPath.row];
tappedItem.completed = !tappedItem.completed;
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
*/
}
-(NSManagedObjectContext *)managedObjectContext {
NSManagedObjectContext *context = nil;
id delegate = [[UIApplication sharedApplication] delegate];
if ([delegate performSelector:#selector(managedObjectContext)]) {
context = [delegate managedObjectContext];
}
return context;
}
// Override to support conditional editing of the table view.
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
// Return NO if you do not want the specified item to be editable.
return YES;
}
// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
NSManagedObjectContext *context = [self managedObjectContext];
if (editingStyle == UITableViewCellEditingStyleDelete) {
[context deleteObject:[self.toDoItems objectAtIndex:indexPath.row]];
NSError *error = nil;
if (![context save:&error])
{
NSLog(#"Can't delete! %# %#", error, [error localizedDescription]);
return;
}
[self.toDoItems removeObjectAtIndex:indexPath.row];
[self.tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
/*
// Override to support rearranging the table view.
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
{
}
*/
/*
// Override to support conditional rearranging of the table view.
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
{
// Return NO if you do not want the item to be re-orderable.
return YES;
}
*/
/*
#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
#end
Change:
tappedItem.completed = !tappedItem.completed;
to:
tappedItem.completed = #(![tappedItem.completed boolValue]);
Which is a minimal code unpacking and re packing of the bool value in an NSNumber instance.
Also, in your if statement, don't check if a BOOL value == 1 because that isn't necessarily true. Just say:
if ([tappedItem.completed]) { ...
You also appear to have a typo in the names of the checkbox images you're using.
What if move the code to update the status of your cell in tableView:tableView didSelectRowAtIndexPath:? In addition, can you explain what you mean with But I am not sure how to update Core Data setting the boolean value of completed to yes?
After updating the status of your managed object you save the context you are using, then you can perfom a reload on the entire table or on a specific index path.
BOOL hasTap = ![tappedItem.completed boolValue];
tappedItem.completed = #(hasTap); // or [NSNumber numberWithBool:hasTap];
// save
// reload
I really follow Jonathan's comment. A NSFetchedResultsController is made to wotk with UITableViews (or UICollectionViews). You can set its delegate, NSFetchedResultsControllerDelegate, to listen and handle for inserting, deleting, moving rows (and section).
An tutorial would be Core Data Tutorial for iOS: How To Use NSFetchedResultsController.
So this is my first time working with core data, and so far it hasn't been the best experience. My application so far consists of two UITableView controllers and a single ViewController. The app simply asks the user to enter the name of a list on UIAlert and it saves to core data, and the name of the list is put into the first tableview. Then the user clicks on the name of the list and it pushes them to the contents on the list, which is empty because it hasn't been populated yet. Then the user populates the TableView, so it pushes to the single view controller where you would enter all the info and hit save. My problem is it doesn't save when I hit save, it goes back to the last UITableView Controller and nothing is there. Thats my first problem, my second is I would like to pass the data between views so when the user clicks on a list it pushes to the second UITableView Controller and says the name of the list at the top. I'm getting really confused with all the core data stuff and relationships so if someone could help me out I would appreciate it. I'll include my code and data model.
Data Model
First Views .m (view that lists all the lists)
- (NSManagedObjectContext *)managedObjectContext {
NSManagedObjectContext *context = nil;
id delegate = [[UIApplication sharedApplication] delegate];
if ([delegate respondsToSelector:#selector(managedObjectContext)]) {
context = [delegate managedObjectContext];
}
return context;
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// Fetch the lists from persistent data store
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"List"];
self.lists = [[managedObjectContext executeFetchRequest:fetchRequest error:nil] mutableCopy];
[self.tableView reloadData];
}
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return self.lists.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Configure the cell...
NSManagedObject *list = [self.lists objectAtIndex:indexPath.row];
[cell.textLabel setText:[list valueForKey:#"name"]];
return cell;
}
-(IBAction)add:(id)sender {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Add List" message:#"Create a New Wish List" delegate:self cancelButtonTitle:#"Cancel" otherButtonTitles:#"Save", nil];
[alert setAlertViewStyle:UIAlertViewStylePlainTextInput];
[alert setTag:2];
[alert show];
alert.delegate = self;
}
- (void) alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex != 0 && alertView.tag == 2) {
UITextField *tf = [alertView textFieldAtIndex:0];
NSManagedObjectContext *context = [self managedObjectContext];
// Create a new managed object
NSManagedObject *newList = [NSEntityDescription insertNewObjectForEntityForName:#"List" inManagedObjectContext:context];
[newList setValue:tf.text forKey:#"name"];
NSError *error = nil;
// Save the object to persistent store
if (![context save:&error]) {
NSLog(#"Can't Save! %# %#", error, [error localizedDescription]);
}
[self dismissViewControllerAnimated:YES completion:nil];
}
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"List"];
self.lists = [[managedObjectContext executeFetchRequest:fetchRequest error:nil] mutableCopy];
[self.tableView reloadData];
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
// Return NO if you do not want the specified item to be editable.
return YES;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
NSManagedObjectContext *context = [self managedObjectContext];
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete object from database
[context deleteObject:[self.lists objectAtIndex:indexPath.row]];
NSError *error = nil;
if (![context save:&error]) {
NSLog(#"Can't Delete! %# %#", error, [error localizedDescription]);
return;
}
// Remove list from table view
[self.lists removeObjectAtIndex:indexPath.row];
[self.items removeObjectAtIndex:indexPath.row];
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
#end
Second view's .m (view that displays all items in list)
- (NSManagedObjectContext *)managedObjectContext {
NSManagedObjectContext *context = nil;
id delegate = [[UIApplication sharedApplication] delegate];
if ([delegate respondsToSelector:#selector(managedObjectContext)]) {
context = [delegate managedObjectContext];
}
return context;
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// Fetch the lists from persistent data store
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Item"];
self.lists = [[managedObjectContext executeFetchRequest:fetchRequest error:nil] mutableCopy];
[self.tableView reloadData];
}
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return self.items.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Configure the cell...
NSManagedObject *item = [self.items objectAtIndex:indexPath.row];
[cell.textLabel setText:[item valueForKey:#"list"]];
return cell;
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
// Return NO if you do not want the specified item to be editable.
return YES;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
NSManagedObjectContext *context = [self managedObjectContext];
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete object from database
[context deleteObject:[self.items objectAtIndex:indexPath.row]];
NSError *error = nil;
if (![context save:&error]) {
NSLog(#"Can't Delete! %# %#", error, [error localizedDescription]);
return;
}
// Remove device from table view
[self.items removeObjectAtIndex:indexPath.row];
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
#pragma mark - Table view delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Navigation logic may go here. Create and push another view controller.
/*
<#DetailViewController#> *detailViewController = [[<#DetailViewController#> alloc] initWithNibName:#"<#Nib name#>" bundle:nil];
// ...
// Pass the selected object to the new view controller.
[self.navigationController pushViewController:detailViewController animated:YES];
*/
}
#pragma mark - Segue
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"ShowDetails"]) {
NSManagedObject *selectedItem = [self.items objectAtIndex:[[self.tableView indexPathForSelectedRow] row]];
AddViewController *destViewController = segue.destinationViewController;
destViewController.items = selectedItem;
}
}
#end
And last view (view that adds items to list)
- (NSManagedObjectContext *)managedObjectContext {
NSManagedObjectContext *context = nil;
id delegate = [[UIApplication sharedApplication] delegate];
if ([delegate respondsToSelector:#selector(managedObjectContext)]) {
context = [delegate managedObjectContext];
}
return context;
}
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction)cancel:(id)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (IBAction)save:(id)sender {
NSManagedObjectContext *context = [self managedObjectContext];
// Create a new managed object
NSManagedObject *newItem = [NSEntityDescription insertNewObjectForEntityForName:#"Item" inManagedObjectContext:context];
[newItem setValue:self.lists forKey:#"list"];
[newItem setValue:self.lists forKey:#"list"];
[newItem setValue:self.lists forKey:#"list"];
NSError *error = nil;
// Save the object to persistent store
if (![context save:&error]) {
NSLog(#"Can't Save! %# %#", error, [error localizedDescription]);
}
[self dismissViewControllerAnimated:YES completion:nil];
}
#end
In my model view that allows me to create a new item:
[newItem setValue:self.name.text forKey:#"itemName"];
[newItem setValue:self.price.text forKey:#"price"];
[newItem setValue:self.desc.text forKey:#"description"];
Error:
self=(AddViewController *)0x8d22e90
newItem=(Item_Item_ *)0x8a636b0
It's not completely clear to me how all of these controllers are wired together. A handful of suggestions to consider when working with Core Data:
There are a variety of ways to deal with the managed object context that you'll use on the main queue. Many use dependency injection to pass the context along the controller hierarchy, rather than ask the application delegate for it in each controller.
You can simplify your life considerably by subclassing NSManagedObject so that you don't have to use KVC to access properties on the entities. Xcode can do it for you; but mogenerator offers much more.
NSFetchedResultsController can sometimes help simplify working with Core Data and table views; but it also adds its own set of quirks.
That said, I can't tell whether any of this is related to your problems or would necessarily help; so I put together a sample app that roughly mirrors part of what you're trying to do. (I think...) It leaves out a lot of the logic for collecting data from your user etc. but you can look at how you might consider putting together a Core Data app such as this.
See this repository on github.
So this is my first time working with core data, and so far it hasn't been the best experience. My application so far consists of two UITableView controllers and a single ViewController. The app simply asks the user to enter the name of a list on UIAlert and it saves to core data, and the name of the list is put into the first tableview. So far so good. Then the user clicks on the name of the list and it pushes them to the contents on the list, which is empty because it hasn't been populated yet. So my problem is when I go to populate it my app just crashes. I don't even get to the the ViewController. I'm really lost I'll put the necessary code here if there's any more let me know. Thanks!
Error argv char ** 0xbfffeda8 0xbfffeda8
Data Model:
The .m To Add Items to the list:
- (NSManagedObjectContext *)managedObjectContext {
NSManagedObjectContext *context = nil;
id delegate = [[UIApplication sharedApplication] delegate];
if ([delegate performSelector:#selector(managedObjectContext)]) {
context = [delegate managedObjectContext];
}
return context;
}
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction)cancel:(id)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (IBAction)save:(id)sender {
NSManagedObjectContext *context = [self managedObjectContext];
// Create a new managed object
NSManagedObject *newItem = [NSEntityDescription insertNewObjectForEntityForName:#"Item" inManagedObjectContext:context];
[newItem setValue:self.name.text forKey:#"itemName"];
[newItem setValue:self.price.text forKey:#"price"];
[newItem setValue:self.desc.text forKey:#"description"];
NSError *error = nil;
// Save the object to persistent store
if (![context save:&error]) {
NSLog(#"Can't Save! %# %#", error, [error localizedDescription]);
}
[self dismissViewControllerAnimated:YES completion:nil];
}
#end
The .m to show all the items in a list:
- (NSManagedObjectContext *)managedObjectContext {
NSManagedObjectContext *context = nil;
id delegate = [[UIApplication sharedApplication] delegate];
if ([delegate performSelector:#selector(managedObjectContext)]) {
context = [delegate managedObjectContext];
}
return context;
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// Fetch the lists from persistent data store
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Item"];
self.lists = [[managedObjectContext executeFetchRequest:fetchRequest error:nil] mutableCopy];
self.items = [[managedObjectContext executeFetchRequest:fetchRequest error:nil] mutableCopy];
[self.tableView reloadData];
}
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return self.items.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Configure the cell...
NSManagedObject *item = [self.items objectAtIndex:indexPath.row];
[cell.textLabel setText:[item valueForKey:#"itemName"]];
return cell;
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
// Return NO if you do not want the specified item to be editable.
return YES;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
NSManagedObjectContext *context = [self managedObjectContext];
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete object from database
[context deleteObject:[self.items objectAtIndex:indexPath.row]];
NSError *error = nil;
if (![context save:&error]) {
NSLog(#"Can't Delete! %# %#", error, [error localizedDescription]);
return;
}
// Remove device from table view
[self.items removeObjectAtIndex:indexPath.row];
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
#pragma mark - Segue
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"ShowDetails"]) {
NSManagedObject *selectedItem = [self.items objectAtIndex:[[self.tableView indexPathForSelectedRow] row]];
AddViewController *destViewController = segue.destinationViewController;
destViewController.items = selectedItem;
}
}
#end
And the .m displaying all the lists:
- (NSManagedObjectContext *)managedObjectContext {
NSManagedObjectContext *context = nil;
id delegate = [[UIApplication sharedApplication] delegate];
if ([delegate performSelector:#selector(managedObjectContext)]) {
context = [delegate managedObjectContext];
}
return context;
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// Fetch the lists from persistent data store
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"List"];
self.lists = [[managedObjectContext executeFetchRequest:fetchRequest error:nil] mutableCopy];
self.items = [[managedObjectContext executeFetchRequest:fetchRequest error:nil] mutableCopy];
[self.tableView reloadData];
}
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return self.lists.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Configure the cell...
NSManagedObject *list = [self.lists objectAtIndex:indexPath.row];
[cell.textLabel setText:[list valueForKey:#"name"]];
return cell;
}
-(IBAction)add:(id)sender {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Add List" message:#"Create a New Wish List" delegate:self cancelButtonTitle:#"Cancel" otherButtonTitles:#"Save", nil];
[alert setAlertViewStyle:UIAlertViewStylePlainTextInput];
[alert setTag:2];
[alert show];
alert.delegate = self;
}
- (void) alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex != 0 && alertView.tag == 2) {
UITextField *tf = [alertView textFieldAtIndex:0];
NSManagedObjectContext *context = [self managedObjectContext];
// Create a new managed object
NSManagedObject *newList = [NSEntityDescription insertNewObjectForEntityForName:#"List" inManagedObjectContext:context];
[newList setValue:tf.text forKey:#"name"];
NSError *error = nil;
// Save the object to persistent store
if (![context save:&error]) {
NSLog(#"Can't Save! %# %#", error, [error localizedDescription]);
}
[self dismissViewControllerAnimated:YES completion:nil];
}
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"List"];
self.lists = [[managedObjectContext executeFetchRequest:fetchRequest error:nil] mutableCopy];
self.items = [[managedObjectContext executeFetchRequest:fetchRequest error:nil] mutableCopy];
[self.tableView reloadData];
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
// Return NO if you do not want the specified item to be editable.
return YES;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
NSManagedObjectContext *context = [self managedObjectContext];
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete object from database
[context deleteObject:[self.lists objectAtIndex:indexPath.row]];
NSError *error = nil;
if (![context save:&error]) {
NSLog(#"Can't Delete! %# %#", error, [error localizedDescription]);
return;
}
// Remove list from table view
[self.lists removeObjectAtIndex:indexPath.row];
[self.items removeObjectAtIndex:indexPath.row];
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
#end
You should consider using an NSFetchedResultsController to make your life easier, and your app more efficient.
This is incorrect:
self.lists = [[managedObjectContext executeFetchRequest:fetchRequest error:nil] mutableCopy];
self.items = [[managedObjectContext executeFetchRequest:fetchRequest error:nil] mutableCopy];
because these sets should contain different types of entity instance. But, you don't actually need self.items. The model contains a relationship between List and Item and you should just get the items for a specified list using that relationship (or a fetch request using the relationship) when you need it. And remove self.items, it's just confusing you. In the list view controller you don't need the items anyway.
When you push to show the items, you should be getting the selected List and passing that. So where you have destViewController.items = selectedItem; you should have something like destViewController.list = selectedList;, and then the item controller uses the relationship again to display the appropriate items.
And, when you add a new item, you have to associated it with self.list:
[newItem setValue:self.list forKey:#"list"];
You should also use managed object subclasses, and the best way to manage them is by using mogenerator.
Aside: Where you have if ([delegate performSelector: it should be if ([delegate respondsToSelector:, and probably log if you don't get a context back...
Update 18/3 #2. I've started counting beginUpdates and EndUpdates to make sure they're even. Right before there's an exception, they get out of sync. Not sure why though.
UPDATE 18/3: I think I've found the problem, but I'm not sure if I know how to fix it. After experimenting for a couple hours, I found I could only crash the app when I had selected more than one item in the master tableview of the svc during that session.
When another item is selected in the master tableview, the detail table view gets a new object set and refreshtables is called EVEN IF it's halfway through an update/move/delete/insert cycle. Hence the problem; I think it has instructions to update the old story object even though a new story object has been set on the detailviewcontroller.
How can I get the animation/coredata/tableview updates to be processed completely before a new story is set in the detail view? I save changes to the managedObjectContext when setEditting == NO. I'm guessing I need to make a custom setStory setter that processes all the updates to the UITableView/CoreData set before accepting the new object?
This code gets called on the master tableview controller of the svc in didSelectRowAtIndexPath:
[detailViewController setStory:storySet]; //where storySet is the new story object
[detailViewController refreshTables];
I have intermittent errors on attempting to delete the row where action won't animate and the application essentially hangs with the following error (the row is deleted from the CoreData set though). This happens if I have chosen more than one row from the master tableview controller in the svc in one session.
After some googling I thought it might be a problem with (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id) which is called after an update to animate the changes the user has made.
How can I fix these intermittent bugs?
16/3I've tried to really simplify my code. I've removed all calls to the Managed Object Context and put them in setEditing, removed superfluous [self.tableview reloadData] and [self.tableview setneedsdisplay] and invalidated the 'reordering' bool completely (it's still in the code, but it's always set to NO, so it makes no difference). The UITableView is more stable then ever, but I still manage to get intermittent errors on delete (and occasionally on add) - it seems to take a while to crash but it still will.**
15/3: I think that it has something to do with the CoreData / UITableView being out of sync - CoreData thinks it has less or more than the UITableView
There's no problem with the CoreData set, it's just the animation/UI side of things (things get removed)
It's intermittent, only on some deletes
After some help from railwayparade I implemented NSFetchedResultsChangeMove in didChangeObject: which fixed the moving error but not the delete errors.
Can anyone see anything I've missed or that I can check? Happy to give more information if that helps solve the problem.
Apologies for the obscene amount of code posted here.
The Error:
CoreData: error: Serious application error. An exception was caught
from the delegate of NSFetchedResultsController during a call to
-controllerDidChangeContent:. attempt to insert row 3 into section 0, but there are only 3 rows in section 0 after the update with userInfo
(null)
//
// MakeSentenceTableViewController.h
// StoryBot
//
// Created by Glen Storey on 25/10/10.
// Copyright 2010 Glen Storey. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "AddStoryItem.h"
#import "Story.h"
#import "Sentence.h"
#interface MakeSentenceTableViewController : UITableViewController <NSFetchedResultsControllerDelegate, AddStoryItemDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate, UITextFieldDelegate, UIPopoverControllerDelegate> {
NSManagedObjectContext *managedObjectContext;
NSFetchedResultsController *fetchedResultsController;
UIPopoverController *popoverController;
UIBarButtonItem *playButtonItem;
UIBarButtonItem *addButtonItem;
BOOL reordering;
BOOL insert;
BOOL delete;
BOOL move;
int beginUpdatesCount;
int endUpdatesCount;
}
#property (nonatomic, retain) Story *story;
#property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;
#property (nonatomic, retain) UIBarButtonItem *playButtonItem;
#property (nonatomic, retain) UIBarButtonItem *addButtonItem;
#property BOOL reordering, insert, delete, move;
-(IBAction)createStoryModal:(id)sender;
-(void)refreshTables;
-(IBAction)pushShare:(id)sender;
#end
//
// MakeSentenceTableViewController.m
//
//
// Created by Glen Storey on 25/10/10.
// Copyright 2010 Glen Storey. All rights reserved.
//
#import "MakeSentenceTableViewController.h"
#import "ShareViewController.h"
#import "StoryBotAppDelegate.h"
#implementation MakeSentenceTableViewController
#synthesize story, managedObjectContext, addButtonItem, playButtonItem, reordering, insert, delete, move;
-(void)addStoryItemAction:(NSString*)text order:(NSNumber*)order image:(NSString*)image thumb:(NSString*)thumb{
NSLog(#"Text: %#, Order: %#, Image: %#.", text, order, image);
NSLog(#"beginUpdatesCount: %d vs. endUpdatescount: %d", beginUpdatesCount, endUpdatesCount);
NSSet *sentences = [story sentences];
NSNumber *maxOrder = [sentences valueForKeyPath:#"#max.order"];
NSLog(#"maxOrder: %#", maxOrder);
if(maxOrder == 0){
maxOrder = [[NSNumber alloc] initWithInteger: 0];
}
//make a new sentence!
Sentence *sentence = [NSEntityDescription insertNewObjectForEntityForName:#"Sentence"
inManagedObjectContext:managedObjectContext];
[sentence setText: text];
[sentence setImage: image];
[sentence setThumb: thumb];
[sentence setBelongsTo: story];
if([maxOrder intValue] >= 1 ){
[sentence setOrder: [[NSNumber alloc] initWithInteger:[maxOrder intValue]+1]];
}else{
[sentence setOrder: [[NSNumber alloc] initWithInteger:1]];
}
NSMutableSet *mutableSet = [[NSMutableSet alloc] initWithSet:sentences];
[mutableSet addObject:sentence];
//NSLog(#"sentences before setWithSet %#", mutableSet);
sentences = [[NSSet alloc] initWithSet: mutableSet];
//NSLog(#"sentences after setWithSet %#", sentences);
[story setSentences:sentences];
//NSError *error;
//BOOL isSaved = [managedObjectContext save:&error];
//NSLog(#"isSaved? %#", (isSaved ? #"YES" :#"NO ") );
//if (!isSaved) {
//NSLog(#"%#:%s Error saving context: %#", [self class], _cmd, [error localizedDescription]);
//Don't worry about this warning - just rem it out when finished (just a log)
// return;
//}
[sentences release];
[mutableSet release];
//[self.tableView reloadData];
//[self.tableView setNeedsDisplay];
[self dismissModalViewControllerAnimated:YES];
}
#pragma mark -
#pragma mark View lifecycle
-(id)initWithNibName:(NSString*)name bundle:(NSBundle*)bundle;
{
self = [super initWithNibName:name bundle:bundle];
if (self) {
self.title = #"My Stories";
addButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
target:self
action:#selector(createStoryModal:)];
playButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPlay
target:self
action:#selector(pushShare:)];
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
[addButtonItem setEnabled:NO];
[playButtonItem setEnabled:NO];
}
NSArray* toolbarItems = [NSArray arrayWithObjects:
addButtonItem,
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
target:nil
action:nil],
playButtonItem,
nil];
[toolbarItems makeObjectsPerformSelector:#selector(release)];
self.toolbarItems = toolbarItems;
//NSLog(#"self: %# self.toolbarItems: %#", self, self.toolbarItems);
//Do I need to release UIBarButtonItems?
NSLog(#"initWithNibName:");
}
return self;
}
-(void)refreshTables{
//use this to refresh for new table content. Also calls self.tableview reloadData so you don't need to call it afterward.
NSLog(#"===================================refreshTables");
NSLog(#"story object %#", story);
if (managedObjectContext == nil)
{
NSLog(#"managedObjectContext == nil");
managedObjectContext = [(StoryBotAppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
NSLog(#"After managedObjectContext: %#", managedObjectContext);
}else{
NSLog(#"managedObjectContext != nil");
}
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Sentence" inManagedObjectContext:managedObjectContext];
[request setEntity:entity];
//sorting stuff:
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"order" ascending: YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects: sortDescriptor, nil];
[request setSortDescriptors:sortDescriptors];
[sortDescriptors release];
[sortDescriptor release];
NSPredicate *predicateTitle = [NSPredicate predicateWithFormat:#"belongsTo=%#",story];
[request setPredicate :predicateTitle];
NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
[dateFormatter setDateFormat:#"EEE, d MMM yyyy HH:mm:ss"];
NSString *dateString = [dateFormatter stringFromDate:[story creationDate]];
NSLog(#"dateString: %#", dateString);
fetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:request managedObjectContext:managedObjectContext
sectionNameKeyPath:nil cacheName:dateString];
fetchedResultsController.delegate = self;
[request release];
NSError *error;
[fetchedResultsController performFetch:&error];
[self.tableView reloadData];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.title = #"My Story";
NSLog(#"Passed Story Object: %#", story);
//NSLog(#"managedObjectContext: %#", managedObjectContext);
//Need a predicate for belongsTo here.
self.tableView.rowHeight = 50;
if(story != NULL){
if (managedObjectContext == nil)
{
NSLog(#"managedObjectContext == nil");
managedObjectContext = [(StoryBotAppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
NSLog(#"After managedObjectContext: %#", managedObjectContext);
}else{
NSLog(#"managedObjectContext != nil");
}
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Sentence" inManagedObjectContext:managedObjectContext];
[request setEntity:entity];
//sorting stuff:
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"order" ascending: YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects: sortDescriptor, nil];
[request setSortDescriptors:sortDescriptors];
[sortDescriptors release];
[sortDescriptor release];
NSPredicate *predicateTitle = [NSPredicate predicateWithFormat:#"belongsTo=%#",story];
[request setPredicate :predicateTitle];
fetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:request managedObjectContext:managedObjectContext
sectionNameKeyPath:nil cacheName:nil];
fetchedResultsController.delegate = self;
[request release];
NSError *error;
[fetchedResultsController performFetch:&error];
}
#pragma mark -
#pragma mark Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
//Return the number of rows in the section.
NSArray *sections = [fetchedResultsController sections];
NSInteger count = 0;
if ([sections count]){
id <NSFetchedResultsSectionInfo> sectionInfo = [sections objectAtIndex:section];
count = [sectionInfo numberOfObjects];
}
NSLog(#"# of rows in section %d", count);
return count;
}
// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
// Configure the cell...
Sentence *sentenceAtCell = [fetchedResultsController objectAtIndexPath:indexPath];
//NSLog(#"sentenceAtCell: %#", sentenceAtCell);
cell.textLabel.text = [sentenceAtCell text];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *uniquePath = [[paths objectAtIndex:0] stringByAppendingPathComponent:[sentenceAtCell thumb]];
// This should crop it as you want - you've just got to create cropRect.
UIImage *largeImage = [UIImage imageWithContentsOfFile: uniquePath];
CGRect cropRect = CGRectMake(0, 0, 66, 50);
/*if ([[UIScreen mainScreen] respondsToSelector:#selector(scale)]) {
if ([[UIScreen mainScreen] scale] == 2) {
// running an iPhone 4 or equiv. res device.
cropRect = CGRectMake(15, 14, 100, 75);
}
}*/
CGImageRef imageRef = CGImageCreateWithImageInRect([largeImage CGImage], cropRect);
cell.imageView.image = [UIImage imageWithCGImage: imageRef];
CGImageRelease(imageRef);
//NSLog(#"indexPath: %#", indexPath);
return cell;
}
// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSLog(#"Delete row");
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectoryPath = [paths objectAtIndex:0];
// 1. Look at the sentence we're about to delete.
Sentence *sentenceRetire = [fetchedResultsController objectAtIndexPath:indexPath];
// 2. Does it have an order of 0?
NSLog(#"=================== sentenceRetire: %#", sentenceRetire);
if([[sentenceRetire order] intValue] == 0){
// YES: Is there another sentence in this story?
Story *storyRetire = [sentenceRetire belongsTo];
NSSet *sentencesInRetiredSentenceSet = [storyRetire sentences];
if ([sentencesInRetiredSentenceSet count] > 1){
// YES: Set the sentence with the smallest order to an order of 0
// then delete the sentence + files
NSPredicate *predicateTitle = [NSPredicate predicateWithFormat:#"order>0"];
NSSet *sentencesWithPotentialToBeTitle = [sentencesInRetiredSentenceSet filteredSetUsingPredicate:predicateTitle];
NSNumber *minOrder = [sentencesWithPotentialToBeTitle valueForKeyPath:#"#min.order"];
NSPredicate *predicateOrder = [NSPredicate predicateWithFormat:#"order=%d",[minOrder intValue]];
NSSet *sentenceWithPotentialToBeTitle = [sentencesWithPotentialToBeTitle filteredSetUsingPredicate:predicateOrder];
//note the sentence (singular) not sentences. This is the sentence who's order needs to be updated.
NSLog(#"setenceWithPotentialToBeTitle (check order isn't null on crash): %#", sentenceWithPotentialToBeTitle);
Sentence *sentenceToPromote = [sentenceWithPotentialToBeTitle anyObject];
//now we know which sentence to promote we can delete the sentence & Files.
[managedObjectContext deleteObject:[fetchedResultsController objectAtIndexPath:indexPath]];
NSString *imageTrash = [documentsDirectoryPath stringByAppendingPathComponent:(NSString*)[sentenceRetire image]];
NSString *thumbTrash = [documentsDirectoryPath stringByAppendingPathComponent:[sentenceRetire thumb]];
NSLog(#"About to delete these files: %#, %#", imageTrash, thumbTrash);
[fileManager removeItemAtPath:imageTrash error:NULL];
[fileManager removeItemAtPath:thumbTrash error:NULL];
//and promote the new title.
[sentenceToPromote setOrder:[[NSNumber alloc] initWithInteger:0]];
}else{
// NO: We're deleting this story
// Delete the files!
[managedObjectContext deleteObject:storyRetire];
NSString *imageTrash = [documentsDirectoryPath stringByAppendingPathComponent:(NSString*)[sentenceRetire image]];
NSString *thumbTrash = [documentsDirectoryPath stringByAppendingPathComponent:[sentenceRetire thumb]];
//NSLog(#"About to delete these files: %#, %#", imageTrash, thumbTrash);
[fileManager removeItemAtPath:imageTrash error:NULL];
[fileManager removeItemAtPath:thumbTrash error:NULL];
//Pop back.
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
NSLog(#"last sentence in story - delete that story and point, somewhere!");
//probably delete the sentece to clear the table, then delete the story using storyRetire
} else{
[self.navigationController popViewControllerAnimated:YES];
}
}
}else{
// NO: Delete the Sentence Object.
[managedObjectContext deleteObject:[fetchedResultsController objectAtIndexPath:indexPath]];
NSString *imageTrash = [documentsDirectoryPath stringByAppendingPathComponent:(NSString*)[sentenceRetire image]];
NSString *thumbTrash = [documentsDirectoryPath stringByAppendingPathComponent:[sentenceRetire thumb]];
//NSLog(#"About to delete these files: %#, %#", imageTrash, thumbTrash);
[fileManager removeItemAtPath:imageTrash error:NULL];
[fileManager removeItemAtPath:thumbTrash error:NULL];
}
// Save the context.
//NSError *error;
//if (![managedObjectContext save:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
// NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
// abort();
//}
}
}
- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
[super setEditing:editing animated:animated];
if (!editing) {
//save here
NSError *error;
BOOL isSaved = [managedObjectContext save:&error];
NSLog(#"isSaved? %# ======================================", (isSaved ? #"YES" :#"NO ") );
}
}
// Override to support rearranging the table view.
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
//this implementation is from here: http://www.cimgf.com/2010/06/05/re-ordering-nsfetchedresultscontroller/
NSMutableArray *things = [[fetchedResultsController fetchedObjects] mutableCopy];
// Grab the item we're moving.
NSManagedObject *thing = [fetchedResultsController objectAtIndexPath:fromIndexPath];
// Remove the object we're moving from the array.
[things removeObject:thing];
// Now re-insert it at the destination.
[things insertObject:thing atIndex:[toIndexPath row]];
// All of the objects are now in their correct order. Update each
// object's displayOrder field by iterating through the array.
int i = 0;
for (NSManagedObject *mo in things)
{
[mo setValue:[NSNumber numberWithInt:i++] forKey:#"order"];
}
NSLog(#"things: %#", things);
[things release], things = nil;
//reordering = YES;
//NSLog(#"moveRowAtIndexPath: IS reordering");
//NSError *error;
//[managedObjectContext save:&error];
}
#pragma mark -
#pragma mark Table view delegate
/**
Delegate methods of NSFetchedResultsController to respond to additions, removals and so on.
*/
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
NSLog(#"controllerWillChangeContent: BeginUpdates");
[self.tableView beginUpdates];
beginUpdatesCount++;
NSLog(#"====================beginUpdates was just incremented");
NSLog(#"beginUpdatesCount %d", beginUpdatesCount);
NSLog(#"endUpdatesCount %d", endUpdatesCount);
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
NSLog(#"didChangeObject: %#", anObject);
UITableView *tableView;
tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
NSLog(#"ResultsChangeInsert:");
insert = YES;
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
NSLog(#"ResultsChangeDelete:");
delete = YES;
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
NSLog(#"ResultsChangeMove:");
move = YES;
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationNone];
break;
default:
NSLog(#"switch problem - default");
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
NSLog(#"didChangeSection:");
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
NSLog(#"didChangeContent");
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
NSLog(#"almost endUpdates==============================================");
if(delete){
NSLog(#"endUpdates delete");
delete = NO;
}
if(move){
NSLog(#"endUpdates move");
move = NO;
}
if(insert){
NSLog(#"endUpdates insert");
insert = NO;
}
[self.tableView endUpdates];
endUpdatesCount++;
NSLog(#"====================endUpdates was just incremented");
NSLog(#"endUpdatesCount %d", endUpdatesCount);
NSLog(#"beginUpdatesCount %d", beginUpdatesCount);
NSLog(#"endUpdates finished");
}
#pragma mark -
#pragma mark Memory management
- (void)dealloc {
NSLog(#"Dealloc Sentence");
//[fliteEngine stopTalking];
//[fliteEngine release];
addButtonItem = nil;
playButtonItem = nil;
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
//It doesn't seem to get allocated because it's not set on the iPhone.
[addButtonItem release];
[playButtonItem release];
}
[story release];
[fetchedResultsController release];
[super dealloc];
}
#end
Update 18/3 #3 I read this question about a fetchedResultsController needing to have its delegate set to nil before creating a new one. I put
detailViewController.fetchedResultsController = nil;
detailViewController.fetchedResultsController.delegate = nil;
in the SVC master view on didselectrowatindexpath and the problem has stopped. RefreshTables created a new fetchedResultsController every time a row was selected in the master view, and I think they were still interfering even when a new story object was selected.
You missed implementing NSFetchedResultsChangeMove in didChangeObject:
Here is my code which does the same thing:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
sectionHeadersChanged=NO;
[self._tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch(type) {
case NSFetchedResultsChangeInsert:
[self._tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[self._tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
sectionHeadersChanged=YES;
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self._tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationBottom];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self._tableView endUpdates];
//reload all sections
if(sectionHeadersChanged==YES){
[self._tableView reloadSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self._tableView.numberOfSections)] withRowAnimation:UITableViewRowAnimationNone];
}
}