Core Data is definitely one of the most essential Apple frameworks in the iOS ecosystem. Core Data provides persistence, meaning it can save data outside the app's memory, and the data you saved can be retrieved after you restart your app.
Let's recap the most important aspects about Core Data. The Core Data stack consists of:
NSManagedObject
- represents our model objects, called entitiesNSPersistentStoreCoordinator
- manages the databaseNSManagedObjectContext
- lets us create, edit, delete or retrieve entitiesIn Core Data language, a stored object is an instance of NSManagedObject
, and from iOS 13, NSManagedObject
conforms to the ObservableObject protocol so that it can be observed directly by a SwiftUI's view.
Also, NSManagedObjectContext
is injected into the environment of the View's hierarchy so that the SwiftUI's View can access it to read and change its managed objects.
A very common feature of Core Data is that you can fetch the objects from the repository. For this purpose, SwiftUI provides the @FetchRequest
property wrapper, which can be used in a view to load the data.
Let's add a Core Data model called Movie
. Select File > New > File ... > Data model. Enter Movie.xcdatamodeld.
Select the Movie.xcdatamodeld
file and do the following:
Movie
title
and year
Setting Codegen to Class Definition means that Xcode creates a convenience class that will manage the entities; in our case, it creates a Movie
class. To simplify this interaction, let's add a static function that will create a full Movie
object:
import CoreData extension Movie { static func insert(in context: NSManagedObjectContext, title: String, year: Int) { let item = Movie(context: context) item.title = title item.year = year } static func addMovies(to managedObjectContext: NSManagedObjectContext) { guard UserDefaults.standard.bool(forKey: "alreadyRun") == false else { return } UserDefaults.standard.set(true, forKey: "alreadyRun") [("The Shawshank Redemption", 1994), ("The Godfather ", 1972), ("The Dark Knight", 2008)] .forEach { (title, year) in Movie.insert(in: managedObjectContext, title: title, year: year) } try? managedObjectContext.save() } }
Next, create PersistenceController
import Foundation import CoreData struct PersistenceController { static let shared = PersistenceController() let container: NSPersistentContainer init(inMemory: Bool = false) { container = NSPersistentContainer(name: "Movie") if inMemory { container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") } container.loadPersistentStores(completionHandler: {(storeDescription, error)in if let error = error as NSError?{ fatalError("Unresolved error \(error), \(error.userInfo)") } }) } func save () { guard container.viewContext.hasChanges else { return } do { try container.viewContext.save() } catch { print(error) } } }
Instantiate PersistenceController
in the App
class
import SwiftUI @main struct SwiftUISandboxApp: App { @Environment(\.scenePhase) private var scenePhase let persistenceController = PersistenceController.shared var body: some Scene { WindowGroup { ContentView().environment(\.managedObjectContext, persistenceController.container.viewContext) } .onChange(of: scenePhase) { phase in switch phase { case .active: Movie.addMovies(to: persistenceController.container.viewContext) case .background: persistenceController.save() case .inactive: break @unknown default: break } } } }
Now, let's move on to the ContentView class
import SwiftUI struct ContentView: View { @FetchRequest( sortDescriptors: [ NSSortDescriptor(keyPath: \Movie.year, ascending: true), NSSortDescriptor(keyPath: \Movie.title, ascending: true), ] ) var movies: FetchedResults<Movie> @State private var isAddPresented = false @Environment(\.managedObjectContext) private var managedObjectContext var body: some View { NavigationView { List{ ForEach(movies, id: \.self) { movie in HStack { Text(movie.title ?? "-") Spacer() Text(String(movie.year)) } }.onDelete(perform: deleteItems) } //.listStyle(PlainListStyle()) .navigationBarTitle("Movies", displayMode: .inline) .navigationBarItems(trailing: Button { isAddPresented.toggle() } label: { Image(systemName: "plus") .font(.headline) }) .sheet(isPresented: $isAddPresented) { AddNewMovie(isAddPresented: $isAddPresented) .environment(\.managedObjectContext, managedObjectContext) } } } private func deleteItems(offsets: IndexSet) { offsets.map { movies[$0] }.forEach(managedObjectContext.delete) do { try managedObjectContext.save() } catch { print(error) } } } struct AddNewMovie: View { @Environment(\.managedObjectContext) private var managedObjectContext @Binding var isAddPresented: Bool @State var title = "" @State var year = "" var body: some View { NavigationView { VStack(spacing: 16) { TextField("Title", text: $title) TextField("Year", text: $year).keyboardType(.phonePad) Spacer() } .padding(16) .navigationBarTitle(Text("Add a new movie"), displayMode: .inline) .navigationBarItems(trailing: Button(action: saveMovie) { Image(systemName: "checkmark") .font(.headline) } .disabled(isDisabled)) } } private var isDisabled: Bool { title.isEmpty || year.isEmpty } private func saveMovie() { Movie.insert(in: managedObjectContext, title: title, year: Int16(year)!) guard managedObjectContext.hasChanges else { return } do { try managedObjectContext.save() } catch { print(error) } isAddPresented.toggle() } }
All the magic of our SwiftUI Core Data integration resides in the @FetchRequest
property wrapper, where we set our SortDescriptors
and they magically populate the movies
property. @FetchRequest
retrieves an object from managedObjectContext
and expects it in @Environment(\.managedObjectContext)
.