Russell Gordon

Search CTRL + K

Core Data and Xcode Previews

Right now I am reading through Practical Core Data by Donny Wals.

Reading that book has been a welcome consolidation of my understanding of Core Data.

My first exposure to the Core Data framework was rough, entirely due to my own predisposition to biting off more than I can chew.

When last using Core Data, I was at first disappointed because I did not understand how to provide sample data to views rendered in Xcode Previews. The ability to rapidly iterate design ideas for a view in Xcode Previews is one of my favourite aspects of programming with the SwiftUI framework.

I was delighted when I eventually cobbled together some understanding of how to use Xcode Previews while obtaining sample data through a Core Data store. My intent is to clearly describe how to do just that in this article.

Starting Point

This Xcode project uses a basic Core Data model without support for working with views in Xcode Previews.

It was created using Xcode 12.5.1 – download the project to follow along, and if necessary, that version of Xcode.

The finished project, including the code necessary to drive Xcode Previews, is included at the end of this article.

In the project you will find an app that I built by, for the most part, following the instructions provided in Chapter 1 of Practical Core Data.

The app is very basic, allowing a user to add a movie to a list of movies, navigate to a detail view, and then edit the name of that movie:

Three app screenshots showing, in order: a list of movies, a detail screen for a movie, and an editing sheet for that same movie.

I will not go into detail here about how the app and the StorageProvider class works other than to say that I think it is a pretty fair example of how to separate the data layer from the view layer of an app. Hopefully the provided comments are clear.

If you examine any of the views in the app, you will see that the Xcode Previews code is commented out.

Let's start correcting that by completing the first of three steps.

Using Core Data Storage In-memory

Core Data's underlying storage, by default, persists changes made. If it didn't, it would not be a very useful framework!

We could drive Xcode Previews with sample data from Core Data, but if we allowed the changes to persist, every time we reload a view in a preview, more data would be added to our Core Data store.

This is generally not ideal, since an Xcode Preview will be reloaded many times.

Open StorageProvider.swift and navigate to the initializer on or around line 20:

Modify the list of parameters for the initializer to the following:

