An Introduction to CoreData
CoreData is the data persistence framework for iOS native development. CoreData can be used for saving application data offline, caching temporary data and sync data across multiple devices using iCloud.
iOS applications use SQLite as a database. If you wish to manipulate the database with raw SQL, look into GRDB. CoreData abstracts away the SQL layer and makes it easy to save data from Swift.
I say easy, but it seems very difficult at the beginning. At least, that was how it was for me.
In this post, let’s talk about what is happening when you use CoreData and how to effectively incorporate CoreData into your development workflow.
Using CoreData
We start with creating a CoreData model. This can be done with simply Cmd+N and then selecting Core Data Model. This creates a .xcdatamodeld
file, from which we can create entities. These entities include data on what’s in your database and their types. For example, a restaurant entity could have ‘Location’, ‘Contact Number’ etc. We can also define releationships between different entities.
Subsequently, we incorporate CoreData into our application code. This requires the setup of the CoreData Stack. This requires the instantiation of the NSManagedObjectModel
- which represents our application’s model file describing types, properties and relationships (we just defined this). It also requires the instantiation of NSManagedObjectContext
. This tracks changes to instances of our app’s types. NSPersistentStoreCoordinator
saves and fetches instances of our app’s types from stores. And lastly, NSPersistentContainer
sets up the model, context and store coordinator all at once.
How does this fit into our code? We first initialise the NSPersistentContainer
.
I do this in our application root - so RestaurantApp
for example.
// credits to apple's tutorial
class CoreDataStack: ObservableObject {
static let shared = CoreDataStack()
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "RestaurantModel") // this is the name of the entity you defined!
container.loadPersistentStores { _, error in
if let error {
fatalError("failed to load persistent stores: \(error.localizedDescription)")
}
}
return container
}()
private init() {}
}
Once this is instantiated, we have a persistent container that holds references to the model, context and store coordinator instances in managedobjectModel
, viewContext
and persistentStoreCoordinator
properties respectively.
Now, we can inject the managed object context (MOC).
In the same root file, we can do this.
// credits to apple's tutorial
struct RestaurantApp: App {
@StateObject private var coreDataStack = CoreDataStack.shared
var body: some Scene {
WindowGroup {
ContentView()
// injection of MOC
.environment(\.managedObjectContext, coreDataStack.persistentContainer.viewContext)
}
}
}
We can then use the environment property wrapper to access the MOC in views.
// credits to apple's tutorial
struct ContentView: View {
// Reference to MOC in environment
@Environment(\.managedObjectContext) private var viewContext
Now, let’s start adding functionality. We would want to save changes and delete objects in the store.
extension CoreDataStack{
func save() {
guard persistentContainer.viewContext.hasChanges else { return }
do {
try persistentContainer.viewContext.save()
} catch {
print("failed to save context:" error.localizedDescription)
}
}
func delete(item: RestaurantItem) {
persistentContainer.viewContext.delete(item)
save()
}
}
We can use Fetch Requests to retrieve data from our database. This can be done with @FetchRequest
. For more information, check out Apple’s documentation on CoreData.
Debugging
Go to XCode’s ‘Product’ tab. From there, click Scheme. Edit your scheme to add some launch arguments. Add these - ‘-com.apple.CoreData.ConcurrencyDebug 1’. This helps to spot concurrency errors and highlights where they originate. Very helpful. Also you might want to add ‘-com.apple.CoreData.SQLDebug 1’. This helps to spot what’s happening under the hood with SQLite in the console during simulation.
Preview and Simulation often have different results - I suspect it’s to do with how both are in different data stores. You can only see logs from SQLite in simulation. So I encourage you to use simulation to understand what is happening with CoreData to better debug.