20

One of my current core data entities - Entity1 - has a Boolean attribute called isSaved.

In the new core data model, I am planning to remove isSaved attribute and add a new Int attribute called type. And for all saved Entity1 objects, I'd like to set the value of type according to the value of isSaved in old core data model. (e.g. if isSaved is true, then type is 1, else type is 2).

I've read some articles about light weight core data migration, but none of them seems helpful.

Just wondering if there is any way that can make my planned migration work?

rmaddy
  • 314,917
  • 42
  • 532
  • 579
Shawn
  • 311
  • 2
  • 9

2 Answers2

72

Lightweight migration can't do this. You'll have to create a mapping model and a subclass of NSEntityMigrationPolicy. It's not difficult but it's unfamiliar territory for most iOS developers. The steps run like this:

  1. Create the mapping model. In Xcode, File --> New --> Mapping Model. When you click "Next", Xcode will ask for the source (old) and destination (new) model files for this mapping.
  2. The model file will infer mappings where possible. Everything else will be blank. With your type and some other properties, it'll look something like the following. Entries like $source.timestamp mean to copy the existing value from before the migration.

Initial mapping

  1. Create a new subclass of NSEntityMigrationPolicy. Give the subclass an obvious name like ModelMigration1to2. This class will tell Core Data how to map the old boolean value to the new integer value.

  2. Add a method to the subclass to convert the value. Something like the following. The method name doesn't matter but it's good if you choose something descriptive. You need to use ObjC types here-- e.g. NSNumber instead of Int and Bool.

     @objc func typeFor(isSaved:NSNumber) -> NSNumber {
         if isSaved.boolValue {
             return NSNumber(integerLiteral: 1)
         } else {
             return NSNumber(integerLiteral: 2)
         }
     }
    
  3. Go back to the mapping model and tell it to use your subclass as its custom mapping policy. That's in the inspector on the right under "custom policy". Be sure to include the module name and class name.

Custom Policy

  1. Tell the mapping model to use that function you created earlier to get values for the type property from the old isSaved property. The following says to call a function on the custom policy class named typeForIsSaved: (the : is important) with one argument, and that the argument should be the isSaved value on $source (the old managed object).

Custom attribute mapping

Migration should now work. You don't have to tell Core Data to use the mapping model-- it'll figure out that migration is needed and look for a model that matches the old and new model versions.

A couple of notes:

  • If you crash with an error that's something like Couldn't create mapping policy for class named... then you forgot the module name above in step 5 (or got it wrong).
  • If you get a crash with an unrecognized selector error then the method signature in step 4 doesn't match what you entered in step 6. This can also happen if you forget to include @objc in the function declaration.
Tom Harrington
  • 69,312
  • 10
  • 146
  • 170
  • 1
    Thank you so much for such a detailed answer, Tom. Just tried it and works like a charm!! Except for the method signature in step 6 needs to be changed to "typeFor:". It'd be good if you can update image for step 6. – Shawn Nov 18 '16 at 05:15
  • The screenshot and method signature are what I used in a working test project. If I set a breakpoint in the method, I can verify that it gets called during migration. What problem did you have, and what did you do to fix it? – Tom Harrington Nov 18 '16 at 17:42
  • I changed it from "typeForIsSaved:" to "typeFor:", otherwise I get unrecognized selector error. BTW, I'm using Xcode 7.3. – Shawn Nov 21 '16 at 02:09
  • That's it then, because I'm using Xcode 8 and Swift 3. The transition to Swift 3 has many unexpected bumps in the road... – Tom Harrington Nov 21 '16 at 17:16
  • 1
    I implemented this, then tested the migration from an older database that was not on the version immediately prior to the new one. So, I was migrating from schema v25 to v26, but the core data store say was on v24 when the new build was launched. The model has been migrated to v26, new properties are present for instance, but the custom migration did not occur. I'm now reading Core Data does not support progressive migration out the box, rather attempts to infer mapping model, ignoring this manual mapping. Adding progressive migration seems complex. Am I reading the situation correctly? – Duncan Babbage May 05 '17 at 11:08
  • 2
    "typeForIsSaved" nor "typeFor" seems to work anymore. According to [this answer](https://stackoverflow.com/questions/44638144/what-is-the-selector-syntax-for-core-data-custom-policies) it should be "typeForWithIsSaved" – SeNeO Jun 20 '17 at 20:51
  • Best answer on the internet. Thank you for the detailed answer – Josh O'Connor Aug 03 '17 at 01:28
  • @TomHarrington Is there a way to get isSaved property in objective-c/swift code – Rahul Dec 05 '17 at 17:12
  • If I want to update only value not attribute then what would be my policy? – Muhammad Hasan Irshad May 30 '21 at 00:19
  • @MuhammadHasanIrshad I don't understand your question. The answer above shows how to update a value. – Tom Harrington May 30 '21 at 23:35
  • @TomHarrington Yes but also there is change of attribute as well Is Saved to TypeFor. I want to just update isSaved value. – Muhammad Hasan Irshad May 31 '21 at 08:08
  • For me it worked without the "With" keyword in the method name. – Nico S. Jul 20 '21 at 15:37
  • Great example. Thank you. :) – Kraig Wastlund Dec 03 '21 at 00:24
  • 1
    @MuhammadHasanIrshad if I understand what you are requesting, this is not, in my experience possible. I had a similar requirement in that I was recording the wrong value into an attribute, it was a simple matter to recalculate the correct value; however, I assume, as I was not changing the model (version a and version b were identical) Core Data did not initiate a migration. It would be nice to do this sort of bulk data change as a migration. Perhaps it is possible to trigger a manual migration? In the end I renamed the offending attribute, but for other reasons the migration fails :) – Gavin May 12 '22 at 09:36
12

Using Xcode 9.1 Beta with Swift 4, I find migration works but you have to be careful how you specify the transform method name, also it seems you need to mark your functions as @objc.

For example, my Value Expression:

FUNCTION($entityPolicy, "changeDataForData:" , $source.name)

My transformation policy method name:

class StudentTransformationPolicy: NSEntityMigrationPolicy {
  @objc func changeData(forData: Data) -> String {
      return String(data: forData, encoding: .utf8)!
  }
}

Definitely tricky and took a lot of experimenting before I got it to trigger when launching my app after model changes. It might be easier to implement "createDestinationInstances" for your policy if all of this doesn't work, but we'll leave that for another day...

noobular
  • 3,257
  • 2
  • 24
  • 19
  • you can make this even a bit nicer by changing the signature to @objc func changeData(forData data: Data) -> String { return String(data: data, encoding: .utf8)! } – mhoeller Apr 14 '18 at 08:26