Skip to content

A Swift Package for creating Core Data models programmatically

Notifications You must be signed in to change notification settings

anconaesselmann/ProgrammaticCoreData

Repository files navigation

ProgrammaticCoreData

ProgrammaticCoreData is a declarative library for iOS and macOS applications for creating CoreData's NSManagedObjectModels programmatically.

Why might I want to create my Core Data models programmatically (and with ProgrammaticCoreData):

  • CoreData models that were created with Xcode's editor are a pain to ship in packages.
  • When linking multiple core data stores certain CoreData features are limited or unavailable. Relationships across data stores for example are not supported (fetched properties can be used instead but have to be updated manually). Programmatic data models can be combined, resulting in one store and the ability to use relationships.
  • Working with CoreData from swift can be annoying. Optionality of properties is one such pain point. There are good practices one can follow for "swiftifying" CoreData but maintaining Entities manually while keeping the data model generated in Xcode up to date introduces the possibility of making mistakes. When changing a property in either place the other has to be updated manually. Creating entity descriptions programmatically allows for the Entity and the Entity Description to live in the same file which makes spotting issues a bit easier. In addition, ProgrammaticCoreData makes use of KeyPaths which allows us to utilize the compiler for spotting many type mismatches.
  • Xcode's CoreData models are stored as XML files. Reviewing changes can be difficult. A programmatic CoreData model can make reviews simpler.
  • Data models created with ProgrammaticCoreData are declarative.
  • Apple's APIs for creating CoreData models programmatically relies heavily on Strings. ProgrammaticCoreData replaces stringy API's with ones utilizing KeyPaths.

Reasons for not creating your data models programmatically:

  • Migrations. A humorous writeup chronicles the pains of how to get migrations to work with programmatically created data models. There is very little public documentation for migrating CoreData models that were not created with Xcode's CoreData model editor. It is possible though.

Author

Axel Ancona Esselmann, axel@anconaesselmann.com

License

ProgrammaticCoreData is available under the MIT license. See the LICENSE file for more info.

Installing ProgrammaticCoreData

ProgrammaticCoreData supports Swift Package Manager.

Swift Package Manager

  1. In Xcode, select “File” → “Add Packages...”
  2. Enter https://github.com/anconaesselmann/ProgrammaticCoreData.git

or add the following dependency to your Package.swift:

.package(url: "https://github.com/anconaesselmann/ProgrammaticCoreData.git", from: "0.0.4")

Getting started

The example projects are a good starting point

Example_01

Example_01 is a simple notes application without any relationships between entities:

We create our Note entity programmatically:

@objc(Note)
final class Note: NSManagedObject, Identifiable {
    @NSManaged var id: UUID
    @NSManaged var text: String
    @NSManaged var created: Date
}

Extending Note to conform to SelfDescribingCoreDataEntity will give us a declarative representation of Note's entity description:

extension Note: SelfDescribingCoreDataEntity {
    static var entityDescription = Note.description(
        .uuid(\.id),
        .string(\.text),
        .date(\.created)
    )
}

We can now build our data model and create a container:

let container = try await NSManagedObjectModel(Note.self)
    .createContainer(name: "Notes", location: .local)

See NotesManager for basic CRUD operations

Example_02

Example_02 is a Book Archive app with a to-many relationship from an Author to their Books and an inverse to-one relationship from a Book and it's Author:

We create an Author entity programmatically:

@objc(Author)
final class Author: NSManagedObject, Identifiable {
    @NSManaged var id: UUID
    @NSManaged var name: String
    @NSManaged var _books: NSOrderedSet

    // CoreData requires an `NSSet` or `NSOrderedSet` for relationships. In this example we
    // use `_` to "hide" the inner none-type-safe objective-c workings and we only 
    // expose a read-only Swift array of `Book`s. We add to `_books` with the auto-generated
    // methods below.
    var books: Array<Book> {
        get { _books.array as! Array<Book> }
    }

    @objc(add_booksObject:)
    @NSManaged func addToBooks(_ value: Book)

    @objc(remove_booksObject:)
    @NSManaged func removeFromBooks(_ value: Book)
}

We create a Book entity programmatically:

@objc(Book)
final class Book: NSManagedObject, Identifiable {
    @NSManaged var id: UUID
    @NSManaged var title: String
    @NSManaged var author: Author
}

Extending Author to conform to SelfDescribingCoreDataEntity will give us a declarative representation of Author's entity description. We also set up the to-many relationship with the Book entity:

extension Author: SelfDescribingCoreDataEntity {
    static var entityDescription = Author.description(
        .uuid(\.id),
        .string(\.name),
        .relationship(\._books, .init(
            inverse: \Book.author,
            deleteRule: .cascadeDeleteRule,
            relationshipType: .toMany(isOrdered: true)
        ))
    )
}

Extending Book to conform to SelfDescribingCoreDataEntity will give us a declarative representation of Book's entity description. We also set up the to-one relationship to the Book's Author entity:

extension Book: SelfDescribingCoreDataEntity {
    static var entityDescription = Book.description(
        .uuid(\.id),
        .string(\.title),
        .relationship(\.author, .init(
            inverse: \Author._books,
            deleteRule: .nullifyDeleteRule,
            relationshipType: .toOne
        ))
    )
}

Note that it should be impossible to create a book without adding the book to it's authors books property, since we have an inverse relationship. We can achieve this by marking all but our own Book initializer as unavailable:

@objc(Book)
final class Book: NSManagedObject, Identifiable {

    @NSManaged var id: UUID
    @NSManaged var title: String
    @NSManaged var author: Author
    
    // The only available initializer initializes all none-optional properties.
    // Below we mark all other initializers as unavailable. The compiler can now
    // enforce that we never instantiate an instance without setting all properties.
    init(
        context: NSManagedObjectContext,
        id: UUID,
        title: String,
        author: Author
    ) {
        super.init(entity: Book.entityDescription, insertInto: context)
        self.id = id
        self.title = title
        self.author = author
        author.addToBooks(self)
    }
    
    // MARK: - Unavailable initializers
    @available(*, unavailable)
    override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
        super.init(entity: entity, insertInto: context)
    }

    @available(*, unavailable)
    init(context: NSManagedObjectContext) {
        fatalError()
    }

    @available(*, unavailable)
    init() {
        fatalError()
    }
}

We can now build our data model and create a container:

let container = try await NSManagedObjectModel(
    Author.self,
    Book.self
).createContainer(name: "Books", location: .local)

See AuthorsManager or BooksManager for simple CRUD operations.

If we didn't care about the order of books on our Author entity we would declare a books Set instead of an Array:

@objc(Author)
final class Author: NSManagedObject, Identifiable {
    // ...
    @NSManaged var _books: NSSet

    var books: Set<Book> {
        get { _books as! Set<Book> }
    }
    // ...

In the Author's extension to conform to SelfDescribingCoreDataEntity we would set isOrdered to false or simply write .toMany:

extension Author: SelfDescribingCoreDataEntity {
    static var entityDescription = Author.description(
        // ...
        .relationship(\._books, .init(
            inverse: \Book.author,
            deleteRule: .cascadeDeleteRule,
            relationshipType: .toMany
        ))
        // ...
    )
}

Limitations

  • Entities in ProgrammaticCoreData can have a maximum of 25 attributes

About

A Swift Package for creating Core Data models programmatically

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages