0

I have two different view controllers displaying almost the same fetch request. One of them displays more info than the other and is also used for editing of the data. The second one is just for display.

Is there a way to speed things up, since both are displaying exactly the same data? I'm looking for something in my code which makes the whole reorder slow after displaying the second view controller for the first time. In other words: Initially the reordering in my main view controller is very fast. Then you just display the second view controller and switch back and then the reordering in the main view controller gets slow. I have reason to assume that it is because they are using the same fetch.

I'm initiating the fetch request in both viewDidLoad methods, as in the following code snippets. The first one is my main view controller, and also the first one displayed when starting the app:

- (void)setupFetchedResultsController
{
    self.managedObjectContext = ((AppDelegate *)[[UIApplication sharedApplication] delegate]).managedObjectContext;

    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"MainCategory"];
    request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"position" ascending:YES]];

    self.fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:request
                                                                       managedObjectContext:self.managedObjectContext
                                                                         sectionNameKeyPath:nil                                                                                  cacheName:@"MainCategoryCache"];
}

And this is my second one:

- (void)setupFetchedResultsController
{
    self.managedObjectContext = ((AppDelegate *)[[UIApplication sharedApplication] delegate]).managedObjectContext;

    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"MainCategory"];
    request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"position" ascending:YES]];

    self.fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:request
                                                                       managedObjectContext:self.managedObjectContext
                                                                         sectionNameKeyPath:nil
                                                                                  cacheName:@"NetCache"];
}

Here are the view did load and cellForRowAtIndexPath of my main view controller:

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self setupFetchedResultsController];

    //Edit/Done button
    UIBarButtonItem *editButton = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Edit", nil) style:UIBarButtonItemStyleBordered target:self action:@selector(editTable:)];
    [self.navigationItem setRightBarButtonItem:editButton];

    //Budget at bottom
    self.sumTitleLabel.text = [NSString stringWithFormat:@"%@%@:",NSLocalizedString(@"Budget", nil),NSLocalizedString(@"PerMonth", nil)];
    self.sumLabel.text = [[DatabaseFetches budgetPerMonthForManagedObjectContext:self.managedObjectContext] getLocalizedCurrencyString];

    //Layout
    self.view.backgroundColor = [UIColor clearColor];
    [self styleTableView];
    [self styleBudgetTotal];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    //Data model and cell setup
    static NSString *CellIdentifier = @"MainCategoryCell";
    MainCategoryTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    MainCategory *mainCategory = [self.fetchedResultsController objectAtIndexPath:indexPath];

    //Clear background color
    cell.backgroundColor = [UIColor clearColor];

    //Setup the cell texts
    cell.title.text = mainCategory.name;

    int numberOfSubcategories = [[mainCategory getNumberOfSpendingCategories] integerValue];
    if (numberOfSubcategories == 1) {
        cell.subcategories.text = [NSString stringWithFormat:@"%i %@", numberOfSubcategories, NSLocalizedString(@"subcategory", nil)];
    } else {
        cell.subcategories.text = [NSString stringWithFormat:@"%i %@", numberOfSubcategories, NSLocalizedString(@"subcategories", nil)];
    }

    cell.costs.text = [[mainCategory getMonthlyCostsOfAllSpendingCategories] getLocalizedCurrencyString];

    //Delegation
    cell.title.delegate = self;

    //Format text
    cell.title.font = [Theme tableCellTitleFont];
    cell.title.textColor = [Theme tableCellTitleColor];
    cell.title.tag = indexPath.row;
    cell.subcategories.font = [Theme tableCellSubTitleFont];
    cell.subcategories.textColor = [Theme tableCellSubTitleColor];
    cell.costs.font = [Theme tableCellValueFont];
    cell.costs.textColor = [Theme tableCellValueColor];

    //Icon
    UIImage *icon;
    if(!mainCategory.icon){
        icon = [UIImage imageNamed:@"DefaultIcon.png"];
    } else {
        icon = [UIImage imageNamed:mainCategory.icon];
    }
    [cell.iconButton setImage:icon forState: UIControlStateNormal];
    cell.icon.image = icon;
    cell.iconButton.tag = indexPath.row;


    //Background image
    cell.cellBackground.image = [[UIImage imageNamed:@"content-bkg"] resizableImageWithCapInsets:UIEdgeInsetsMake(10, 10, 10, 10)];

    //Selection
    //cell.title.highlightedTextColor = cell.title.textColor;
    cell.subcategories.highlightedTextColor = cell.subcategories.textColor;
    cell.costs.highlightedTextColor = cell.costs.textColor;

    return cell;
}

Complete second view controller:

@interface NetIncomeViewController()
@property (nonatomic, strong) NSManagedObjectContext *managedObjectContext;
@end

@implementation NetIncomeViewController

