0

Core Data Model

I have the above CoreData Model in my first iPad app. I'm building a filtering system in a TableViewController as shown below. The problem is that whenever I make a UI change, toggle a switch of tap a button, My UI becomes non-responsive for a second or two. I run a really long function that recreates the fetch request for the photos, then runs more count fetches to determine whether the control should be enabled. I just don't know how I can break this apart in a meaningful way that would prevent the hang. Even If I need to add a spinning view for a second or so, I'm happy with that. Just want to get rid of the lag.

As I mentioned, this is my first attempt at iOS development so I would appreciate any suggestions...

enter image description here

-(void) refilterPhotos {
/*
*   First section builds the NSCompoundPredicate to use for searching my CoreData Photo objects. 
    Second section runs queries so 0 result controls can be disabled.
*/
subpredicates = [[NSMutableArray alloc] init];

NSPredicate *isNewPredicate;
if(newSwitch.on) {
    isNewPredicate = [NSPredicate predicateWithFormat:@"is_new == 1"];
} else {
    isNewPredicate = [NSPredicate predicateWithFormat:@"is_new == 0"];
}

[subpredicates addObject:isNewPredicate];

//Photo Types
PhotoType *photoType;
NSPredicate *photoTypePredicate;
for (UISwitch *photoSwitch in photoSwitches) {
     PhotoType * type = (PhotoType *) photoSwitch.property;
    if([type.selected boolValue] == YES) {
        NSLog(@"photo_type.label == %@", type.label);
        photoType = type;
        photoTypePredicate = [NSPredicate predicateWithFormat:@"photo_type.label == %@", type.label];
        break;
    }
}

//Feed Types
FeedType *feedType;
NSPredicate *feedTypePredicate;
for (UISwitch *feedSwitch in feedSwitches) {
    FeedType * type = (FeedType *) feedSwitch.property;
    if([type.selected boolValue] == YES) {
        NSLog(@"feed_type.label == %@", type.label);
        feedType = type;
        feedTypePredicate = [NSPredicate predicateWithFormat:@"feed_type.label == %@", type.label];
        break;
    }
}

//Markets
NSArray *filteredMarkets = [model.availableMarkets filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"selected == 1"]];
for (Market *market in filteredMarkets) {
    [subpredicates addObject:[NSPredicate predicateWithFormat:@"ANY markets.name == %@", market.name]];
}


//Tags
NSArray *filteredTags = [model.availableTags filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"selected == 1"]];
for (Tag *tag in filteredTags) {
    NSLog(@"ANY tags.name == %@",tag.name);
    [subpredicates addObject:[NSPredicate predicateWithFormat:@"ANY tags.name == %@", tag.name]];
}

if(photoTypePredicate)
    [subpredicates addObject:photoTypePredicate];
if(feedTypePredicate)
    [subpredicates addObject:feedTypePredicate];
NSPredicate *finished = [NSCompoundPredicate andPredicateWithSubpredicates:subpredicates];//Your final predicate

model.availablePhotos = [model fetchPhotoswithPredicate:finished];
[[self parentViewController] setTitle:[NSString stringWithFormat:@"%d items",[model.availablePhotos count]]];

NSLog(@"FILTERED PHOTOS:::: %d", [model.availablePhotos count]);
[gridVC reloadGrid];   

/**
 *  Filtering Section Here, I'm running count requests for each grouping of controls to ensure if they're selected, results will be returned.
 *  If zero results, I'll disable that control. For the switch-based controls, I need to removed them before running my fetches since there can only be
 *  one switch value per photo.
 */



//Have to remove the existing type predicate since they're exlcusive values
[subpredicates removeObject:isNewPredicate];

//New Toggle
NSPredicate *newRemainderPredicate = [NSPredicate predicateWithFormat:@"is_new == %d",newSwitch.on?0:1];
[subpredicates addObject:newRemainderPredicate];

