Search code examples
objective-cuitableviewnspredicatensfetchedresultscontrolleruistoryboardsegue

How to prepareForSegue with NSFetchedResults drilldown to another tableview with objective c


I know there is probably a very simple solution to this. I'm using the apple iphoneCoreDataRecipes example. I'm trying to drill down from an initial table(RecipeTypeTableViewController) of RecipeTypes (ie Beverage, Dessert, Entree) to the next tableview with listing of those type of Recipes(RecipeListTableViewController). I created a new class RecipeType, thinking I could just use this to return recipes of a selected RecipeType. So far the best I can get is allowing selection but it is showing the same list of all recipes. The original code starts with RecipeListTableViewController using NSFetchedResults to show all recipes. Now I have the initial view of RecipeTypes but I'm not sure how to pass the selected type to RecipeListTableViewController. I need to change RecipeListTableViewController to show list of the selected RecipeType but I'm not sure how to implement this. I think I basically want to pass an NSMutableArray of fetchedresults to another tableview but not sure which direction to take. And I don't want to mess up other working actions in the RecipeListTableViewController, ie search and editing of recipes. Here's images of table action so far [RecipeTypeTableView][1] & [RecipeListTableview][2]. Thank you in advance for any suggestions.

Deleted these

File: RecipeType.h

File: RecipeType.m

I use the NSManagedObject *type from my Recipe Class NSManagedObject to create initial view of recipe types(Appetizer, Bev, etc.) on the RecipeTypeTableViewController -

File: Recipe.h

@interface Recipe : NSManagedObject
@property (nonatomic, strong) NSManagedObject *type;

File: RecipeTypeTableViewController.h

//  RecipeTypeTableViewController.h
//  Recipes
#import <UIKit/UIKit.h>
@interface RecipeTypeTableViewController : UITableViewController <NSFetchedResultsControllerDelegate>
@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, strong) NSManagedObject *type;
@end

File: RecipeTypeTableViewController.m - the prepare for segue part

#import "RecipeTypeTableViewController.h"
#import "Recipe.h"
#import "RecipeListTableViewController.h"
@interface RecipeTypeTableViewController () <NSFetchedResultsControllerDelegate>
@property (nonatomic, strong) NSFetchedResultsController *fetchedResultsController;
@end

@implementation RecipeTypeTableViewController

static NSString *MyIdentifier = @"showRecipes";

- (void)viewDidLoad {
[super viewDidLoad];
// register this class for our cell to this table view under the specified identifier 'ARecipeType'
self.title = @"Category";
//[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"ARecipeType"];

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didChangePreferredContentSize:)name:UIContentSizeCategoryDidChangeNotification object:nil];
// 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;
NSError *error = nil;
if (![[self fetchedResultsController] performFetch:&error]) {

    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    //abort();
}
}

- (void)viewWillAppear:(BOOL)animated {

[super viewWillAppear:animated];
self.title = @"Category";

}

- (void)didChangePreferredContentSize:(NSNotification *)notification
{
[self.tableView reloadData];
}

- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}

- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIContentSizeCategoryDidChangeNotification object:nil];
}
#pragma mark - Table view data source

//- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
//#warning Incomplete implementation, return the number of sections
//return [[self.recipeTypeFetchedResultsController sections] count];

//    return 1;
//}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// Number of rows is the number of recipe types
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
//return self.recipeTypes.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ARecipeType" forIndexPath:indexPath];

// Configure the cell
NSManagedObject *recipeType = [self.recipeTypes objectAtIndex:indexPath.row];
cell.textLabel.text = [recipeType valueForKey:@"name"]; 
return cell;
}

#pragma mark - Fetched results controller

- (NSFetchedResultsController *)fetchedResultsController {

// Set up the fetched results controller if needed.
if (_fetchedResultsController == nil) {
    // Create the fetch request for the entity.
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    // Edit the entity name as appropriate.
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"RecipeType" inManagedObjectContext:self.managedObjectContext];
    [fetchRequest setEntity:entity];
    NSLog(@"Setting up a Fetched Results Controller for the Entity named %@", entity);

    // Edit the sort key as appropriate.
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];

    [fetchRequest setSortDescriptors:sortDescriptors];

    // Edit the section name key path and cache name if appropriate.
    // nil for section name key path means "no sections".
    NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:@"Recipe"];
    aFetchedResultsController.delegate = self;
    self.fetchedResultsController = aFetchedResultsController;


    NSError *error = nil;
    if (![self.fetchedResultsController performFetch:&error])
    {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    }
}

return _fetchedResultsController;
}

#pragma mark - Navigation

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([[segue identifier] isEqualToString:@"showRecipes"]) {
    NSLog(@"Setting RecipeType for the RecipeListTableViewController");
    NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
    Recipe *selectedType = [self.fetchedResultsController objectAtIndexPath:indexPath];
    RecipeListTableViewController *recipeListViewController = segue.destinationViewController;
    recipeListViewController.type = selectedType;
    recipeListViewController.managedObjectContext = self.managedObjectContext;
}
}

File: RecipeListTableViewController.h - the NSPredicate part and the FRC cache

#import <UIKit/UIKit.h>
#import "RecipeAddViewController.h"

@interface RecipeListTableViewController : UITableViewController <RecipeAddDelegate, NSFetchedResultsControllerDelegate>

