Core Data codegen

Xcode has a number of ways of generating Core Data files from your Core Data model, but what do they actually do? A quick look at "Create NSManagedObject Subclass...", "Manual / None", "Class Definition", and "Category / Extension".

Codegen

The codegen menu sounds tempting—after all, anything that generates code must be helpful! But sometimes experimenting with these options can get confusing, and it is easy to end up with either files not being generated, or duplicate files getting generated that cause fun compilation errors.

Project and Model

Created an iOS Master-Detail project "TestCodegen", and opt to use Core Data. We're using Swift 5 and Xcode 11.

The resulting .xcdatamodeld has a single entity, Event, with a single attribute, timestamp, which is a Date.

The "Class Definition" option

This isn't the first option in the menu, but it is the default so let's start with that.

That's it. No dialogs, no files to save. Project will build and run. But where are the generated files, do they even exist? Let's look to see what was compiled:

In the 'Compile Swift source files' section it will compile Event+CoreDataClass.swift and Event+CoreDataProperties.swift files, with locations hidden away in DerivedData.

Inside Event+CoreDataClass.swift:

import Foundation
import CoreData

@objc(Event)
public class Event: NSManagedObject {
}

Inside Event+CoreDataProperties.swift:

import Foundation
import CoreData

extension Event {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Event> {
        return NSFetchRequest<Event>(entityName: "Event")
    }

    @NSManaged public var timestamp: Date?

}

They even have a helpful comment:

//  This file was automatically generated and should not be edited.

So the "Class Definition" option creates the required files, but does it behind the scenes. We have an empty class, with the properties of the entity (along with a handy fetchRequest() function) being stored in an extension to that class. This is convenient for quickly getting started with CoreData (no additional files to manually add to the project), and it means that any changes to the .xcdatamodel are automatically incorporated when building the project.

But this approach does have a couple of issues (which may or may not be drawbacks depending on your perspective)—the files aren't added to the project so they aren't in version control, and there's no obvious way of adding custom logic.

The "Manual / None" option

Our experiments with 'Class Definition' will have left the two files lurking in DerivedData, so we need to clean up:

If we hadn't done this step then we'd end up with compiler errors later on complaining that we have two files of the same name—one in our project, the other in DerivedData.

Let's create the two files manually:

