Search code examples
iosentity-frameworkcore-datanspredicatensfetchrequest

iOS Core Data Predicate for filtering based on related data


Alright, I'm a predicate noob. They are just alien to me.

About the app:

I have an app that handles gaming matches. There are entities for players, check-ins, and matches. The idea is that players are added to the app, then can be checked in for playing, and the match results are stored.

Relationships:

Players <->> CheckIns (each player can have multiple check-ins on different dates)

  • From: Player entity
  • Relationship: playerCheckins
  • Inverse: checkedInPlayer
  • Destination: CheckIn entity

Players <<->> Matches (each match can have two players and players can have multiple matches at each meet)

  • From: Player entity
  • Relationship: playerMatches
  • Inverse: matchPlayers
  • Destination: Match entity

I have a shared collection view that lists all the players in the app. It is used when players check in and when they are added to new match entries. All this works fine so far.

What I want to do:

I would like the players collection view to filter the players listed based on their check-in status. For example, when checking in a new player, the players collection view should only show players who have not yet checked in that day. (CheckIns has a date property for each entry) Also, when adding a player to a match, the collection view should show only players who have already checked in that day.

I was planning to add an NSString property to the players collection view that is set with the predicate text when I load the collection view modally. This way, I can change the predicate based on whether I am invoking the collection view from my matches and check in views respectively.

Is this possible with what I am trying to do? What would these predicate strings look like?

Thanks!

Update

I should be more clear. I am using a fetchedresultscontroller to get the players into the collectionview, so I am looking for a predicate to use in the fetch request...

Adding Code...

My CollectionViewController:

PlayersCollectionViewController.h
#import "CoreCollectionViewController.h"
#import "Player.h"

@protocol PlayersCollectionViewControllerDelegate;


@interface PlayersCollectionViewController : CoreCollectionViewController <NSFetchedResultsControllerDelegate>

@property (nonatomic, assign) id <PlayersCollectionViewControllerDelegate> delegate;
@property (nonatomic, strong) NSString *titleText;
@property (nonatomic, strong) NSString *predicate;

- (IBAction)cancel:(UIBarButtonItem *)sender;
- (IBAction)done:(UIBarButtonItem *)sender;

@end

@protocol PlayersCollectionViewControllerDelegate <NSObject>

@optional
-(void)ViewController:(UIViewController *)sender
     didSelectPlayer:(Player *)selectedPlayer;

@optional
-(void)ViewController:(UIViewController *)sender
      didSelectPlayer1:(Player *)selectedPlayer1;

@optional
-(void)ViewController:(UIViewController *)sender
      didSelectPlayer2:(Player *)selectedPlayer2;

@end

...and the implementation:

#import "PlayersCollectionViewController.h"
#import "AppDelegate.h"
#import "PlayerCollectionViewCell.h"

@interface PlayersCollectionViewController ()
@property(nonatomic, strong) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, strong) NSFetchedResultsController *fetchedResultsController;
@end

@implementation PlayersCollectionViewController
@synthesize titleText, predicate;

static NSString * const reuseIdentifier = @"Cell";

- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // Custom initialization
    }
    return self;
}

-(NSManagedObjectContext*)managedObjectContext{
    return [(AppDelegate*)[[UIApplication sharedApplication]delegate]managedObjectContext];
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    // Uncomment the following line to preserve selection between presentations
    // self.clearsSelectionOnViewWillAppear = NO;

    // Register cell classes
    [self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:reuseIdentifier];

    // Do any additional setup after loading the view.
    NSError *error = nil;
    if (![[self fetchedResultsController]performFetch:&error]) {
        NSLog(@"Error: %@", error);
        abort();
    }
}

-(void)viewWillAppear:(BOOL)animated
{
    [self setTitle:titleText];
    [self.collectionView reloadData];
}

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

/*
#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.
}
*/

#pragma mark <UICollectionViewDataSource>

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
    return [[self.fetchedResultsController sections]count];
}


- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return [[self.fetchedResultsController fetchedObjects]count];
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{

    PlayerCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"PlayerCollCell" forIndexPath:indexPath];
    Player *player = [self.fetchedResultsController objectAtIndexPath:indexPath];

    // Configure the cell

    if (player.playerImage) {
        cell.playerImageView.image = [UIImage imageWithContentsOfFile:player.playerImageSmall];
    }

    [cell.playerNameLabel setText:player.firstName];

    return cell;
}

-(BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
    return YES;
}

-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{

    PlayerCollectionViewCell *selectedPlayerCell = (PlayerCollectionViewCell*)[collectionView cellForItemAtIndexPath:indexPath];

    [collectionView dequeueReusableCellWithReuseIdentifier:@"PlayerCollCell" forIndexPath:indexPath];

    selectedPlayerCell.backgroundColor = [UIColor lightGrayColor];

    Player *selectedPlayer = [self.fetchedResultsController objectAtIndexPath:indexPath];

    NSLog(@"Selected player %@", selectedPlayer.firstName);

    if ([self.title isEqualToString:@"Select Player 1"]) {
        [self.delegate ViewController:self didSelectPlayer1:selectedPlayer];
    } else if ([self.title isEqualToString:@"Select Player 2"]) {
        [self.delegate ViewController:self didSelectPlayer2:selectedPlayer];
    } else if ([self.title isEqualToString:@"Select Player"]) {
        [self.delegate ViewController:self didSelectPlayer:selectedPlayer];
    }

    [self dismissViewControllerAnimated:YES completion:nil];
}