@synthesize incomeTitleLabel = _incomeTitleLabel;
@synthesize incomeValueTextField = _incomeValueTextField;
@synthesize incomeBackground = _incomeBackground;
@synthesize incomeValueBackground = _incomeValueBackground;
@synthesize netIncomeTitleLabel = _netIncomeTitleLabel;
@synthesize netIncomeValueLabel = _netIncomeValueLabel;
@synthesize netIncomeBackground = _netIncomeBackground;
@synthesize netIncomeValueBackground = _netIncomeValueBackground;
@synthesize managedObjectContext = _managedObjectContext;

#pragma mark Initializer and view setup
- (void)setupFetchedResultsController
{
    self.managedObjectContext = ((AppDelegate *)[[UIApplication sharedApplication] delegate]).managedObjectContext;

    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"MainCategory"];
    request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"position" ascending:YES]];

    self.fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:request
                                                                       managedObjectContext:self.managedObjectContext
                                                                         sectionNameKeyPath:nil
                                                                                  cacheName:nil];
}

#pragma mark View lifecycle
- (void)awakeFromNib{
    [super awakeFromNib];

    //Title (necessary here because otherwise the tab bar would be only localized after first click
    //on the respective tab bar and not from beginning
    self.title = NSLocalizedString(@"Net Income", nil);
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [self updateNetIncome];
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self setupFetchedResultsController];

    //Edit/Done button
    UIBarButtonItem *editButton = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Edit", nil) style:UIBarButtonItemStyleBordered target:self action:@selector(editIncome:)];
    [self.navigationItem setRightBarButtonItem:editButton];

    //Get or set and get the gross income
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSNumber *grossIncome = [defaults objectForKey:@"income"];
    if (!grossIncome) {
        [defaults setDouble:0 forKey:@"income"];
        [defaults synchronize];
        grossIncome = [defaults objectForKey:@"income"];
    }
    self.incomeValueTextField.enabled = NO;
    self.incomeValueTextField.delegate = self;
    self.incomeValueTextField.keyboardType = UIKeyboardTypeDecimalPad;
    self.incomeValueTextField.text = [grossIncome getLocalizedCurrencyString];

    [self styleTableViewAndIncome];
}


- (void)styleTableViewAndIncome
{
    self.view.backgroundColor = [UIColor clearColor];
    self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;

    self.tableView.backgroundColor = [UIColor clearColor];

    self.incomeTitleLabel.textColor = [Theme budgetValueTitleColor];
    self.incomeTitleLabel.font = [Theme budgetValueTitleFont];
    self.incomeValueTextField.textColor = [Theme budgetValueColor];
    self.incomeValueTextField.font = [Theme budgetValueTitleFont];

    self.netIncomeTitleLabel.textColor = [Theme budgetValueTitleColor];
    self.netIncomeTitleLabel.font = [Theme budgetValueTitleFont];
    self.netIncomeValueLabel.textColor = [Theme budgetValueColor];
    self.netIncomeValueLabel.font = [Theme budgetValueFont];

    self.incomeTitleLabel.text = [NSString stringWithFormat:@"%@:",NSLocalizedString(@"Income", nil)];
    self.netIncomeTitleLabel.text = [NSString stringWithFormat:@"%@%@:",NSLocalizedString(@"Net", nil),NSLocalizedString(@"PerMonth", nil)];

    self.incomeBackground.image = [[UIImage imageNamed:@"content-bkg"] resizableImageWithCapInsets:UIEdgeInsetsMake(10, 10, 10, 10)];
    self.incomeValueBackground.image = [[UIImage imageNamed:@"price-bkg"] resizableImageWithCapInsets:UIEdgeInsetsMake(10, 10, 10, 10)];
    self.netIncomeBackground.image = [[UIImage imageNamed:@"content-bkg"] resizableImageWithCapInsets:UIEdgeInsetsMake(10, 10, 10, 10)];
}

#pragma mark Table view delegate methods
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"CategoryCostCell";
    CategoryCostTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

    cell.backgroundColor = [UIColor clearColor];

    // Configure the cell layout
    MainCategory *mainCategory = [self.fetchedResultsController objectAtIndexPath:indexPath];

    // Configure the cell layout    
    cell.categoryBackground.image = [[UIImage imageNamed:@"content-bkg"] resizableImageWithCapInsets:UIEdgeInsetsMake(10, 10, 10, 10)];
    if(!mainCategory.icon){
        cell.categoryImage.image = [UIImage imageNamed:@"DefaultIcon.png"];
    } else {
        cell.categoryImage.image = [UIImage imageNamed:mainCategory.icon];
    }

    cell.categoryName.text = mainCategory.name;
    NSNumber *expenditures = [mainCategory getMonthlyCostsOfAllSpendingCategories];
    double expendituresDouble =-[expenditures doubleValue];
    if (expendituresDouble == 0) {
        expenditures = [NSNumber numberWithDouble: 0];
    } else {
        expenditures = [NSNumber numberWithDouble: expendituresDouble];
    }
    cell.categoryCosts.text = [expenditures getLocalizedCurrencyString];

    //Format the text
    cell.categoryName.font = [Theme tableCellSmallTitleFont];
    cell.categoryName.textColor = [Theme tableCellSmallTitleColor];
    cell.categoryCosts.font = [Theme tableCellSmallValueFont];
    cell.categoryCosts.textColor = [Theme tableCellSmallValueColor];

    return cell;
}

