Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I guarantee unique entries in a Core Data store in a shared app container used by both the host app and an extension?

To ask my question effectively, let's first consider the exact scenario I'm facing:

General Setup

  • A host iOS 8 app.
  • One or more iOS 8 extensions (WatchKit, Share, etc.) bundled with the host app.
  • The host app and all extensions share the same Core Data SQLite store in the shared app group container.
  • Each app/extension has its own NSPersistentStoreCoordinator and NSManagedObjectContext.
  • Each persistent store coordinator uses a persistent store that shares the same SQLite resources in the group container as all the other persistent stores.
  • The app and all extensions use a common codebase for syncing content from a remote API resource on the Internet.

Sequence of Events Leading to the Problem

  1. The user launches the host app. It begins fetching data from the remote API resource. Core data model objects are created based on the API response and "upserted" into the host app's managed object context. Each API entity has a uniqueID that identifies it in the remote API backend. By "upsert," I mean that for each API entity, the host app only creates a new entry in Core Data if an existing entry for a given uniqueID cannot be found.

  2. Meanwhile, the user also launches one of the host app's extensions. It, too, performs some kind of fetch from the same remote API. It also attempts to perform an "upsert" when parsing the API responses.

  3. The Problem: What happens if both the host app and an extension try to upsert a Core Data entry for the same API entity at the same time? To see how this could come about, let's look at the sequence of events for an upsert:

Core Data Upsert Sequence:

  1. The API parsing code parses the uniqueID for a given API entity.
  2. The parser performs a Core Data fetch for any entry that matches a predicate where uniqueID is equal to the parsed uniqueID.
  3. If an existing entry is not found, the parser inserts a new Core Data entry for this API entity, set's its uniqueID attribute to the parsed uniqueID.
  4. The parser saves the managed object context, which pushes the new entry data down to the SQLite backing store.

Problem in Detail

Let's assume the host app and the extension are independently parsing an API response for the same API entity at the same time. If both the host app and an extension reach Step 3 before either of them has finished Step 4, then they will both be trying to insert a new Core Data entry for the same uniqueID. When they reach Step 4 and call save: on their respective managed object contexts, Core Data will happily create duplicate entries.

As far as I'm aware, Core Data doesn't have any way to mark an attribute as unique. I need a Core Data equivalent to a SQLite INSERT OR IGNORE + UPDATE combo.. Or else I need a way to "lock" the persistent store's SQLite backing store, which sounds like a recipe for trouble.

Is there a known approach to this rather novel problem introduced by iOS 8 extensions?

like image 847
jaredsinclair Avatar asked Nov 19 '14 15:11

jaredsinclair


People also ask

How to share Core Data?

On device A, tap the Add button (+) to show the photo picker, and then select a photo and add it to the Core Data store. Touch and hold the photo to display the context menu and then tap Create New Share to present the CloudKit sharing UI. Follow the UI to send a link to the Apple ID on device B.

What is Share extension in iOS?

Share Extension is an easy way that Apple provides to share contents (images, audio, files, etc.) from one app to another, even made by different developers.

What is app extension in Swift?

App extensions let you extend custom functionality and content beyond your app and make it available to users while they're interacting with other apps or the system.


1 Answers

Is there a known approach to this rather novel problem introduced by iOS 8 extensions?

Yes, it's the same approach that applies when using iCloud with Core Data: let the duplicates happen, but then go and clean them up. Both situations run the risk of creating duplicate entries, and there's no completely reliable way to prevent them. Since you have a uniqueID key, you're in good shape as far as this is concerned.

It would be a lot easier, as Dave DeLong notes, to avoid the problem in the first place. If that's impossible, you can deal with it, with some extra work.

Finding duplicates would be something like:

NSError *error = nil;
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] init];
[moc setPersistentStoreCoordinator:self.persistentStoreCoordinator];

NSFetchRequest *fr = [[NSFetchRequest alloc] initWithEntityName:@"MyEntityName"];
[fr setIncludesPendingChanges:NO];

NSExpression *countExpr = [NSExpression expressionWithFormat:@"count:(uniqueID)"];
NSExpressionDescription *countExprDesc = [[NSExpressionDescription alloc] init];
[countExprDesc setName:@"count"];
[countExprDesc setExpression:countExpr];
[countExprDesc setExpressionResultType:NSInteger64AttributeType];

NSAttributeDescription *uniqueIDAttr = [[[[[_psc managedObjectModel] entitiesByName] objectForKey:@"MyEntityName"] propertiesByName] objectForKey:@"uniqueID"];
[fr setPropertiesToFetch:[NSArray arrayWithObjects:uniqueIDAttr, countExprDesc, nil]];
[fr setPropertiesToGroupBy:[NSArray arrayWithObject:uniqueIDAttr]];

[fr setResultType:NSDictionaryResultType];

NSArray *countDictionaries = [moc executeFetchRequest:fr error:&error];

This is pretty much the Core Data equivalent of something like this in SQL:

SELECT uniqueID, COUNT(uniqueID) FROM MyEntityName GROUP BY uniqueID;

You get an array of dictionaries, each of which contains a uniqueID and a count of the number of times that value is used. Run through the dictionary and deal with duplicates appropriately.

I described this in more detail in a blog post. There's also a sample project from Apple that demonstrates the process, called SharedCoreData, but I believe it's only available as part of the WWDC 2012 sample code bundle. It was also described in session 227 at that conference.

like image 170
Tom Harrington Avatar answered Oct 22 '22 15:10

Tom Harrington