All videos. All books. One low price.

Get unlimited access to all video courses and books on this site with the new raywenderlich.com Ultimate Subscription. Plans start at just $19.99/month.

Home iOS & Swift Tutorials

Core Data with SwiftUI Tutorial: Getting Started

In this Core Data with SwiftUI tutorial, you’ll learn to persist data in an app using @State, @Environment and @FetchRequest property wrappers.

4.3/5 31 Ratings

Version

  • Swift 5, iOS 13, Xcode 11

Imagine jotting down something important in Notes, only to find your data is gone the next time you open the app! Fortunately, persistence is in excellent hands on iOS. All your notes, photos and other data are safe, thanks to Core Data. :]

There are a number of different technologies you can use when you need to store data across app launches. Core Data is the go-to solution on iOS. With impressive performance and a broad set of features, Apple’s Core Data framework manages the entire model layer of your app and handles persistence to your device’s storage disk.

In this Core Data with SwiftUI tutorial, you’ll refactor an app to add persistence and prevent the nightmare of losing your data when the app restarts. Along the way, you’ll learn to:

  • Set up Core Data in a project.
  • Use SwiftUI’s data flow to access what you need in the Core Data framework.
  • Define and create new model objects using Core Data.
  • Use Fetch Requests to retrieve objects from disk.

So buckle up and learn more about Core Data’s capabilities and how it works!

Getting Started

To start, download the project materials using the Download Materials button at the top or bottom of this tutorial. Then, open the starter project in Xcode. Then build and run.

Fave Flicks App Main Screen

Welcome to FaveFlicks, your own personal tally of your favorite movies. It’s a simple app that lets you add or delete a movie to the list. However, it has one glaring problem.

Yes, you guessed it right: The app does not persist data! This means that if you add some movies to your list, then restart the app, those movies you carefully added have gone. :[

Testing FaveFlick’s Persistence

To remove a movie from the list, swipe left and tap Delete.

Delete a row in the app

Next, tap the Plus button on the top-right to add one of your favorites.

Add a row in the app

You’ll see the Add Movie screen.

Add a new movie on the Add Movie screen

Each Movie object exists only in memory. They aren’t stored to disk, so closing the app removes your changes and reverts to my tasteful list of favorite movies.

Note: If you try to open the “add movie” screen a second time, then nothing happens. It’s a known Apple bug in SwiftUI. As a workaround, you need to update the UI in some way to add more movies. You can pull down the list to update the UI and then add more movies.

Force close the app to test its persistence. With the app in foreground, enter the fast app switcher. To do this, gently drag up from the bottom of the screen. If your device has one, double-tap the Home button to enable the fast app switcher.

Fast App Switcher on iPhone

Now, select FaveFlicks and swipe up to close the app. On the home screen, tap FaveFlicks to open it again.

Notice that your changes are gone, and the default movies are back.

Fave Flicks App Main Screen

It’s time to fix that. Begin by setting up Core Data.

Setting Up Core Data

Before you start setting up persistence, you should learn about the moving parts of Core Data, also known as the Core Data stack. The Core Data stack includes:

  • A managed object model which defines model objects, called entities, and their relationships with other entities. Think of it as your database schema. In FaveFlicks, you’ll define the Movie entity as part of the managed object model inside FaveFlicks.xcdatamodeld. You’ll use the NSManagedObjectModel class to access your managed object model in the code.
  • An NSPersistentStoreCoordinator, which manages the actual database.
  • An NSManagedObjectContext, which is an in-memory scratchpad that lets you create, edit, delete or retrieve entities. Usually, you’ll work with a managed object context when you’re interacting with Core Data.

With that out of the way, it’s time to get started!

Adding the Core Data stack

While it might seem daunting to set up the entire Core Data stack, it’s easy thanks to NSPersistentContainer. It can create everything for you. Open SceneDelegate.swift and add the following after import SwiftUI:

import CoreData

Core Data lives in its own framework, so you must import it in order to use it.

Now, add the following at the end of SceneDelegate:

// 1
lazy var persistentContainer: NSPersistentContainer = {
  // 2
  let container = NSPersistentContainer(name: "FaveFlicks")
  // 3
  container.loadPersistentStores { _, error in
    // 4
    if let error = error as NSError? {
      // You should add your own error handling code here.
      fatalError("Unresolved error \(error), \(error.userInfo)")
    }
  }
  return container
}()

Here’s what that does:

  1. Add a lazy property called persistentContainer to your SceneDelegate. The first time you reference the property, it will create an NSPersistentContainer.
  2. Create a container named FaveFlicks. You’ll see a file called FaveFlicks.xcdatamodeld if you look in the app’s list of files in the Project navigator. This file is where you will later design your Core Data model schema. The name of this file needs to match the name of the container.
  3. Instruct the container to load the persistent store, which simply sets up the Core Data stack.
  4. If an error occurs, it’s logged and the app is killed. In a real app, you should handle this by showing a dialog indicating the app is in a weird state and needs reinstalling. Any errors here should be rare and result from developer mistakes, so you’ll catch them before submitting your app to the App Store.

That’s it. That’s all you need to set up Core Data stack. Quite a journey, huh? :]