init(inMemory: Bool = false) {

By providing a default value for the newly introduced parameter, existing call sites do not need to be updated. A default value of false is logical as when the app is running outside of a preview, we want changes to the Core Data store to persist.

Note that the first thing that occurs within the initializer is that an NSPersistentContainer instance is created:

// Access the model file
persistentContainer = NSPersistentContainer(name: "PracticalCoreData")

Immediately after that line of code, add the following:

// Don't save information for future use if running in memory...
if inMemory {
	persistentContainer.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}

The edits we have made here will allow us to create an instance of StorageProvider whose data, when changes are made, are not persisted because they are kept in-memory only, as opposed to being written to a long-term storage medium.

On to the second step...

Generating Sample Data

The second property of the StorageProvider class is as follows:

// For initializing the Core Data stack and loading the Core Data model file
let persistentContainer: NSPersistentContainer

Immediately below that property, add the following closure:

// For use with Xcode Previews, provides some data to work with for examples
static var preview: StorageProvider = {
	
	// Create an instance of the provider that runs in memory only
	let storageProvider = StorageProvider(inMemory: true)
	
	// Add a few test movies
	let titles = [
				"The Godfather",
				"The Shawshank Redemption", 
				"Schindler's List", 
				"Raging Bull", 
				"Casablanca", 
				"Citizen Kane",
				]
				
	for title in titles {
		storageProvider.saveMovie(named: title)
	}
	
	// Now save these movies in the Core Data store
	do {
		try storageProvider.persistentContainer.viewContext.save()
	} catch {
		// Something went wrong 😭
		print("Failed to save test movies: \(error)")
	}

	return storageProvider
}()

The third step will be completed a bit later on.

At this point, we are ready to try things out with the Xcode Preview for MovieListView.

Navigate to MovieListView.swift and scroll to the bottom of the file.

Uncomment the structure for Xcode Previews, then add the following modifier to the NavigationView:

.environmentObject(StorageProvider.preview)

The entire structure should look like this:

struct MovieListView_Previews: PreviewProvider {

    static var previews: some View {

        NavigationView {
            MovieListView()
        }
        .environmentObject(StorageProvider.preview)
    }

}

The static preview property on the StorageProvider class returns an instance of StorageProvider itself, and that instance is then inserted into the environment – which in turn is accessed by the MovieListView structure when the Xcode Preview runs.

Now start a Live Preview inside the Xcode Previews panel.

You should be able to navigate around the three views of the app, and work with the provided data.

Before continuing, let's explore what happens if we do not load the Core Data store in-memory only.

Return to StorageProvider.swift and, on or around line 24, modify the code such that false is passed as the argument for the inMemory parameter:

// Create an instance of the provider that runs in memory only
let storageProvider = StorageProvider(inMemory: false)

Now try running and re-running the Live View of MovieListView once or twice.

You will not at first see the list of movies change. Run the project in the simulator. Then, return to a Live View of MovieListView in Xcode Previews.

At this point, you should see that the list of movies has been repeated several times. That is because each time a Live View was run, the preview closure was invoked, re-adding several movies to the list of movies in the Core Data store.

I am not certain why it is necessary to run the app in the simulator to see the result of re-running the preview closure several times. I am not at all familiar with the underpinnings of how Xcode works.

Nonetheless, this behaviour is probably not what you want in the long term – so revert the code around line 24 of StorageProvider such that the Core Data store runs in-memory again:

// Create an instance of the provider that runs in memory only
let storageProvider = StorageProvider(inMemory: true)

Close Xcode entirely, then open the project again.

When you run Xcode Previews, you should see just the initial list of movies on MovieListView, with no repeats.

Providing a Single Instance of a Movie

This is the third and final step of the tutorial.

The Movie class is connected to the Movie entity I defined in the Core Data model editor.

Here we are going to extend this class so that a static property provides a single movie that we can use to drive MovieDetailView and MovieEditView.

In the Model group of the Xcode project, create a new file named Movie+PreviewProvider.swift.

Add the following code to the new file:

import CoreData
import Foundation

// Exists to provide a movie to use with MovieDetailView and MovieEditView
extension Movie {
    
    // Example movie for Xcode previews
    static var example: Movie {
        
        // Get the first movie from the in-memory Core Data store
        let context = StorageProvider.preview.persistentContainer.viewContext
        
        let fetchRequest: NSFetchRequest<Movie> = Movie.fetchRequest()
        fetchRequest.fetchLimit = 1
        
        let results = try? context.fetch(fetchRequest)
        
        return (results?.first!)!
    }
    
}

The optionals that are being force-unwrapped on the last line of that static computed property may give you pause.

Let's edit MovieDetailView.swift and then re-visit the code above.

Uncomment the structure for Xcode Previews, then again add the following modifier to the NavigationView:

.environmentObject(StorageProvider.preview)

Additionally, for the movie parameter on MovieDetailView, provide Movie.example as the argument.

The entire structure should look like this:

struct MovieDetailView_Previews: PreviewProvider {
    
    static var previews: some View {
        NavigationView {
            MovieDetailView(movie: Movie.example)
        }
        .environmentObject(StorageProvider.preview)
    }
    
}

When the Xcode Preview is loaded, our sample data is loaded via the preview static property on StorageProvider and again, inserted into the environment.

When MovieDetailView loads, from the Movie extension above, we pass the static computed property example as the argument for the movie whose details will be shown.

Since the sample data is loaded first, in StorageProvider, we can safely force-unwrap the result of the fetch request in our extension to the Movie class.

You should now be able to see MovieDetailView in the Xcode Previews panel.

In much the same way, make edits to the preview structure for MovieEditView, such that the structure looks like this:

struct MovieEditView_Previews: PreviewProvider {
    
    static var previews: some View {
        NavigationView {
            MovieEditView(dismissView: .constant(false),
                          movie: Movie.example)
        }
        .environmentObject(StorageProvider.preview)
    }
}

Conclusion

Going forward, when a new entity is added to the Core Data model, two steps need to occur:

  1. In the preview property of StorageProvider, appropriate sample data must be created and added to the Core Data store.
  2. Write an extension to the class for the newly defined entity, and create a static computed property named example that loads a record from that entity.

Here is a completed version of this project, with all edits described above.

If you have found any errors in this article, or if this article has been useful to you, please let me know. 😀