Building a Fully Dynamic, Type-Safe Core Data + CloudKit Stack in Swift
As iOS developers, we all know the pain of managing Core Data. Every new project starts with repetitive boilerplate:
I kept asking myself: there has to be a better way.
Recently, I built a reusable solution that I want to share.
The Idea
I wanted to:
Goal: A reusable, SOLID-compliant data layer for any entity, without repetitive boilerplate.
Flow Diagram
Swift Structs → Dynamic Core Data Stack → Type-Safe Wrapper → CloudKit Sync → App Data
Step 1: Define Entities in Swift
struct AttributeDefinition {
let name: String
let type: NSAttributeType
let isOptional: Bool
}
struct EntityDefinition {
let name: String
let attributes: [AttributeDefinition]
}
Now I can create any entity dynamically without touching .xcdatamodeld.
Step 2: Build a Dynamic Core Data Stack
Here’s the heart of the solution:
final class DynamicCloudDataStack {
private let container: NSPersistentCloudKitContainer
init(entities: [EntityDefinition], cloudContainerId: String) {
let model = NSManagedObjectModel()
model.entities = entities.map { entityDef in
let entity = NSEntityDescription()
entity.name = entityDef.name
entity.managedObjectClassName = NSManagedObject.self.description()
entity.properties = entityDef.attributes.map { attr in
let attribute = NSAttributeDescription()
attribute.name = attr.name
attribute.attributeType = attr.type
attribute.isOptional = attr.isOptional
return attribute
}
return entity
}
container = NSPersistentCloudKitContainer(name: "DynamicStack", managedObjectModel: model)
container.persistentStoreDescriptions.first?.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: cloudContainerId)
container.loadPersistentStores { storeDescription, error in
if let error = error {
fatalError("Failed to load store: \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
func create(entityName: String) -> NSManagedObject? {
guard let entity = container.managedObjectModel.entitiesByName[entityName] else { return nil }
return NSManagedObject(entity: entity, insertInto: container.viewContext)
}
func fetch(entityName: String, predicate: NSPredicate? = nil) throws -> [NSManagedObject] {
let request = NSFetchRequest<NSManagedObject>(entityName: entityName)
request.predicate = predicate
return try container.viewContext.fetch(request)
}
func save() throws {
if container.viewContext.hasChanges {
try container.viewContext.save()
}
}
}
Diagram of Core Data Stack
[Dynamic Entity Struct] → [NSManagedObject] → [NSPersistentCloudKitContainer] → [CloudKit Sync]
Step 3: Type-Safe Swift Wrapper
No more setValue(forKey:):
@propertyWrapper
struct CoreDataField<Value> {
let key: String
let object: NSManagedObject
var wrappedValue: Value {
get { object.value(forKey: key) as! Value }
set { object.setValue(newValue, forKey: key) }
}
}
Usage:
let stack = DynamicCloudDataStack(
entities: [
EntityDefinition(name: "User",
attributes: [
AttributeDefinition(name: "username", type: .stringAttributeType, isOptional: false),
AttributeDefinition(name: "age", type: .integer16AttributeType, isOptional: false)
])
],
cloudContainerId: "iCloud.com.yourapp.container"
)
if let user = stack.create(entityName: "User") {
user.setValue("Alice", forKey: "username")
user.setValue(25, forKey: "age")
try? stack.save()
}
Step 4: CloudKit Sync
With NSPersistentCloudKitContainer:
Basically, once your stack is configured, CloudKit just works.
Step 5: Reusable & SOLID
You can reuse this stack for any entity, anywhere:
struct ProductAttributes {
static let name = AttributeDefinition(name: "name", type: .stringAttributeType, isOptional: false)
static let price = AttributeDefinition(name: "price", type: .doubleAttributeType, isOptional: false)
}
let productStack = DynamicCloudDataStack(
entities: [EntityDefinition(name: "Product", attributes: [ProductAttributes.name, ProductAttributes.price])],
cloudContainerId: "iCloud.com.yourapp.container"
)
✅ Fully dynamic ✅ Type-safe ✅ CloudKit-ready ✅ SOLID-compliant
Takeaways
This approach lets me iterate quickly, test new entities on the fly, and focus on features—not boilerplate.