#pragma mark TextField delegate methods
-(BOOL)textFieldShouldBeginEditing:(UITextField *)textField
{
    if(self.editing){
        return YES;
    } else {
        return NO;
    }
}

-(void)textFieldDidBeginEditing:(UITextField *)textField
{
    textField.text = [NSString stringWithFormat:@"%.2f",[NSNumber getUnLocalizedCurrencyDoubleWithString:textField.text]];
}

-(void)textFieldDidEndEditing:(UITextField *)textField
{
    textField.text = [[NSNumber numberWithDouble:[textField.text doubleValue]] getLocalizedCurrencyString];
}

-(BOOL)textFieldShouldReturn:(UITextField *)textField
{
    //Handle the enter button
    [textField resignFirstResponder];
    [self endEdit];
    //Since this method is called AFTER DidEndEditing, we have to call editTable or in this case since we have put all the endEdit code in a seperate method this method directly in order to format the table back from edit mode to normal.
    return YES;
}


#pragma mark Edit/Add/Delete action
- (IBAction) editIncome:(id)sender
{
    if(self.editing)
    {
        [self endEdit];
    } else {
        [self beginEdit];
    }
}

- (void) beginEdit
{
    //Change to editing mode
    [super setEditing:YES animated:YES];
    //Exchange the edit button with done button
    [self.navigationItem.rightBarButtonItem setTitle:NSLocalizedString(@"Done", nil)];
    [self.navigationItem.rightBarButtonItem setStyle:UIBarButtonItemStyleDone];
    self.incomeValueTextField.borderStyle = UITextBorderStyleRoundedRect;
    self.incomeValueTextField.enabled = YES;

    self.incomeValueBackground.hidden = YES;
    self.incomeValueTextField.textColor = [Theme budgetValueTitleColor];
}

- (void) endEdit
{
    //Change to editing no
    [super setEditing:NO animated:YES];
    //Remove Done button and exchange it with edit button
    [self.navigationItem.rightBarButtonItem setTitle:NSLocalizedString(@"Edit", nil)];
    [self.navigationItem.rightBarButtonItem setStyle:UIBarButtonItemStylePlain];
    self.incomeValueTextField.borderStyle = UITextBorderStyleNone;
    self.incomeValueTextField.enabled = NO;
    self.incomeValueBackground.hidden = NO;
    self.incomeValueTextField.textColor = [Theme budgetValueColor];
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setDouble:[NSNumber getUnLocalizedCurrencyDoubleWithString:self.incomeValueTextField.text] forKey:@"income"];
    [self updateNetIncome];
    //[defaults synchronize];
}

#pragma mark Net income
- (void) updateNetIncome {
    double grossIncome = [NSNumber getUnLocalizedCurrencyDoubleWithString:self.incomeValueTextField.text];
    double budget = [[DatabaseFetches budgetPerMonthForManagedObjectContext:self.managedObjectContext] doubleValue];
    double netIncome = grossIncome - budget;

    if(netIncome >0){
        self.netIncomeValueBackground.image = [[UIImage imageNamed:@"price-bkg_green"] resizableImageWithCapInsets:UIEdgeInsetsMake(10, 10, 10, 10)];
    } else if(netIncome <0) {
        self.netIncomeValueBackground.image = [[UIImage imageNamed:@"price-bkg_red"] resizableImageWithCapInsets:UIEdgeInsetsMake(10, 10, 10, 10)];
    } else {
        self.netIncomeValueBackground.image = [[UIImage imageNamed:@"price-bkg"] resizableImageWithCapInsets:UIEdgeInsetsMake(10, 10, 10, 10)];
    }

    self.netIncomeValueLabel.text = [[NSNumber numberWithDouble:netIncome] getLocalizedCurrencyString];
}

@end
Nathaniel Ford
  • 20,545
  • 20
  • 91
  • 102