@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, strong) NSManagedObject *type;

File: RecipeListTableViewController.m

#import "RecipeListTableViewController.h"
#import "RecipeDetailViewController.h"
#import "Recipe.h"
#import "RecipeTableViewCell.h"
#import "Recipe+Extensions.h"
#import "TypeSelectionViewController.h"
#import "IngredientDetailViewController.h"
#import "Ingredient.h"
#import "WhereViewController.h"
#import "FavoriteListTableViewController.h"
#import "RecipeTypeTableViewController.h"
#import "RecipeType.h"


@interface RecipeListTableViewController () <NSFetchedResultsControllerDelegate, UISearchBarDelegate, UISearchResultsUpdating>

@property (nonatomic, strong) NSFetchedResultsController *fetchedResultsController;
@property (nonatomic, strong) NSArray *filteredList;
@property (nonatomic, strong) NSFetchRequest *searchFetchRequest;
@property (nonatomic, strong) UISearchController *searchController;

typedef NS_ENUM(NSInteger, RecipesSearchScope)
{
searchScopeRecipe = 0,
searchScopeIngredients = 1
};


@end

@implementation RecipeListTableViewController


#pragma mark - Fetched results controller

- (NSFetchedResultsController *)fetchedResultsController {

// Set up the fetched results controller if needed.
if (_fetchedResultsController == nil) {
    // Create the fetch request for the entity.
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    // Edit the entity name as appropriate.
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Recipe" inManagedObjectContext:self.managedObjectContext];
    [fetchRequest setEntity:entity];

[NSFetchedResultsController deleteCacheWithName:@"Recipe"];

    // Create predicate
    //if (self.type) {
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"type == %@", self.type];
    [fetchRequest setPredicate:predicate];
    //}


    // Edit the sort key as appropriate.
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];

    [fetchRequest setSortDescriptors:sortDescriptors];

    // Edit the section name key path and cache name if appropriate.
    // nil for section name key path means "no sections".
    NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:@"Recipe"];
    aFetchedResultsController.delegate = self;
    self.fetchedResultsController = aFetchedResultsController;

    NSError *error = nil;
    if (![self.fetchedResultsController performFetch:&error])
    {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    }
}

return _fetchedResultsController;
}

/

When I have the FRC cache set to "Recipe". It crashes. It showed this clue to look at cache CoreData: FATAL ERROR: The persistent cache of section information does not match the current configuration. You have illegally mutated the NSFetchedResultsController's fetch request, its predicate, or its sort descri...

If I set the cache to nil, or add [NSFetchedResultsController deleteCacheWithName:@"Recipe"]; before the predicate set, things work as expected. I have this cache in the inital view controller and the second view controller. Maybe that is the problem - I only need cache in the initial view controller?


Solution

  • First, I think your data model needs a little refinement. Each RecipeType can presumably have many associated Recipes. So the relationship from RecipeType to Recipe should be to-many. If you change this in the data model editor, and regenerate the model classes, you should then have a RecipeType class that looks something like this:

    @class Recipe;
    
    @interface RecipeType : NSManagedObject
    // not sure what purpose this property serves; it might now be superfluous...
    @property (nonatomic, strong) NSManagedObject *type;
    
    @property (nonatomic, strong) NSString *name;
    @property (nonatomic, strong) NSSet<Recipe *> *recipes;
    
    @end 
    

    I'll assume for the time being that each Recipe can belong to only one RecipeType. So the inverse relationship, from Recipe to RecipeType should be to-one, and the Recipe class will therefore have a property:

    *property (nonatomic, strong) RecipeType *type;
    

    Next, you want your RecipeListTableViewController to display only the Recipes that are related to the relevant RecipeType. To achieve that, you need to add a predicate to the fetched results controller. In the fetchedResultsController method, add:

    if (self.recipeType) {
        fetchRequest.predicate = [NSPredicate predicateWithFormat:@"type == %@", self.recipeType];
    }
    

    (You will also need to amend your search to likewise limit the search to the relevant RecipeType). In the RecipeTypeTableViewContoller, your prepareForSegue needs only to pass the correct RecipeType:

    - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {    
        if ([[segue identifier] isEqualToString:@"showRecipes"]) {
            NSLog(@"Setting RecipeType for the RecipeListTableViewController");
            NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
            RecipeType *selectedType = [self.fetchedResultsController objectAtIndexPath:indexPath];
            RecipeListTableViewController *recipeListViewController = segue.destinationViewController;
            recipeListViewController.recipeType = selectedType;
            recipeListViewController.managedObjectContext = self.managedObjectContext;
        }
    }
    

    Whenever you add a new Recipe, you will need to assign it to the correct RecipeType. So in the RecipeListViewController amend your prepareForSegue to set the relationship:

    ...
    else if ([segue.identifier isEqualToString:kAddRecipeSegueID]) {
        // add a recipe
        //
        Recipe *newRecipe = [NSEntityDescription insertNewObjectForEntityForName:@"Recipe" inManagedObjectContext:self.managedObjectContext];
        newRecipe.type = self.recipeType;
    
        UINavigationController *navController = segue.destinationViewController;
        RecipeAddViewController *addController = (RecipeAddViewController *)navController.topViewController;
        addController.delegate = self;  // do didAddRecipe delegate method is called when cancel or save are tapped
        addController.recipe = newRecipe;
    }