0

I have an app that uses Core Data to manage Entities that we call lists. Each list has an offlineData property which is of the type Binary Data. The Allows External Storage option is true for this.

This entity holds an array of custom objects using the following method:

NSArray *customObjects;// My custom objects that have been added
self.list.offlineData = [NSKeyedArchiver archivedDataWithRootObject:customObjects];

This will save the objects to a Core Data blob since all the objects are are using initWithEncoder: and encodeWithCoder:.

When users then want to view the list we do the opposite:

NSArray *items = [NSKeyedUnarchiver unarchiveObjectWithData:self.list.offlineData];

The Problem

This seemed like a great solution at first (obviously stupid now) because we could easily add additional properties to the objects, etc. However what we didn't anticipate is users adding thousands of objects. This has caused:

  • Memory problems and crashes when trying to archive or create an array of thousands of objects
  • Memory problems when unarchiving the objects
  • Depending on device speed unarchiving can take a very long time, leaving the user to sit and wait
  • Limitations in the amount of objects the users can add to a list.
  • Speed issues when searching objects since I can't do a fetch request.

Solution?

The solution I am thinking of would be to migrate all the custom objects to Core Data entities. This would be a To Many relationship between the list entity and the object entity.

This seems like it would solve the problems outlined above, but I am not 100% sure.

Does anyone have advice on this, and if this is the best way to "migrate"?

Nic Hubbard
  • 41,587
  • 63
  • 251
  • 412
  • You are not describing your use-case, do you fetch thousands of objects simultaniously or do you set a fetch limit? Do you need to fetch the actual property for the BLOB or can you fetch the entities without the property and only fire the property when it actually needs to be used etc.? –  Jan 25 '17 at 22:52
  • @Sneak I load a `UITableView` that shows all the `list` entities that have the `offlineData` property. When a user taps one of the rows that data is unarchived on a background thread, once that is done the UI is shown to the user. I could fetch the list entities without the blob (maybe?) since I don't need it for the listing, but I do need it when users tap the row. – Nic Hubbard Jan 25 '17 at 22:55
  • Check my answer. :) –  Jan 25 '17 at 23:01

1 Answers1

2

Migrating to a new entity as you describe should solve your problem nicely. In your new data model version, you'd create the new list item entity, delete the existing list property, and create the to-many relationship you describe.

You can't rely on automatic migration for something like this though-- you'll need to customize the process. I described the process in a different answer that mostly applies to you. You'll have to create a mapping model and a custom subclass of NSEntityMigrationPolicy. It's not complex but it's unfamiliar to most iOS developers.

Unlike that other answer, you'll need to implement the createDestinationInstances(forSource:, in:, manager:) method in your NSEntityMigrationPolicy subclass. For instances of your entity, this is where you'll create a bunch of list item objects. Unarchive the existing data blob and create new instances.

Keep in mind that this means you'll have to unarchive all of the existing list blobs. It might take a while to migrate for the scenario you describe. But you only have to do it once per user. Make sure to show some informative information on the screen so people know what's going on, and make sure to test the migration with a list that would be large enough to be a problem.

Apple's docs on custom migrations will also probably come in handy.

Community
  • 1
  • 1
Tom Harrington
  • 69,312
  • 10
  • 146
  • 170
  • Where in this process would I be able to show a UI to the user to let them know what is happening? – Nic Hubbard Jan 26 '17 at 20:31
  • You'd have to display it before loading Core Data and hide it when loading finishes, i.e. before and after the steps describe above. One approach: Before loading Core Data set a timer for something like 1 second. When the timer fires, show the new UI. When Core Data finishes loading, hide the UI if it's showing or stop the timer if it's not showing. – Tom Harrington Jan 26 '17 at 20:38
  • I don't think I need to create a new entity to map to though. My current list entity is all I need, then I would be adding new core data entities as relations. Should I still use this method? – Nic Hubbard Jan 26 '17 at 20:39
  • Your question sounded to me like you did not already have an entity that would hold a single list item, and since you didn't include details of the data model it's still hard to be certain. If you do have an entity that can represent a single list item, use that. If you don't have such an entity, you need a new one. – Tom Harrington Jan 26 '17 at 21:01
  • Yes, I do already have an entity that I can use. However it would be nice to do all the migration at app startup and never have to do it again. Could I still use the method you described but not actually migrate from one entity to another? – Nic Hubbard Jan 26 '17 at 21:38
  • 1
    Yes, you could just migrate your existing binary blob to new instances of the existing entity. – Tom Harrington Jan 26 '17 at 21:52
  • After I add the mapping model I assume it will take some testing. How do I do this since I assume it would only use the mapping file on the first time I run my app since it would think the upgrade is complete? – Nic Hubbard Jan 26 '17 at 23:33
  • 1
    You'd have to reinstall the current version of the app, create some test data, and then install the new version. – Tom Harrington Jan 27 '17 at 03:55