ProgrammaticCoreData is a declarative library for iOS and macOS applications for creating CoreData's NSManagedObjectModel
s programmatically.
- 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
KeyPath
s 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
String
s. ProgrammaticCoreData replaces stringy API's with ones utilizingKeyPath
s.
- 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.
Axel Ancona Esselmann, axel@anconaesselmann.com
ProgrammaticCoreData is available under the MIT license. See the LICENSE file for more info.
ProgrammaticCoreData supports Swift Package Manager.
- In Xcode, select “File” → “Add Packages...”
- 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")
The example projects are a good starting point
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 is a Book Archive app with a to-many relationship from an Author
to their Book
s 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
))
// ...
)
}
- Entities in
ProgrammaticCoreData
can have a maximum of 25 attributes