if([model countPhotoswithPredicate:[NSCompoundPredicate andPredicateWithSubpredicates:subpredicates]]<1) {
    [newSwitch setEnabled:NO];
} else {
    [newSwitch setEnabled:YES];
}

[subpredicates removeObject:newRemainderPredicate];
[subpredicates addObject:isNewPredicate];



[subpredicates removeObject:photoTypePredicate];
//Photo Type Toggles
NSArray *remainderPhotoTypes = [photoSwitches filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"on == NO"]];
for( UISwitch*control in remainderPhotoTypes) {
    PhotoType *remainderPhotoType = (PhotoType*)control.property;

    [subpredicates addObject:[NSPredicate predicateWithFormat:@"photo_type == %@", remainderPhotoType]];
    if([model countPhotoswithPredicate:[NSCompoundPredicate andPredicateWithSubpredicates:subpredicates]]<1) {
        //NSLog(@"PHOTOTYPE OFF %@", remainderPhotoType.label);
        control.enabled = NO;
    } else {
        //NSLog(@"PHOTOTYPE ON %@ count = %d", remainderPhotoType.label, [[model fetchPhotoswithPredicate:[NSCompoundPredicate andPredicateWithSubpredicates:subpredicates]] count]);
        control.enabled = YES;
    }
    remainderPhotoType.enabled = [NSNumber numberWithBool:control.enabled];
    [subpredicates removeObject:[NSPredicate predicateWithFormat:@"photo_type == %@", remainderPhotoType]];
}
if(photoTypePredicate)
[subpredicates addObject:photoTypePredicate];



[subpredicates removeObject:feedTypePredicate];
//Feed Type Toggles
NSArray *remainderFeedTypes = [feedSwitches filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"on == NO"]];
for( UISwitch*control in remainderFeedTypes) {
    PhotoType *remainderFeedType = (PhotoType*)control.property;

    [subpredicates addObject:[NSPredicate predicateWithFormat:@"feed_type == %@", remainderFeedType]];
    if([model countPhotoswithPredicate:[NSCompoundPredicate andPredicateWithSubpredicates:subpredicates]]<1) {
        control.enabled = NO;
    } else {
        control.enabled = YES;

    }
    remainderFeedType.enabled = [NSNumber numberWithBool:control.enabled];
    [subpredicates removeObject:[NSPredicate predicateWithFormat:@"feed_type == %@", remainderFeedType]];
}
if(feedTypePredicate)
[subpredicates addObject:feedTypePredicate];



NSArray *remainderMarkets = [[model availableMarkets] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"selected == 0"]];
//Markets..many-to-many so I don't remove the existing predicate
for( Market *remainderMarket in remainderMarkets) {
    [subpredicates addObject:[NSPredicate predicateWithFormat:@"ANY markets == %@", remainderMarket]];
     NSInteger countForTag = [model countPhotoswithPredicate:[NSCompoundPredicate andPredicateWithSubpredicates:subpredicates]];

    if(countForTag<1) {
        remainderMarket.enabled = [NSNumber numberWithInt:0];
    } else {
        remainderMarket.enabled = [NSNumber numberWithInt:1];
    }
    [subpredicates removeObject:[NSPredicate predicateWithFormat:@"ANY markets == %@", remainderMarket]];
}



NSArray *remainderTags = [[model availableTags] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"selected == 0"]];
//TAGS..many-to-many so I don't remove the existing predicate 
int tagCounter = 0;
for( Tag *remainderTag in remainderTags) {
    [subpredicates addObject:[NSPredicate predicateWithFormat:@"ANY tags == %@", remainderTag]];
    NSInteger countForTag = [model countPhotoswithPredicate:[NSCompoundPredicate andPredicateWithSubpredicates:subpredicates]];
    if(countForTag<1) {
        NSLog(@"TAG OFF %@", remainderTag.name);
        remainderTag.enabled = 0;
    } else {
        NSLog(@"TAG ON %@ count = %d", remainderTag.name, countForTag);
        remainderTag.enabled = [NSNumber numberWithInt:1];
    }
    [subpredicates removeObject:[NSPredicate predicateWithFormat:@"ANY tags.name == %@", remainderTag.name]];
    tagCounter ++;
} 
//Update the controls with this new data
[self.tableView reloadData];    

}

ChickensDontClap
  • 1,871
  • 4
  • 22
  • 39
  • It seems in the code you are not doing anything with `subpredicates`. In your UI you should perhaps use a `UISegmentedControl` sometimes instead of a `UISwitch`. – Mundi Apr 30 '12 at 20:46
  • Hi thanks... I'm sending the subpredicates to my model that does the NSManagedObjectContext fetches...I create an NSCompoundPredicate ([NSCompoundPredicate andPredicateWithSubpredicates:subpredicates];) to join all the predicates together. An NSArray of Photos is returned or NSInteger of the count. – ChickensDontClap Apr 30 '12 at 21:02

1 Answers1

1

OK, there are several things to consider here.

First, I would consider creating indexes for the main search fields. Without an index, each search is linear, because it has to check the value of each record. An index will result in much faster searching times.

Second, I'd be very careful about ordering in a compound predicate. It will filter them based on order. Thus, you want to make you fastest, most filtering predicates first. Trim the possible solution space a quickly as possible.

You can gain a lot by indexing the attributes you use in the first 1-3 predicates. I note at the bottom, when you query for counts, you are still using the same compound predicate. Do you really want that? Also, in this code

//Have to remove the existing type predicate since they're exlcusive values
[subpredicates removeObject:isNewPredicate];
//New Toggle
NSPredicate *newRemainderPredicate = [NSPredicate predicateWithFormat:@"is_new == %d",newSwitch.on?0:1];
[subpredicates addObject:newRemainderPredicate];

You are removing the is_new check from the front, and placing it at the rear. If you are just checking this one predicate to toggle that switch, and you only care to see if there are 0 or more, why even use the entire compound predicate? Is the "toggle" going to be on/off relative to all the other fields?

If you continue with this, remember, it is going to do all those other predicates first (and some are references). Try to keep them in a good order to filter as much as possible, as quickly as you can.

Third, using references is convenient, but expensive. You can possible get better performance by querying those separately, and then using the compound predicate to filter the in-memory objects.

Fourth, you should be executing all these queries in a separate thread. That is very easy to do, but the exact method depends on your current ManagedObjecttContext arrangement. Do you have a single MOC, a parent/child relationship, a UIManagedDocument? Basically, you can create a separate MOC, and call performBlock to execute the fetches. In fact, you can fire all those fetches off asynchronously at the same time with multiple MOCs.

Then, you can just call into the main thread when they are done.

Finally, you may want to consider denormalizing your database. It will cause you to use more space, but fetches will be much faster. Specifically, the relationship fields... you could put the photo/feed labels in with the Photo itself. That way, when searching, you don't have to do the extra join to get those records.

So, it's not a simple answer, but implement each of these, and see if your performance does not improve considerably (not to mention your UI responsiveness).

Jody Hagins
  • 27,943
  • 6
  • 58
  • 87
  • Thank you so much for your thoroughness. This is excellent information and exactly what I need to expand my knowledge. To answer your question about the is_new check, yes that 'new' switch at the top needs to be enabled/disabled relative to the other fields. I have a model class that holds the reference to my context, so I call the two methods: fetchPhotosWithPredicate and countPhotoswithPredicate when I need an NSArray of Photos or just the count. Do you think there would be a performance improvement going with multiple MOCs for the filtering vs just one more for threading? – ChickensDontClap May 01 '12 at 04:12
  • Performing work on a background thread will certainly help your UI from becoming unresponsive. For the rest, personally, I'd run instruments to determine the main contributors to performance issues, and then use that information to attack the problems, in order of their performance impact. – Jody Hagins May 01 '12 at 11:05