You’re going to also need a way of saving any data to disk, because Core Data does not handle that automatically. Still in SceneDelegate.swift, add the following method at the end of the class:

func saveContext() {
  // 1
  let context = persistentContainer.viewContext
  // 2
  if context.hasChanges {
    do {
      // 3
      try context.save()
    } catch {
      // 4
      // The context couldn't be saved.
      // You should add your own error handling here.
      let nserror = error as NSError
      fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
    }
  }
}

This creates a method called saveContext() which does the following:

  1. Obtain the viewContext of the persistent container. This is a special managed object context which is designated for use only on the main thread. You’ll use this one to save any unsaved data.
  2. Save only if there are changes to save.
  3. Save the context. This call may throw an error, so it’s wrapped in a try/catch.
  4. In the event of an error, it’s logged and the app killed. Just like in the previous method, any errors here should only happen during development, but should be handled appropriately in your application just in case.

Now that you have the Core Data stack set up and a method to save changes, it’s time to wire this into the rest of the app.

Now, in scene(_:willConnectTo:options:), replace let contentView = MovieList() with the following:

let context = persistentContainer.viewContext
let contentView = MovieList().environment(\.managedObjectContext, context)

This simply grabs the same viewContext you used earlier and sets it as an environment variable on the MovieList SwiftUI view. The view will use this later to add and remove movies from the Core Data store.

Now add the following method to the end of SceneDelegate:

func sceneDidEnterBackground(_ scene: UIScene) {
  saveContext()
}

This instructs the app to call the save method you previously added when the app goes into the background. This is a good time to save data to disk. Later on, you’ll see how to save more frequently.

Build and run to check that the app still works. There are no functional changes just yet!

Creating the Data Model

Phew! Now that the Core Data stack is out of the way, it’s finally time to work on the main part of the app. In Xcode, open FaveFlicks.xcdatamodel. Right now it’s empty, but you’ll declare the Movie entity below. This is where you define the schema of your data model. You’ll add the relevant entities, which are the types of objects you can create, and define relationships to indicate how the entities are connected.

Click Add Entity.

Add an Entity to the Data Model

Xcode creates a new entity in the data model, named Entity by default. Double-click the name and change it to Movie.

Rename an Entity in the Data Model

Next, click the + icon under Attributes to add a new attribute. Name it title and set the type to String.

Add the Title Attribute

Finally, add two more attributes: One named genre of type String and another named releaseDate of type Date. Once you’re done, the Movie entity’s attributes will match the following:

Completed Movie Entity

Relationships and Fetched Properties

Although FaveFlicks only has a single Movie entity, you may run into relationships and fetched properties in apps with larger data models. A relationship is the same as a relationship in any database: It lets you define relationships between two entities.

However, Fetched properties is a more advanced Core Data topic. You can think of it as computed properties that work like weak one-way relationships. For instance, if FaveFlicks had a Cinema entity, it might have a currentlyShowingMovies fetched property that would fetch Movies that are currently in movie theaters.

Note: For more information on both — and much more! — check out Core Data by Tutorials.

Removing the Old Movie Struct

Open Movie.swift. At the beginning of this tutorial, Movie struct was the model object. Core Data creates its own Movie class so you need to remove Movie.swift. Delete Movie.swift by right-clicking it in the Project navigator and selecting Delete. In the resulting dialog, click Move to Trash.

Build the app. You’ll see a couple of errors that need fixing because you’ve just removed Movie.

Errors after Deleting Movie

Note: You’ll need to be precise and delete quite a bit of code for the old Movie struct in this section, so follow closely!

First, open MovieList.swift. You’ll find the movies for the list stored in a simple movies array. At the top of MovieList, change the line declaring the movies array to an empty array like below:

@State var movies: [Movie] = []

The @State property wrapper is a vital piece of the SwiftUI data flow. The class that declares this local property owns it. If anything changes the value of movies, the view that owns it will trigger an update of the UI.

Now, delete makeMovieDefaults(), since it’s no longer in use.

In addMovie(title:genre:releaseDate:), movies are created and added to the movies array. Remove its contents and leave it as a blank method. You’ll use it to create new instances of the Movie entity in a later section.

Finally, remove the contents of deleteMovie(at:). You’ll replace it later with code that deletes Core Data entities.

Using the New Movie Entity

Now that you created a Movie entity in the data model, Xcode will auto-generate it’s own Movie class that you’ll use instead. All entities in the data model are subclasses of NSManagedObject. It’s a managed object because Core Data handles its lifecycle and persistence for you, mainly through the use of the Managed Object Context.

The old Movie struct didn’t use optional properties. But, all NSManagedObject subclasses use optional properties for their attributes. This means you’ll need to make some changes in files that use Movie.

Note: There is an exception to all properties being optional: scalar types. For example, if your entity has a Float property and Use Scalar Type is enabled, then the generated property will be a non-optional of type Float.

Using an Entity’s Attributes in a View

Now, you’ll learn to use an entity’s attributes in a view. Open MovieRow.swift. Then, replace the body property with:

var body: some View {
  VStack(alignment: .leading) {
    // 1
    movie.title.map(Text.init)
      .font(.title)
    HStack {
      // 2
      movie.genre.map(Text.init)
        .font(.caption)
      Spacer()
      // 3
      movie.releaseDate.map { Text(Self.releaseFormatter.string(from: $0)) }
        .font(.caption)
    }
  }
}

The structure of the view is exactly the same, but you’ll notice that all the movie attributes are mapped to Views.

All attributes on a Core Data entity are optional. That is to say, the title attribute is type String?, referenceDate is type Date? and so on. So, now you’ll need a method to get the optional’s value.

Inside a ViewBuilder, like MovieRows body property, you can’t add control flow statements such as if let. Each line should be either a View or nil.

The line marked with 1, 2 and 3 above are a Text view if the attributes are non-nil. Otherwise, it’s nil. It’s a handy way to deal with optionals in SwiftUI code.

Finally, build and run. You removed the old Movie struct and replaced it with a Core Data entity. As your reward, you now have an empty view rather than a list of classy films. :]

Empty Movie List

If you create a movie, nothing happens. You’ll fix this next.

Using Environment to Access Managed Object Context

Next, you’ll learn to access objects from a managed object context. Back in MovieList.swift, add the following line under the declaration of movies:

@Environment(\.managedObjectContext) var managedObjectContext

Remember earlier you set the managedObjectContext environment variable on MovieList? Well, now you’re declaring that it exists and, therefore, can access it.

@Environment is another important piece of SwiftUI data flow that lets you access global properties. When you want to pass an environment object to a view, you pass it in when creating an object.

Now add the following method to MovieList.swift:

func saveContext() {
  do {
    try managedObjectContext.save()
  } catch {
    print("Error saving managed object context: \(error)")
  }
}

When you create, update or delete entities, you do so in your managed object context — the in-memory scratchpad. To actually write the changes to disk, you must save the context. This method saves new or updated objects to the persistent store.

Next, find addMovie(title:genre:releaseDate:). The method is still blank from when you removed the old Movie, so replace it with the method below to create new Movie entities:

func addMovie(title: String, genre: String, releaseDate: Date) {
  // 1
  let newMovie = Movie(context: managedObjectContext)

  // 2
  newMovie.title = title
  newMovie.genre = genre
  newMovie.releaseDate = releaseDate

  // 3
  saveContext()
}