A lot more steps than before (and does seem odd that the code generation command is tucked away in the Editor menu and not in the Data Model Inspector, but at least the two files have now been created and added to the project.

Event+CoreDataClass.swift:

import Foundation
import CoreData

@objc(Event)
public class Event: NSManagedObject {
}

Event+CoreDataProperties.swift:

import Foundation
import CoreData

extension Event {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Event> {
        return NSFetchRequest<Event>(entityName: "Event")
    }

    @NSManaged public var timestamp: Date?

}

These files look rather familiar!

We can add our own custom logic to +CoreDataClass.swift, but we must remember to leave +CoreDataProperties.swift alone so that any changes don't get overwritten if we re-generate the files when our model changes.

However, if we change the model the +CoreDataProperties.swift file doesn't get updated automatically. We need to perform the same 'Create NSManagedObject Subclasses...' steps, although this time only the +CoreDataProperties.swift file actually gets generated soany custom logic we've added to +CoreDataClass.swift is preserved. This is good, but it is another step that we have to remember to do every time we modify the model—if we don't remember to do so we get lovely runtime issues.

The "Category/Extension" option

Let's do another clean build (to ensure DerivedData is empty) and try the 'Category/Extension' option.

The two terms in this menu are language-specific: it'll create a category if the project is Objective-C, and an extension if the project is in Swift. Which then leads to questioning whether the "Manual / None" menu option is also language-specific? Is it "Manual" for Objective-C and "None" for Swift?

Now when we build the project we get lots of compiler errors: mostly "Use of undeclared type 'Event'".

Let's see what files were generated this time. Using the same technique as before to get the path to DerivedData we can see the single file generated for this entity, Event+CoreDataProperties.swift:

import Foundation
import CoreData

extension Event {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Event> {
        return NSFetchRequest<Event>(entityName: "Event")
    }

    @NSManaged public var timestamp: Date?

}

Our compiler errors are due to the fact that we are declaring an extension to a class that doesn't exist.

Now we could create the +CoreDataClass.swift file by following the same 'Create NSManagedObject subclass...' steps as before, but this will also create a duplicate +CoreDataProperties.swift file which we'll have to manually remove.

If we do this though. we then have the +CoreDataClass in our project, which we can amend to add custom logic, and the +CoreDataProperties file in DerivedData which will get automatically updated whenever our .xcdatamodeld is changed.

The "Class Definition and We'll Add Our Own Extension" option

No, this isn't an option in the Data Model Inspector, but it is an alternative approach when we only need to add custom logic to some entities.

We get Xcode to generate the two files in DerivedData as part of "Class Definition" and then we can manually add our own extension to entities as we need to, in something like Event+Additions.swift:

import Foundation

extension Event {
    
   func someCustomFunction() -> String? {
    ...
   }

}

This approach is quite convenient for adding custom functionality but isn't suitable if we want to add stored properties—as the compiler helpfully points out:

Extensions must not contain stored properties.

It may be possible to get around this limitation with associatedtype, but don't really want to go down that route for now.

It may also be the case that some use cases for stored properties are better handled as transient properties in the model.

And while it doesn't really annoy the compiler that much, the fact that there are now three files for the entity does seem a bit much.

The "Hybrid approach" option

Given "Class Definition" is convenient for maintaining the DerivedData files generated in the background, why not use this for every entity unless we need custom logic—in which case opt for simply adding our own extension (if we don't need stored properties) or using' Category/Extension' and then 'Create NSManagedObject subclass...' (if we do need stored properties)?

The "mogenerator" option

There is always mogenerator—which traces its roots back to eogenerator back in the WebObjects days. It follows the generation gap pattern, creating a class and subclass, also known as human and machine files, for each entity. Run from the command line following changes to the data model, it can keep the files up to date, while preserving custom logic. Templates also provide the ability to customise the code that is generated, and many of the templates include useful shortcuts for common tasks.

As a long-time user of mogenerator I'd only recently stopped using it to see how the built-in tools had progressed.

What's wrong with DerivedData?

Why would we want to keep the +CoreDataProperties in the project if we shouldn't be editing them? Just control-freakery where we want to have all the files and not rely on them being created behind the scenes? Having the +CoreDataProperties files in the project and in version control provides a good way of tracking changes on a per-entity basis; whereas changes to the Core Data model itself are a lot harder to track.

We've also had instances where a model is shared by multiple projects, and having the files in a framework which could be built and shared solved many of the build problems.

Wishlist

(This is purely what I want in Xcode—it may be completely wrong, or utterly useless for anyone else, or it may not work with whatever forthcoming things Apple have in store for Core Data and Xcode).

What would be really nice is an option in the Data Model Inspector which acknowledged the split between the two files (+CoreDataClass and +CoreDataProperties) and simply allowed the decision whether the files should be kept in 'In Project' or 'In DerivedData'.

Remove the 'Create NSManagedObject Subclasses...' command, and remove the three codegen options "Manual / None", "Class Definition", "Category / Extension". Now add a checkbox to indicate if each file should be added to the project (left unchecked the file will be created in DerivedData).

The ability to keep both files in DerivedData keeps the simplicity of the "Class Definition" option for quickly getting started with Core Data apps, and for the case when custom logic isn't required.

Opting to put the +CoreDataClass file in the project should be simple—selecting 'In Project' should add the file (preferably in a folder named after the Core Data model), where custom logic can be added.

Put the +CoreDataProperties files in a '+Properties' folder within the aforementioned Core Data model folder, and add the ability to lock the file to prevent editing, or at least show a warning if it is about to be overwritten. The +CoreDataProperties file should always be kept up to date if there are changes to the data model, regardless as to whether it is in the project or DerivedData.

Even nicer is if we add the ability to nominate a code snippet to use as the template when building each of these two files, allowing the same sort of extensibility provided by mogenerator's templates. Allow changing the template at any time—just prompt if there are going to be any destructive losses to the +CoreDataClass file.

And even nicer would be that any change to the location—particularly from "In DerivedData" to "In Project"—would either prompt for a clean build to remove the duplicate file problem, or do so silently.

With these changes it should be simpler to see where each of the two files resides; should be easier to transition from one location to the other; and should be more transparent (and extensible) through the use of code snippets as templates.