25
Feb
2014
 

Deleting Objects in Core Data

by Marcus Zarra

I very rarely speak out against another blog post. I find the resulting argument back and forth draining. However, there are exceptions and one occurred over the weekend.

Brent Simmons has been blogging about his conversion of Vesper to a Core Data application. A recent post of his titled Core Data and Deleting Objects set my teeth on edge. Brent says:

“The best advice I’ve heard about deleting managed objects is to 1) not do it, or 2) do it only at startup, before any references to those to-be-deleted objects can be made.”

I do not know who is giving Brent advice but he must be playing tricks with him or just trying to wind him up. The advice was simple; don’t delete Core Data objects or if you are going to delete them, delete them at launch.

Will that work? Sure. Is it the right answer? Not even close.

Advice like this comes from mistakes. Mistakes in application design. Mistakes in application planning. Someone, somewhere, coded himself into a corner and decided that Core Data was to blame. I hear this a lot. It is one of the common conversation starters I have with new clients.

So what is the right answer? Plan your application out. Plan your application for deleting objects.

When we are talking about working on iOS the easiest way to handle this is to use the built in tools that the framework already provides for you. Use the NSFetchedResultsController. Use it everywhere it makes sense. Squeeze it into places that it kind of makes sense. Why? It watches the NSManagedObjectContext for you! It will tell you when an object has been inserted, updated or deleted! It is like magic!

Don’t like it or it doesn’t fit for you? Build your own! Seriously, the core of the NSFetchedResultsController is not hard at all. Here is a very basic one that just watches for deletes:

//
//  MCDeleteWatcher.h
//  MCDeleteWatcher
//
//  Created by Marcus S. Zarra on 2/24/14.
//  Copyright (c) 2014 MartianCraft. All rights reserved.
//  Permission is hereby granted, free of charge, to any person
//  obtaining a copy of this software and associated documentation
//  files (the "Software"), to deal in the Software without
//  restriction, including without limitation the rights to use,
//  copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the
//  Software is furnished to do so, subject to the following
//  conditions:
//
//  The above copyright notice and this permission notice shall be
//  included in all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
//  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
//  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
//  OTHER DEALINGS IN THE SOFTWARE.
//

typedef void (^DeletedObjectsBlock)(NSSet *deletedObjects);

@interface MCDeleteWatcher : NSObject

@property (copy) DeletedObjectsBlock deletedObjectsBlock;

- (id)initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator*)psc;

- (void)addObjectToWatch:(NSManagedObject*)object;
- (void)addObjectsToWatch:(NSArray*)objects;

@end

//
//  MCDeleteWatcher.m
//  MCDeleteWatcher
//
//  Created by Marcus S. Zarra on 2/24/14.
//  Copyright (c) 2014 MartianCraft. All rights reserved.
//  Permission is hereby granted, free of charge, to any person
//  obtaining a copy of this software and associated documentation
//  files (the "Software"), to deal in the Software without
//  restriction, including without limitation the rights to use,
//  copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the
//  Software is furnished to do so, subject to the following
//  conditions:
//
//  The above copyright notice and this permission notice shall be
//  included in all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
//  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
//  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
//  OTHER DEALINGS IN THE SOFTWARE.
//
#import "MCDeleteWatcher.h"

@interface MCDeleteWatcher()

@property (weak) NSPersistentStoreCoordinator *persistentStoreCoordinator;
@property (strong) NSMutableSet *objectsToWatch;

@end

@implementation MCDeleteWatcher

- (id)initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator*)psc;
{
  ZAssert(psc, @"PSC is nil!");
  ZAssert([NSThread isMainThread], @"Initialization must be on main thread");

  if (!(self = [super init])) return nil;

  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contextUpdated:) name:NSManagedObjectContextObjectsDidChangeNotification object:nil];

  [self setPersistentStoreCoordinator:psc];
  [self setObjectsToWatch:[NSMutableSet set]];

  return self;
}

- (void)dealloc
{
  [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)addObjectToWatch:(NSManagedObject*)object;
{
  [[self objectsToWatch] addObject:object];
}

- (void)addObjectsToWatch:(NSArray*)objects;
{
  [[self objectsToWatch] addObjectsFromArray:objects];
}

- (void)contextUpdated:(NSNotification*)notification
{
  if (![self deletedObjectsBlock]) return;

  if ([[notification object] persistentStoreCoordinator] != [self persistentStoreCoordinator]) return;

  NSSet *deleted = [[notification userInfo] objectForKey:NSDeletedObjectsKey];
  if (![[self objectsToWatch] intersectsSet:deleted]) return;

  NSMutableSet *objectsDeleted = [NSMutableSet set];
  for (NSManagedObject *object in deleted) {
    if ([[self objectsToWatch] containsObject:object]) [objectsDeleted addObject:object];
  }

  if ([NSThread isMainThread]) {
    [self deletedObjectsBlock](objectsDeleted);
  } else {
    dispatch_async(dispatch_get_main_queue(), ^{
      [self deletedObjectsBlock](objectsDeleted);
    });
  }
}

@end

So why are deleted NSManagedObject instances so dangerous? Well it depends on your version of Core Data. Prior to iOS 5 (meaning iOS 3 and 4) if you tried to access a property on a deleted NSManagedObject instance you would get an immediate exception of “CoreData could not fulfill a fault” which would result in your application crashing.

I like to pretend that iOS 5 never happened.

In iOS 6 and 7, accessing the property will not cause a crash. You will just get nil back which can be unexpected. However if you try and set a property, you get the familiar crash.

How do we avoid this edge case? Well, it is kind of like memory management, when you have released the object (aka deleted it) don’t touch it again!

Watch for notifications on objects that can be deleted under you. React to those notifications. Plan for them. Presenting an edit view and the object is deleted? React!

If you are in legacy code and need to test for an object to see if it was deleted from under you and the ripple is too large to do it right then you can ask the object -isDeleted and you can also test for the NSManagedObjectContext being nil.

Be warned though. -isDeleted really should be called -isScheduledForDeletion as it will only return YES if the object will be deleted during the next save. Once the NSManagedObjectContext has been saved it will return NO.

Testing -managedObjectContext for nil also has a catch. There are multiple situations where the -managedObjectContext property of a NSManagedObject can be nil. For example, you can create a NSManagedObject without a context. Therefore this is not an absolute test either.

You can also play around with -[NSManagdObjectContext existingObjectWithID:error:] if you want.

These are a code smells.

Your application should be handling deletes properly and should know that the object has been deleted and react to it. Waiting until you are on the edge of the cliff and then asking the now-deleted object if it has been deleted is bad.

What is the right answer?

Plan for deletions.

Core Data is a framework with a nearly impossible job. Writing a universal persistence engine is right up there with solving PI. You can go really deep but you are never going to “solve it”. Eventually it is “good enough” and has edge cases.

There is a reason Brent keeps coming back to Core Data. Writing your own persistence engine is more trouble than it is worth. Better to use an existing framework that has a team of highly skilled engineers working on it than to write your own. Yes it will have edges. Yes it can break. But it is better than the alternative 99.999% of the time.

Our job as developers is to learn where those edges are and plan for them.

Don’t scream in the darkness; learn how to turn on the light.