Slate is a Swift framework that provides immutable data models for Core Data, enabling safe and efficient access to your application's data with thread safety guarantees. It sits on top of Core Data's object graph and provides a clean, type-safe interface for querying and mutating data.
Slate addresses common Core Data challenges by offering:
- Single-writer/multi-reader transactional access to the object graph
- Immutable data models with a clean query DSL that ensures thread safety and prevents accidental mutations
Slate automatically generates immutable representations of your Core Data entities, providing:
- Thread-safe access to data models
- Protection against accidental mutations outside of mutation contexts
- Clean separation between read and write operations
All immutable models in Slate conform to Sendable, making them safe for concurrent access across different threads. This eliminates the need for manual synchronization when passing data between background and main threads.
Slate implements a single-writer/multi-reader pattern that ensures:
- Mutations occur in isolated barriers, preventing race conditions
- Queries always operate on consistent snapshots of the data model
- Safe concurrent access to read operations
Slate provides Combine publisher support for streaming NSFetchedResultsController updates, enabling reactive UI updates that respond to data changes in real-time.
- Slate Instance: The central management context for all operations on a NSPersistentStore
- Core Data Integration: Works directly with Core Data's object graph and managed objects
- Immutable Model Generation: Automatically generates immutable representations of your Core Data entities
- Query Contexts: Thread-local contexts for safe read operations with snapshot consistency
- Mutation Contexts: Single-writer barrier operations that ensure data integrity
- Data Model Definition: Define your Core Data entities in
.xcdatamodelfiles - Code Generation: Use
slategento generate both Core Data managed objects and immutable Slate models - Runtime Usage:
- Use
slate.query()for read operations that return immutable models - Use
slate.mutate()for write operations that modify the Core Data store
- Use
- Thread Safety: Immutable models can be safely shared across threads without synchronization
Slate generates immutable representations of your Core Data entities that:
- Cannot be modified after creation
- Provide thread-safe access patterns
- Are automatically cached for performance
- Support the
Sendableprotocol
Slate provides a fluent API for querying data:
let books = try await slate.query { context in
return try context[Book.self]
.where(\.pageCount, .greaterThan(100))
.sort(\.title)
.fetch()
}Mutations are performed in barrier operations:
try await slate.mutate { writeContext in
if let book = try writeContext[CoreDataBook.self]
.where(\.id, .equals(bookId))
.fetchOne()
{
book.pageCount = newPageCount
}
}- Swift 5.9 or later
- iOS 17+, macOS 14+, tvOS 17+, watchOS 6+
- Xcode 15 or later
Slate depends on:
- Swift Argument Parser (v1.6.1+) - for the code generation tool
- Foundation framework (built-in)
- Core Data framework (built-in)
Slate is distributed as a Swift Package. Add it to your project using Xcode's package manager or by adding the following dependency to your Package.swift:
dependencies: [
.package(url: "https://github.com/jmfieldman/Slate", from: "<latest version>")
]- Create a new Core Data model file (
New > File from Template > Core Data > Data Model) - Set the Codegen property of each Entity to
Manual/None - Configure module abstraction for logical separation of entities
- For each entity, you can specify
struct: truein the User Info dictionary to generate structs instead of classes
Use the slategen command-line tool to generate both Core Data managed objects and immutable Slate models:
$ swift run slategen gen-core-data \
--input-model <path-to-implementation-module>/SlateTests.xcdatamodel \
--output-core-data-entity-path <path-to-implementation-module>/DatabaseModels \
--output-slate-object-path <path-to-api-module>/ImmutableModels \
--cast-int \
--core-data-file-imports "Slate, ImmutableModels"- Create a
Slateinstance in your implementation module:
let slate = Slate()- Configure the persistent store:
guard
let momPath = Bundle.main.path(forResource: "YourDataModel", ofType: "mom"),
let managedObjectModel = NSManagedObjectModel(contentsOf: URL(fileURLWithPath: basePath))
else {
throw // no data model!
}
let persistentStoreDescription = NSPersistentStoreDescription()
persistentStoreDescription.type = // Choose the type and set additional parameters
slate.configure(
managedObjectModel: managedObjectModel,
persistentStoreDescription: persistentStoreDescription
) { desc, error in
if let error {
// Handle configuration errors
} else {
// Success - slate is ready to be accessed.
}
}- Query data using immutable models:
let books = try await slate.query { context in
return try context[Book.self]
.where(\.pageCount, .greaterThan(100))
.fetch()
}- Mutate data safely:
try await slate.mutate { writeContext in
if let book = try writeContext[CoreDataBook.self]
.where(\.id, .equals(bookId))
.fetchOne()
{
book.pageCount = newPageCount
}
}Slate supports reactive streaming of data changes using Combine publishers:
let streamPublisher = slate.stream { request -> SlateQueryRequest<Book> in
return request.sort(\.pageCount)
}- Thread Safety: Immutable models guarantee Sendable conformance, making them safe for concurrent access
- Snapshot Isolation: Queries operate on consistent snapshots of the data model
- Unidirectional Flow: Enforces clear separation between read and write operations
- Performance: Caching of immutable objects improves performance for repeated queries
- No Faulting: All queried objects are fully loaded, which may impact performance for large datasets
- No Dynamic Relationships: Relationships must be pre-fetched as arrays of immutable objects
Slate: Main entry point for all operationsSlateObject: Protocol that immutable models must conform toSlateQueryContext: Context for read operationsSlateTransactionError: Errors that can occur during transactions
slate.query(): Asynchronous read operations returning immutable modelsslate.mutate(): Asynchronous write operations modifying Core Dataslate.stream(): Reactive streaming of data changes
let authors = try await slate.query { context in
return try context[Author.self].fetch()
}let books = try await slate.query { context in
return try context[Book.self]
.where(\.pageCount, .greaterThan(100))
.sort(\.title)
.fetch()
}do {
try await slate.mutate { context in
let author = try context[CoreDataAuthor.self].fetchOne()
author.name = "New Name"
}
} catch {
// Handle mutation errors
}let publisher = slate.stream { request in
return request.sort(\.title)
}
publisher.sink(
receiveCompletion: { completion in
// Handle stream completion
},
receiveValue: { update in
// Update UI with new data and change indices
}
)Contributions to Slate are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch
- Make your changes with tests
- Submit a pull request
Slate is released under the MIT license. See LICENSE.txt for details.