MichiZH
  • 5,587
  • 12
  • 41
  • 81
  • What do you mean when you say the controller "gets slow"? Does the table scroll lag noticeably? And how many records are we talking about here? Given FRC's (Fetched Results Controller) efficiency in pulling records out of a Core Data model, I'd suspect that your problem lies somewhere other than running a second FRC. – Carter Fort Jan 05 '14 at 13:50
  • Yeah it lags noticeably. Right now I have only 3 records (with only 5 attributes, no images only strings and numbers) and it is already laggy. The more I insert the bigger the lag gets.. – MichiZH Jan 05 '14 at 13:54
  • Then it's probably not your FRC. Are you doing anything fancy with your UITableViewCell drawing? – Carter Fort Jan 05 '14 at 14:04
  • Come to think of it, it would be better if you just posted the relevant UITableViewController methods from your code here. Give us viewDidLoad and cellForRowAtIndexPath to start, and we'll see where we are. – Carter Fort Jan 05 '14 at 14:05
  • I'd venture to guess that the problem is something other than your frc. Show us how you are instantiating and configuring your cells. Where are you executing the fetch? – Dean Davids Jan 05 '14 at 14:06
  • What's the performance like if you don't initialize the images for every cell? Creating UIImages can be a resource hog. – Carter Fort Jan 05 '14 at 14:20
  • How would I do that? I mean how would I reuse the image without initializing it every time? – MichiZH Jan 05 '14 at 14:23
  • Using storyboard/prototype cells? It looks like you are doing all that formatting and appearance processing every time you provide a cell. That would do it, especially the image creation as Carter says. – Dean Davids Jan 05 '14 at 14:23
  • You could create two UIImage instance variables and initialize them in the viewDidLoad method. Then reference them in the cellForRowAtIndexPath where appropriate. – Carter Fort Jan 05 '14 at 14:25
  • Hm not sure if I can do that. I mean I only format the font type and size in this method and of course all data pulled from core data. And the images etc. are different for all entries, I wouldn't know how to do that differently? And the background image maybe, but I don't know how to set the capinsets in storyboard (cell.cellBackground.image = [[UIImage imageNamed:@"content-bkg"] resizableImageWithCapInsets:UIEdgeInsetsMake(10, 10, 10, 10)];) – MichiZH Jan 05 '14 at 14:27
  • I'm writing an answer using this line of thinking, and we can continue the comments there. – Carter Fort Jan 05 '14 at 14:29

1 Answers1

1

It looks like you're doing UIImage creation in your cellForRowAtIndexPath method, which would cause a slowdown.

Better to create the background UIImage as an instance variables and set its image in the viewDidLoad, then reference it in the cellForRowAtIndexPath method. Like so:

//Declare the instance variable
UIImage *backgroundImage

//Set it in viewDidLoad
self.backgroundImage = [[UIImage imageNamed:@"content-bkg"] resizableImageWithCapInsets:UIEdgeInsetsMake(10, 10, 10, 10)];

//Assign it in cellForRowAtIndexPath
cell.backgroundImage = self.backgroundImage;

As for the icon, which has to be set for each row, you'll want to move the actual image loading off the main thread. You can do this either with dispatch_async() or NSOperationQueue. (pulled from here: Understanding dispatch_async)

dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
     UIImage *icon;
     if(!mainCategory.icon){
          icon = [UIImage imageNamed:@"DefaultIcon.png"];
     } else {
          icon = [UIImage imageNamed:mainCategory.icon];
     }
     dispatch_async(dispatch_get_main_queue(), ^(void){
          [cell.iconButton setImage:icon forState: UIControlStateNormal];
          cell.icon.image = icon;
          cell.iconButton.tag = indexPath.row;
     });
});

Notice how the imageNamed method is called in the background thread, but the actual assignment takes place on the main thread. You have to do it this way because you're not allowed to update UI elements anywhere except the main thread.

Community
  • 1
  • 1
Carter Fort
  • 953
  • 7
  • 14
  • Thanks for your thorough answer. Great improvement for my code I guess and I've implemented it all. However for my real problem it didn't help :-( Lag is still there. I have really no idea why...I don't think the cells load slowly at all..and the funny thing is this lag only happens if I switch to this second view controller using the same fetch. If I switch to any other view controller (and never open the other one) it doesn't get slowed down.. – MichiZH Jan 05 '14 at 14:46
  • Can you post your code for the second view controller? Sounds like something might be going on there. – Carter Fort Jan 05 '14 at 14:50
  • Yep, just did :-) Posted complete second view controller. I really don't know what stuff I'm missing..my app isn't really complicated, I never save any images to coredata, however lots of UI stuff lags (UISwitch, AlertView, selectin an image) and the row reordering as discussed here. I usually like looking for problems and optimize my app..but here I don't know where to go further.. – MichiZH Jan 05 '14 at 14:56
  • I know where the problem originates now. Have to start another thread for this. Will mark your answer as accepted since it still improved my code :) – MichiZH Jan 05 '14 at 15:14