Here, you:

  1. Create a new Movie in your managed object context.
  2. Set all the properties of the Movie that are passed as parameters into addMovie(title:genre:releaseDate:).
  3. Save the managed object context.

Build and run and create a new movie. You’ll notice a blank list.

Empty Movie List

That’s because you’re creating Movies, but you’re not retrieving them to display in the list. In the next section, you’ll fix this, and you’ll finally see movies in the app again.

Fetching Objects

Now you’ll learn to display the movies you’ve created. You need to fetch them from the persistent store with a FetchRequest.

At the top of MovieList, remove the line declaring the movies array. Replace it with this FetchRequest:

// 1
@FetchRequest(
  // 2
  entity: Movie.entity(),
  // 3
  sortDescriptors: [
    NSSortDescriptor(keyPath: \Movie.title, ascending: true)
  ]
// 4
) var movies: FetchedResults<Movie>

When you need to retrieve entities from Core Data, you create a FetchRequest. Here, you:

  1. Declare the property using the @FetchRequest property wrapper, which lets you use the results directly in your SwiftUI view.
  2. Inside the property wrapper, specify which entity you’d like Core Data to fetch. This will fetch instances of the Movie entity.
  3. Add an array of sort descriptors to determine the order of the results. For instance, you could sort the Movies by genre, then by title for Movies with the same genre. But here, you simply order by title.
  4. Finally, after the property wrapper, you declare the movies property of type FetchedResults.

Predicates

This will fetch all Movies stored by Core Data. But, what if you need to filter the objects, or only retrieve one specific entity? You can also configure a fetched request with a predicate to limit the results, such as only fetching Movies from a certain year or matching a certain genre. To do so, you would add the predicate parameter at the end of the @FetchRequest property wrapper, like so:

predicate: NSPredicate(format: "genre contains 'Action'")

There’s no need to add this now, as your fetch request should fetch all Movies. But if you want to play around with this then by all means do!

Testing the Results

Build and run. You’ll see your list of movies. Congratulations!

List of Created Movies

Well, this just gets you back to where you started. To test that the movies are storing to disk, add a few movies and then kill the app by pressing stop in Xcode. Then build and run again. All your movies will still be there!

Deleting Objects

Next, you’ll learn to delete objects. If you swipe left and try to delete a movie, nothing happens. To fix this, replace deleteMovie(at:) with:

func deleteMovie(at offsets: IndexSet) {
  // 1
  offsets.forEach { index in
    // 2
    let movie = self.movies[index]
    // 3
    self.managedObjectContext.delete(movie)
  }
  // 4
  saveContext()
}

Here’s what’s happening:

  1. A SwiftUI List provides you with an IndexSet of deletions when you swipe to delete an object in the list. Iterate over the IndexSet with forEach.
  2. Get the movie for the current index.
  3. Delete the movie from the managed object context.
  4. Save the context to persist your changes to disk.

Build and run. Then, delete a movie.

Attempt to delete a movie in the app

You’ve restored all the functionality of the app, and what’s more, it’ll still be there in the morning thanks to Core Data! And you’re done!

Where to Go From Here?

You used Core Data to introduce persistence into a SwiftUI project and store entities to disk. You can download the completed project using the Download Materials button at the top or bottom of this tutorial.

If you want more hands-on experience with Core Data and SwiftUI, check out these tutorials:

Core Data is a huge topic and there’s still a lot to learn. Take a look at Core Data by Tutorials for a deep dive into Core Data using UIKit.

You’ve gone over a fair bit of data flow in this tutorial, but if you’d like to learn more, take a look at WWDC 2019’s Data Flow Through SwiftUI video.

For data flow, as well as everything else you need to become a SwiftUI master, SwiftUI by Tutorials will get you where you need to go.

Getting set up with Core Data is easier than ever before, thanks to SwiftUI. I hope you enjoyed this Core Data with SwiftUI tutorial. If you have any questions or insights to share, please join the forum below.

Average Rating

4.3/5

Add a rating for this content

31 ratings

More like this

Contributors

Comments