#pragma mark - ViewController methods
- (IBAction)cancel:(UIBarButtonItem *)sender {
    [self dismissViewControllerAnimated:YES completion:nil];
}

- (IBAction)done:(UIBarButtonItem *)sender { //not sure if this method and UI are needed...
    [self dismissViewControllerAnimated:YES completion:nil];
}


#pragma mark - Fetched Results Controller Section
-(NSFetchedResultsController*) fetchedResultsController
{
    if (_fetchedResultsController !=nil) {
        return _fetchedResultsController;
    }

    NSFetchRequest *fetchedRequest = [[NSFetchRequest alloc]init];

    NSManagedObjectContext *context = [self managedObjectContext];

    NSEntityDescription *entity =[NSEntityDescription entityForName:@"Player" inManagedObjectContext:context];

    [fetchedRequest setEntity:entity];

    NSSortDescriptor *lastNameSortDescriptor = [[NSSortDescriptor alloc]initWithKey:@"lastName" ascending:YES];
    NSSortDescriptor *firsttNameSortDescriptor = [[NSSortDescriptor alloc]initWithKey:@"firstName" ascending:YES];

    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:lastNameSortDescriptor, firsttNameSortDescriptor, nil];

    fetchedRequest.sortDescriptors = sortDescriptors;


    _fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchedRequest
                                                                    managedObjectContext:context
                                                                      sectionNameKeyPath:nil
                                                                               cacheName:nil];

    _fetchedResultsController.delegate = self;

    return _fetchedResultsController;

}

#pragma mark - Fetched Results Controller Delegates
/*
 -(void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
 [self.collectionView beginUpdates];
 }

 -(void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
 [self.collectionView endUpdates];
 }
 */

-(void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {

    UICollectionView *collectionView = self.collectionView;

    switch (type) {
        case NSFetchedResultsChangeInsert:
            [collectionView insertItemsAtIndexPaths:[NSArray arrayWithObjects:newIndexPath, nil]];
            break;

        case NSFetchedResultsChangeDelete: [collectionView deleteItemsAtIndexPaths:[NSArray arrayWithObjects:indexPath, nil]];
            break;

        case NSFetchedResultsChangeUpdate: {
            PlayerCollectionViewCell *selectedPlayerCell = (PlayerCollectionViewCell*)[collectionView cellForItemAtIndexPath:indexPath];

            //selectedPlayerCell.backgroundColor = [UIColor lightGrayColor];

            Player *selectedPlayer = [self.fetchedResultsController objectAtIndexPath:indexPath];

            if (selectedPlayer.playerImage) {
                selectedPlayerCell.playerImageView.image = [UIImage imageWithContentsOfFile:selectedPlayer.playerImageSmall];
            }

            [selectedPlayerCell.playerNameLabel setText:selectedPlayer.firstName];

        }
            break;

        case NSFetchedResultsChangeMove: [collectionView deleteItemsAtIndexPaths:[NSArray arrayWithObjects:indexPath, nil]];
            [collectionView insertItemsAtIndexPaths:[NSArray arrayWithObjects:newIndexPath, nil]];
            break;

    }

}

@end

Solution

  • Step away from your view controller and fetched results controller for a moment. Your predicate belongs with the Model layer, not with the Controller or View layers. There's nothing special about predicates for NSFetchRequest or NSFetchedResultsController. It's still just an NSPredicate.

    Why do this at the Model layer? Your question of filtering based on "checked in that day" has nothing to do with views, or what's displayed to user. It's purely a data question. So solve it at the Model layer (bonus: it's now easy to write a unit test for this fetch).

    If you cheat a little and use what you know about the data, you can make this easier. Make the Player<->>CheckIn relationship an ordered one (side note: entity names in Core Data are singular, just like class names, although to-many relationships are plural). Now it's easy to get a player's most recent checkin: it's thePlayer.checkins[0], and the checkin time is [thePlayer.checkins[0] timeOfEvent]. Just make sure that when you add a CheckIn to a Player, you put it at index 0.

    You can make it even easier, though, by denormalizing your data model a little. Keep the to-many relationship from Player to CheckIn. But add an attribute on Player, timeOfLastCheckIn. Make sure that you update timeOfLastCheckIn whenever you add a CheckIn; "say it once" means you should wrap this pair of steps in a method, and always invoke that method to add a CheckIn.

    Now to the Xcode Data Modeler. Create a new fetch request on Player with a substitution variable. Choose Expression in the drop down and enter timeOfLastCheckIn > $fromdate. Write a method on Player (+playersCheckingInAfterDate:context:) to execute that fetch request, accepting two parameters, the NSManagedObjectContext and an NSDate, and returning an array of Players. You'll use fetchRequestFromTemplateWithName:substitutionVariables: to instantiate a fetch request, substituting the NSDate corresponding to local midnight for $fromdate. Add a wrapper method, if you like, to compute time of local midnight and pass it to +playersCheckingInAfterDate:context:. You can switch the > to < in a similar set of methods/fetch template to find players who haven't checked in today.