Using Core Data with SwiftUI iOS 26.05.2021

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 entities
  • NSPersistentStoreCoordinator - manages the database
  • NSManagedObjectContext - lets us create, edit, delete or retrieve entities

In 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:

  1. Add a new entity called Movie
  2. Add one string and one integer attributes called title and year
  3. Check that Codegen is set to Class Definition in Data Model inspector

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).