内容简介:Imagine jotting down something important in Notes, only to find your data is gone the next time you open the app! Fortunately,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 s
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.
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 .
Next, tap the Plus button on the top-right to add one of your favorites.
You’ll see 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.
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.
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:
-
Add a
lazy
property calledpersistentContainer
to yourSceneDelegate
. The first time you reference the property, it will create anNSPersistentContainer
. -
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. - Instruct the container to load the persistent store, which simply sets up the Core Data stack.
- 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:
viewContext try/catch
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 .
Xcode creates a new entity in the data model, named Entity by default. Double-click the name and change it to Movie .
Next, click the + icon under Attributes to add a new attribute. Name it title and set the type to String .
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:
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
.
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
.
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 View
s.
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 MovieRow
s 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. :]
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:
-
Create a new
Movie
in your managed object context. -
Set all the properties of the
Movie
that are passed as parameters intoaddMovie(title:genre:releaseDate:)
. - Save the managed object context.
Build and run and create a new movie. You’ll notice a blank 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:
-
Declare the property using the
@FetchRequest
property wrapper , which lets you use the results directly in your SwiftUI view. -
Inside the property wrapper, specify which entity you’d like Core Data to fetch. This will fetch instances of the
Movie
entity. -
Add an array of sort descriptors
to determine the order of the results. For instance, you could sort the
Movie
s by genre, then by title forMovie
s with the same genre. But here, you simply order by title. -
Finally, after the property wrapper, you declare the
movies
property of typeFetchedResults
.
Predicates
This will fetch all Movie
s 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 Movie
s 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 Movie
s. 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!
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:
-
A SwiftUI
List
provides you with anIndexSet
of deletions when you swipe to delete an object in the list. Iterate over theIndexSet
withforEach
. -
Get the movie for the current
index
. - Delete the movie from the managed object context.
- Save the context to persist your changes to disk.
Build and run. Then, delete a movie.
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.
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。