Chapters

Hide chapters

UIKit Apprentice

Second Edition · iOS 15 · Swift 5.5 · Xcode 13

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 13 chapters
Show chapters Hide chapters

27. Saving Locations
Written by Fahim Farook

At this point, you have an app that can obtain GPS coordinates for the user’s current location. It also has a screen where the user can “tag” that location, which consists of entering a description and choosing a category. Later on, you’ll also allow the user to pick a photo.

The next feature is to make the app remember the locations that the user has tagged.

This chapter covers the following:

  • Core Data overview: A brief overview of what Core Data is and how it works.
  • Add Core Data: Add the Core Data framework to the app and use it.
  • The data store: Initializing the data store used by Core Data.
  • Pass the context: How to pass the context object used to access Core Data between view controllers.
  • Browse the data: Looking through the saved data.
  • Save the locations: Saving entered location information using Core Data.
  • Handle Core Data errors: Handling Core Data errors when there’s an issue with saving.

Core Data overview

You have to persist the data for these captured locations somehow — they need to be remembered even when the app terminates.

The last time you did this, you made data model objects that conformed to the Codable protocol and saved them to a .plist file. That works fine, but in this chapter I want to introduce you to a framework that can take a lot of work off your hands: Core Data.

Core Data is an object persistence framework for iOS apps. If you’ve looked at Core Data before, you may have found the official documentation a little daunting, but the principle is quite simple.

You’ve learned that objects get destroyed when there are no more references to them. In addition, all objects get destroyed when the app terminates.

With Core Data, you can designate some objects as being persistent so they will always be saved to a data store. Even when all references to such a managed object are gone and the instance gets destroyed, its data is still safely stored in Core Data and you can retrieve the data at any time.

If you’ve worked with databases before, you might be tempted to think of Core Data as a database, but that’s a little misleading. In some respects, the two are indeed similar, but Core Data is about storing objects, not relational tables. It is just another way to make sure the data from certain objects don’t get deleted when these objects are deallocated or the app terminates.

Using Core Data

Core Data requires the use of a data model. This is a special file that you add to your project to describe the objects that you want to persist. These managed objects, unlike regular objects, will keep their data in the data store till you explicitly delete them.

The data model

Back in Chapter 22, when you first created the MyLocations project, the project settings had an option named Use Core Data. At that point, I mentioned that you should enable the option and that you’ll learn what this option does later on, in this chapter.

What that option did was to let Xcode know that you would be using Core Data in your project and that it should create a data model and add the necessary set up code as part of the initial project. If you check the Project Navigator, you will see the data model file, MyLocations.xcdatamodeld, listed.

If you forgot to enable the option when creating the project — or, you want to know how to add a Core Data model to an existing project — simply select New File… from the Xcode context menu and then select the Data Model option under Core Data in the template chooser under iOS, give the new data model a name and save it. It’s as simple as that!

➤ Click MyLocations.xcdatamodeld in the Project navigator to open the Data Model editor:

The empty data model
The empty data model

For each object that you want Core Data to manage, you have to add an entity.

An entity describes which data fields your objects will have. In a sense, it serves the same purpose as a class, but specifically for Core Data’s data store — if you’ve worked with SQL databases before, you can think of an entity as a table.

This app will have one entity, Location, which stores all the properties for a location that the user tagged. Each Location will keep track of the following data:

  • Latitude and longitude
  • Placemark – the street address
  • The date when the location was tagged
  • The user’s description
  • Category

These are the items from the Tag Location screen, except for the photo. Photos can potentially be very big and can take up several megabytes of storage space. Even though the Core Data store can handle big “blobs” of data, it is usually better to store photos as separate files in the app’s Documents directory. More about that later.

➤ Click the Add Entity button at the bottom of the data model editor. This adds a new entity under the ENTITIES heading. Name it Location — you can rename the entity by clicking its name, or from the Data Model inspector pane on the right.

The new Location entity
The new Location entity

The entity detail pane in the center shows three sections: Attributes, Relationships and Fetched Properties. The Attributes are the entity’s data fields.

This app only has one entity, but generally, apps will have many entities that are all related to each other somehow. With Relationships and Fetched Properties, you can tell Core Data how your objects depend on each other.

For this app, you will only use the Attributes section only.

➤ Click the Add Attribute button at the bottom of the editor, or the small + button below the Attributes section. Name the new attribute latitude and set its Type to Double:

Choosing the attribute type
Choosing the attribute type

Attributes are basically the same as properties, and therefore they have a type. You’ve seen earlier that the latitude and longitude coordinates really have the data type Double. So, that’s what you’re choosing for the attribute as well.

Note: Don’t let the change in terminology scare you. Just think:

entity = object (or class)

attribute = property

If you’re wondering where you’ll define methods in Core Data, then the answer is: you don’t. Core Data is only for storing the data portion of objects. That is what an entity describes: the data of an object, and optionally, how that object relates to other objects if you use Relationships and Fetched Properties.

In a short while, you’re going to define your own Location class by creating a Swift file, just as you’ve been doing all along. Because it describes a managed object, this class will be associated with the Location entity in the data model. But it’s still a regular class, so you can add your own methods to it.

➤ Add the rest of the attributes for the Location entity:

  • longitude – Type: Double
  • date – Type: Date
  • locationDescription – Type: String
  • category – Type: String
  • placemark – Type: Transformable

The data model should look like this when you’re done:

All the attributes of the Location entity
All the attributes of the Location entity

Why didn’t you just call the description value “description” instead of “locationDescription”? As it turns out, description is the name of a method from NSObject.

If you try to name an attribute “description”, then it will cause a naming conflict with the NSObject method since Core Data managed objects are derived from NSObject. Xcode will give you an error message if you try to do this.

The type of the placemark attribute is Transformable. Core Data only supports a limited number of data types out of the box, such as String, Double, and Date. The placemark is a CLPlacemark object and is not in the list of supported data types.

Fortunately, Core Data has a provision for handling arbitrary data types. Any class that conforms to the NSCoding protocol can be stored in a Transformable attribute without additional work. Fortunately for us, CLPlacemark does conform to NSCoding, so you can store it in Core Data with no trouble.

And in case you are wondering, NSCoding is the Objective-C equivalent of the Swift Codable protocol — it allows classes to encode and decode themselves if they support it.

By default, entity attributes are optional, meaning they can be nil. In our app, the only thing that can be nil is the placemark, in case reverse geocoding failed. It’s a good idea to embed this constraint in the data model.

➤ Select the category attribute. In the inspectors panel, switch to the Data Model inspector and uncheck the Optional setting:

Making the category attribute non-optional
Making the category attribute non-optional

➤ Repeat this for the other attributes, except for placemark.

Tip: you can select multiple attributes at the same time, either by Command+clicking to select individually, or Shift+Clicking to select a range.

➤ Press ⌘+S to save your changes. Xcode is supposed to do this automatically, but I’ve found the data model editor to be a little unreliable at times. Better safe than sorry!

You’re done with the data model, but there’s one more thing to do.

Generate the code

➤ Click on the Location entity to select it and go to the Data Model inspector.

The Data Model inspector
The Data Model inspector

The Class > Name field says “Location”. When you retrieve a Location entity from Core Data, it gives you an instance of the Location class which is derived from NSManagedObject. NSManagedObject is the base class for all objects that are managed by Core Data. Regular objects inherit from NSObject, but Core Data objects extend NSManagedObject.

Because using NSManagedObject directly is a bit limiting, Xcode helpfully sets you up to use your own Location class instead. You’re not required to make your own classes for your entities, but it does make Core Data easier to use. So now when you retrieve a Location entity from the data store, Core Data doesn’t give you an NSManagedObject but an instance of your own Location class.

Note also that the Class > Codegen dropdown is set to “Class Definition”. Xcode will automatically generate the code for your entity’s class with this setting so that you don’t have to do any extra work. However, it is useful to understand how to make your own NSManagedObject subclass rather than relying on Xcode magic. So, for this app, you’ll write the code yourself.

➤ In the inspector, change Codegen to Manual/None.

Even though you won’t be using automatic class generation, Xcode can still lend a helping hand.

➤ From the menu bar, choose Editor ▸ Create NSManagedObject Subclass….

The assistant will now ask you for the data models and entities you wish to create classes for.

➤ Select MyLocations (which is the name of your data model) and click Next. In the next step, make sure Location is selected and click Next again.

➤ Choose a location to save the source files — in your case, the folder for your project. Press Create to finish.

This adds two new files to the project. The first one is named Location+CoreDataClass.swift and looks something like this:

import Foundation
import CoreData

@objc(Location)
public class Location: NSManagedObject {
  
}

As you can see in the class line, the Location class extends NSManagedObject instead of the regular NSObject.

You already know what the public and @objc attributes are for since you’ve encountered them before, but what does the (Location) bit do?

That is actually a part of the @objc attribute. The Swift compiler uses a mechanism called name mangling to rename methods internally so that they can be identified uniquely. After all, if you have two methods named copyFiles in the same project, how does the compiler know which one a particular bit of code refers to? It has to have a way to identify each method uniquely so that all method calls are resolved correctly.

Name mangling works fine if your project has only Swift code. But since you can combine Swift and Objective-C code in the same project, sometimes you run into trouble in such “hybrid” projects because Objective-C is not able to identify a Swift class correctly due to name mangling. This happens often when working with archived data since the archived data saves the class name and you run into issues when Objective-C can’t reconcile the name it receives with a known class.

This is where the @objc(Location) (or similar) notation comes into play. The part inside the brackets, in this case Location, tells the compiler that that is the name Objective-C code will use to refer to this particular class.

You shouldn’t have to worry about the above notation at all in this book since you’ll be working with Swift code only. However, it’s always a good idea to know things such as this for when you are a full-blown developer since you most likely will encounter a “hybrid” project at some point.

The second file that got created is Location+CoreDataProperties.swift:

import Foundation
import CoreData

extension Location {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Location> {
        return NSFetchRequest<Location>(entityName: "Location")
    }

    @NSManaged public var latitude: Double
    @NSManaged public var longitude: Double
    @NSManaged public var date: Date?
    @NSManaged public var locationDescription: String?
    @NSManaged public var category: String?
    @NSManaged public var placemark: NSObject?
}

extension Location : Identifiable {

}

In this file, Xcode has created properties for the attributes that you specified in the Data Model editor. But what is this extension thing?

With an extension you can add additional functionality to an existing object without having to change the original source code for that object. This even works when you don’t actually have the source code for those objects. Later on you’ll see an example of how you can use an extension to add new methods to objects from iOS frameworks.

Here, the extension is used for another purpose. If you change your Core Data model at some later time and you want to automatically update the code to match those changes, then you can choose Create NSManagedObject Subclass again and Xcode will only overwrite what is in Location+CoreDataProperties.swift but not anything you added to Location+CoreDataClass.swift. So, it’s not a good idea to make changes to Location+CoreDataProperties.swift if you plan on overwriting this file later.

The second extension simply indicates that the Location class conforms to the Identifiable protocol – which basically means that all Location items can be uniquely identified by means of a few methods provided by the protocol.

Fix the code

Unfortunately, Xcode made a few small boo-boos in the types of the properties, so you’ll have to make some changes to Location+CoreDataProperties.swift.

The first thing to fix is the placemark variable. Because you made placemark a Transformable attribute, Xcode doesn’t really know what kind of object this will be. So, it chose the generic type NSObject.

But you know it’s going to be a CLPlacemark object. So, you can make things easier for yourself by changing it.

➤ First, import Core Location into Location+CoreDataProperties.swift:

import CoreLocation

➤ Then change the placemark property to:

@NSManaged public var placemark: CLPlacemark?

➤ Finally, remove the question marks behind the date, category and locationDescription properties. Earlier you told Core Data these attributes were not optionals. So, they don’t need the question mark.

Because this is a managed object, and the data lives inside a data store, Swift will handle Location’s variables in a special way. The @NSManaged keyword tells the compiler that these properties will be resolved at runtime by Core Data. When you put a new value into one of these properties, Core Data will place that value into the data store for safekeeping, instead of in a regular instance variable.

And if you are wondering, the @nonobjc attribute is the reverse of the @objc attribute — it makes a class, method, or property not available to Objective-C. Since this came by way of generated boilerplate code, don’t worry too much about why you’d want to do that in this particular case :]

This concludes the definition of the data model for MyLocations. Now you have to hook it up to a data store.

The data store

On iOS, Core Data stores all of its data into an SQLite — pronounced “SQL light” — database. It’s OK if you have no idea what SQLite is. You’ll take a peek into that database later, but you don’t really need to know what goes on inside the data store in order to use Core Data.

However, you do need to initialize this data store when the app starts. The code for that is the same for just about any app that uses Core Data and it goes in the app delegate class.

As you learnt previously, the app delegate is the object that gets notifications that concern the application as a whole and the scene delegate is the object which gets notified about changes to the main application window. Just a couple of years ago, the scene delegate did not exist and the main application window was also handled by the app delegate.

If you had enabled the Use Core Data option when you created the MyLocations project, then most of the necessary code will already be there in your AppDelegate. Unfortunately, due to the addition of scene delegate, and how we’ll be passing on Core Data references to the rest of your app, this code really needs to be in scene delegate instead :]

So, let’s move the code piece by piece over to scene delegate and I’ll explain what each bit does.

➤ First, move the Core Data import at the very top of AppDelegate.swift over to SceneDelegate.swift:

import CoreData

➤ Then, move the following code for the persistentContainer variable over to the SceneDelegate class — the existing code might look slightly different to this and will also have a lot of comments but essentially, it’s the same:

lazy var persistentContainer: NSPersistentContainer = {
  let container = NSPersistentContainer(name: "MyLocations")
  container.loadPersistentStores {_, error in
    if let error = error {
      fatalError("Could not load data store: \(error)")
    }
  }
  return container
}()

This is the code you need to load the data model that you’ve defined earlier, and to connect it to an SQLite data store.

The goal here is to create an NSManagedObjectContext object. That is the object you’ll use to talk to Core Data. To get that NSManagedObjectContext object, the app needs to do several things:

  1. Create an NSManagedObjectModel from the Core Data model you created earlier. This object represents the data model during runtime. You can ask it what sort of entities it has, what attributes these entities have, and so on. In most apps, you don’t need to use the NSManagedObjectModel object directly.

  2. Create an NSPersistentStoreCoordinator object. This object is in charge of the SQLite database.

  3. Finally, create the NSManagedObjectContext object and connect it to the persistent store coordinator.

Together, these objects are also known as the “Core Data stack”.

Previously, you had to perform the above steps one-by-one in code, which could get a little messy. But now, there is a new object, the NSPersistentContainer, that takes care of everything.

That doesn’t mean you should immediately forget what you just learned about the NSManagedObjectModel and the NSPersistentStoreCoordinator, but it does save you from writing a bunch of code.

The code that you just added creates an instance variable persistentContainer of type NSPersistentContainer. To get the NSManagedObjectContext that we’re after, you can simply ask the persistentContainer for its viewContext property.

➤ For convenience, add another property to get the NSManagedObjectContext from the persistent container — this code would not have been added by Xcode, so you need to add this yourself to SceneDelegate.swift:

lazy var managedObjectContext = persistentContainer.viewContext

➤ There should be a saveContext() method in AppDelegate.swift – move that to SceneDelegate.swift as well.

➤ Finally, in sceneDidEnterBackground(_:) in SceneDelegate.swift, there’s a line calling the saveContext() method like this:

(UIApplication.shared.delegate as? AppDelegate)?.saveContext()

Change it to:

saveContext()

Since you moved the saveContext method to the scene delegate, instead of calling the saveContext method from AppDelegate you change the method call to indicate that it is now an internal scene delegate method. That’s it.

Now we’re ready to start using Core Data!

➤ Build the app to make sure it compiles without errors. If you run it, you won’t notice any difference because you’re not actually using Core Data anywhere yet.

Pass the context

When the user presses the Done button in the Tag Location screen, the app currently just closes the screen. Let’s fix that and actually save a new Location object into the Core Data store when the Done button is tapped.

I mentioned the NSManagedObjectContext object. This is the object that you use to talk to Core Data. It is often described as a “scratchpad”. You first make your changes to the context and then you call its save() method to store those changes permanently in the data store.

This means that every object that needs to do something with Core Data needs to have a reference to the NSManagedObjectContext object.

Get the context

➤ Switch to LocationDetailsViewController.swift. First, import Core Data at the top, and then add a new instance variable:

var managedObjectContext: NSManagedObjectContext!

The problem is: how do you put the NSManagedObjectContext object from the scene delegate into this property?

The context object is created by SceneDelegate, but SceneDelegate has no reference to the LocationDetailsViewController.

That’s not so strange since the Location Details view controller doesn’t exist until the user taps the Tag Location button. Prior to that, there simply is no LocationDetailsViewController object in existence.

The answer is to pass along the NSManagedObjectContext object during the segue that presents the LocationDetailsViewController. The obvious place for that is prepare(for:sender:) in CurrentLocationViewController.

But then you need to find a way to get the NSManagedObjectContext object into the CurrentLocationViewController in the first place. And this means CurrentLocationViewController needs its own property for the NSManagedObject context.

➤ Add the following property to CurrentLocationViewController.swift (and don’t forget to add the Core Data import):

var managedObjectContext: NSManagedObjectContext!

➤ Add the following to prepare(for:sender:), so that it passes on the context to the Tag Location screen:

override func prepare(
  for segue: UIStoryboardSegue, 
  sender: Any?
) {
  if segue.identifier == "TagLocation" {
    . . .
    // New code
    controller.managedObjectContext = managedObjectContext 
  }
}

This should also explain why the managedObjectContext variable is declared as an implicitly unwrapped optional with the type NSManagedObjectContext!.

You should know by now that variables in Swift must always have a value. If they can be nil — which means “no value” —, then the variable must be made optional.

If you were to declare managedObjectContext without the exclamation point, like this:

var managedObjectContext: NSManagedObjectContext

Then Swift demands you give it a value in an init method — for objects loaded from a storyboard, such as view controllers, that method is init?(coder:).

However, prepare(for:sender:) happens after the new view controller is instantiated, long after the call to init?(coder:). As a result, inside init?(coder:) you can’t know what the value for managedObjectContext will be.

You have no choice but to leave the managedObjectContext variable nil for a short while until the segue happens, and therefore it must be an optional.

You could also have declared it like this:

var managedObjectContext: NSManagedObjectContext?

The difference between ? and ! is that the former requires you to manually unwrap the value with if let every time you want to use it.

That gets annoying really fast, especially when you know that managedObjectContext will get a proper value during the segue and that it will never become nil afterwards again. In that case, the exclamation mark is the best type of optional to use.

These rules for optionals may seem very strict — and possibly confusing — when you’re coming from another language such as Objective-C. But they are there for a good reason — by only allowing certain variables to have no value, Swift can make your programs safer and reduce the number of programming mistakes.

The fewer optionals you use, the better, but sometimes you can’t avoid them — as in this case with managedObjectContext.

Pass the context from SceneDelegate

SceneDelegate.swift now needs some way to pass the NSManagedObjectContext object to CurrentLocationViewController.

Unfortunately, Interface Builder does not allow you to make outlets for your view controllers on the Scene Delegate. Instead, you have to look up these view controllers by digging through the view hierarchy.

➤ Change the scene(_:willConnectTo:options:) method to:

func scene(
  _ scene: UIScene, 
  willConnectTo session: UISceneSession, 
  options connectionOptions: UIScene.ConnectionOptions
) {
  let tabController = window!.rootViewController as! UITabBarController
  if let tabViewControllers = tabController.viewControllers {
    let navController = tabViewControllers[0] as! UINavigationController
    let controller = navController.viewControllers.first as! CurrentLocationViewController
    controller.managedObjectContext = managedObjectContext
  }
}

In order to get a reference to the CurrentLocationViewController, you first have to find the UITabBarController and then look at its viewControllers array.

And since the first controller for the first tab is a navigation controller, then you have to go through the navigation controller’s list of controllers to finally get at the CurrentLocationViewController.

Once you have a reference to the CurrentLocationViewController object, you pass it the managedObjectContext. It may not be immediately obvious from looking at the code, but something special happens at this point…

Remember the code for persistentContainer you added to the scene delegate earlier? You probably recognized it as a lazy loading variable since you’ve encountered something similar before. This is the point at which the closure for the variable is actually executed and a new NSPersistentContainer instance is created.

What actually happens inside the closure is fairly straightforward:

let container = NSPersistentContainer(name: "MyLocations")
container.loadPersistentStores {_, error in
  if let error = error {
    fatalError("Could not load data store: \(error)")
  }
}
return container

You instantiate a new NSPersistentContainer object with the name of the data model you created earlier, MyLocations. Then you tell it to loadPersistentStores(), which loads the data from the database into memory and sets up the Core Data stack.

There is another closure here, the trailing closure for loadPersistentStores. The code in this closure gets invoked when the persistent container is done loading the data. If something went wrong, you print an error message — useful for debugging! — and terminate the app using the function fatalError().

Now that you know what it does, you may be wondering why you didn’t just put all of this code into a regular method like this:

var persistentContainer: NSPersistentContainer

init() {
  persistentContainer = createPersistentContainer()
}

func createPersistentContainer() -> NSPersistentContainer {
  // all the initialization code here
  return container
}

That would certainly work, but now the initialization of persistentContainer is spread over three different parts of the code: the declaration of the variable, the method that performs all the initialization logic, and the init method to tie it all together.

Isn’t it nicer to keep all this stuff in one place, rather than in three different places? Swift lets you perform complex initialization right where you declare the variable. I think that’s pretty nifty!

There’s another thing going on here:

lazy var persistentContainer: NSPersistentContainer = { ... }()

Notice the lazy keyword? That means the entire block of code in the { ... }() closure isn’t actually performed right away. The context object won’t be created until you ask for it. This is another example of lazy loading, Similar to how you handled DateFormatter earlier.

The managedObjectContext property is also declared lazy:

lazy var managedObjectContext = persistentContainer.viewContext

This is necessary because its initial value comes from persistentContainer.

➤ Run the app. Everything should still be the way it was, but behind the scenes a new database has been created for Core Data.

Browse the data

Core Data stores the data in an SQLite database. That file is named MyLocations.sqlite and it lives in the app’s Library folder. That’s similar to the Documents folder that you saw previously.

Core Data data store location

➤ The easiest way to find the location of the Core Data folder is to add the following to Functions.swift:

let applicationDocumentsDirectory: URL = {
  let paths = FileManager.default.urls(
    for: .documentDirectory, 
    in: .userDomainMask)
  return paths[0]
}()

This creates a new global constant, applicationDocumentsDirectory, containing the path to the app’s Documents directory. It’s a global because you’re not putting this inside a class. This constant will exist for the duration of the app; it never goes out of scope. You could have made a method for this as you did for Checklists, but using a global constant works just as well.

As before, you’re using a closure to provide the code that initializes this constant. Like all globals, this is evaluated lazily the very first time it is used.

Note: Globals have a bad reputation. Many programmers avoid them at all costs. The problem with globals is that they create hidden dependencies between the various parts of your program. And dependencies make the program hard to change and hard to debug.

But used well, globals can be very handy. It’s feasible that your app will need to know the path to the Documents directory in several different places. Putting it in a global constant is a great way to solve that design problem.

➤ Add the following line to application(_:didFinishLaunchingWithOptions:) — a good place would be just before the final return statement:

print(applicationDocumentsDirectory)

On my computer this prints out:

file:///Users/fahim/Library/Developer/CoreSimulator/Devices/31DCE39D-CEC6-4228-B6DF-98FA08E5544F/data/Containers/Data/Application/D21A7746-093C-4431-82CF-917A86AB7186/Documents/

➤ Open a new Finder window and press Shift+⌘+G. Then copy-paste the path without the file:// bit — note that you leave out only two slashes out of the three… — to go to the Documents folder.

The database is not actually in the Documents folder, so go back up one level and enter the Library folder, and then Application Support:

The new database in the app’s Documents directory
The new database in the app’s Documents directory

The MyLocations.sqlite-shm and -wal files are also part of the data store.

This database is still empty because you haven’t stored any objects in it yet, but just for the fun of it, you’ll take a peek inside. There are several handy — and free! — tools that give you a graphical interface for interacting with your SQLite databases.

Browse the Core Data store using a GUI app

You will use Liya to examine the data store file. Download it from the Mac App Store or cutedgesystems.com/software/liya/.

➤ Start Liya. It asks you for a database connection. Under Database Type choose SQLite.

Liya opens with this dialog box
Liya opens with this dialog box

➤ On the right of the Database Type field is a small icon. Click this to open a file picker.

You can navigate to the CoreSimulator/…/Library/Application Support folder, but that’s a lot of work – it’s a very deeply nested folder.

If you have the Finder window still open, it’s easier to drag the MyLocations.sqlite file from Finder directly on to the open file picker. Click Choose when you’re done.

Tip: You can also right-click the MyLocations.sqlite file in Finder and choose Open With ▸ Liya from the popup menu.

The Database URL field should now point to the correct folder and Database Name should say MyLocations.sqlite:

Connecting to the SQLite database
Connecting to the SQLite database

➤ Click Login to proceed.

The screen should look something like this:

The empty MyLocations.sqlite database in Liya
The empty MyLocations.sqlite database in Liya

The ZLOCATION table is where your Location objects will be stored. It’s currently empty, but on the right you can already see the column names that correspond to your fields: ZDATE, ZLATITUDE, and so on. Core Data also adds its own internal columns and tables with the Z_ prefix.

You’re not really supposed to change anything in this database by hand, but sometimes using a visual tool like this is handy to see what’s going on. You’ll come back to Liya once you’ve inserted new Location objects.

Note: An alternative to Liya is SQLiteStudio, sqlitestudio.pl. You can find more tools, paid and free, on the Mac App Store by searching for “sqlite”.

Troubleshoot Core Data issues

There is another handy tool for troubleshooting Core Data. By setting a special flag on the app, you can see the SQL statements that Core Data uses under the hood to talk to the data store.

Even if you have no experience with SQL, this is still valuable information. At least you can use it to tell whether Core Data is doing something or not. To enable this tool, you have to edit the project’s scheme.

Schemes are how Xcode lets you configure your projects. A scheme is a bunch of settings for building and running your app. Standard projects have just one scheme, but you can add additional schemes, which is handy when your project becomes bigger.

➤ Click on the left part of the MyLocations > iPhone bar at the top of the screen and choose Edit Scheme… from the menu.

The Edit Scheme... option
The Edit Scheme... option

The following panel should pop up:

The scheme editor
The scheme editor

➤ Choose the Run option on the left-hand side.

➤ Select the Arguments tab.

➤ In the Arguments Passed On Launch section, add the following:

-com.apple.CoreData.SQLDebug 1
-com.apple.CoreData.Logging.stderr 1

Adding the SQLDebug launch argument
Adding the SQLDebug launch argument

➤ Press Close to close this dialog, and run the app.

You should see something like this in the Xcode Console:

CoreData: annotation: Connecting to sqlite database file at "/Users/fahim/Library/Developer/CoreSimulator/Devices/31DCE39D-CEC6-4228-B6DF-98FA08E5544F/data/Containers/Data/Application/2BB1CA6C-E43C-4D48-B744-90206ECD3667/Library/Application Support/MyLocations.sqlite"
CoreData: sql: SELECT TBL_NAME FROM SQLITE_MASTER WHERE TBL_NAME = 'Z_METADATA'
CoreData: sql: pragma recursive_triggers=1
CoreData: sql: pragma journal_mode=wal
CoreData: sql: SELECT Z_VERSION, Z_UUID, Z_PLIST FROM Z_METADATA
CoreData: sql: SELECT TBL_NAME FROM SQLITE_MASTER WHERE TBL_NAME = 'Z_METADATA'
CoreData: sql: SELECT TBL_NAME FROM SQLITE_MASTER WHERE TBL_NAME = 'Z_MODELCACHE'
CoreData: sql: SELECT TBL_NAME FROM SQLITE_MASTER WHERE TBL_NAME = 'ACHANGE'
CoreData: sql: SELECT TBL_NAME FROM SQLITE_MASTER WHERE TBL_NAME = 'ATRANSACTIONSTRING'

This is the debug output from Core Data. If you understand SQL, some of this will look familiar. The specifics don’t matter, but it’s clear that Core Data is connecting to the data store at this point. Excellent!

Save the locations

You’ve successfully initialized Core Data and passed the NSManagedObjectContext to the Tag Location screen. Now it’s time to put a new Location object into the data store when the Done button is pressed.

➤ Add the following instance variable to LocationDetailsViewController.swift:

var date = Date()

You’re adding this variable because you need to store the current date in the new Location object. You only want to make that Date object once.

➤ In viewDidLoad(), change the line that sets the dateLabel’s text to:

dateLabel.text = format(date: date)

This now uses the new property instead of creating the date on the fly.

➤ Change the done() method to the following:

@IBAction func done() {
  guard let mainView = navigationController?.parent?.view 
  else { return }
  let hudView = HudView.hud(inView: mainView, animated: true)
  hudView.text = "Tagged"
  // 1
  let location = Location(context: managedObjectContext)
  // 2
  location.locationDescription = descriptionTextView.text
  location.category = categoryName
  location.latitude = coordinate.latitude
  location.longitude = coordinate.longitude
  location.date = date
  location.placemark = placemark
  // 3
  do {
    try managedObjectContext.save()
    afterDelay(0.6) {
      hudView.hide()
      self.navigationController?.popViewController(
        animated: true)
    }
  } catch {
    // 4
    fatalError("Error: \(error)")
  }
}

This is where you do all the work:

  1. First, you create a new Location instance. Because this is a managed object, you have to use its init(context:) method. You can’t just write Location() because then the managedObjectContext won’t know about the new object.

  2. Once you have created the Location instance, you can use it like any other object. Here you set its properties to whatever the user entered in the screen.

  3. You now have a new Location object whose properties are all filled in, but if you were to look in the data store at this point, you’d still see no objects there. That won’t happen until you save() the context.

    Saving takes any objects that were added to the context, or any managed objects that had their contents changed, and permanently writes these changes to the data store. That’s why they call the context a “scratchpad”; its changes aren’t persisted until you save them. The save() method can fail for a variety of reasons and therefore you need to catch any potential errors. That’s done using Swift error handling, which you’ve encountered before.

  4. Output the error and then terminate the application via the system method fatalError. But where does the error variable that you output come from? This is a local constant that Swift automatically populates with the error that it caught — handy, huh?

➤ Run the app and tag a location. Enter a description and press the Done button.

If everything went well, Core Data will dump a whole bunch of debug information into the debug area:

CoreData: sql: BEGIN EXCLUSIVE
. . .
CoreData: sql: INSERT INTO ZLOCATION(Z_PK, Z_ENT, Z_OPT, ZCATEGORY, ZDATE, ZLATITUDE, ZLOCATIONDESCRIPTION, ZLONGITUDE, ZPLACEMARK) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
. . .
CoreData: sql: COMMIT

These are the SQL statements that Core Data performs to store the new Location object in the database.

➤ In Liya, refresh the contents of the ZLOCATION table by pressing the Go button below the Tables list. There should now be one row in that table:

A new row was added to the table
A new row was added to the table

Note: If you don’t see any rows in the table, press the Stop button in Xcode first to exit the app. You can also try closing the Liya window and opening a new connection to the database. Sometimes, the Simulator data folder locations might change between app runs. So, you might need to set up a new database connection in Liya if this happens.

As you can see, the columns in this table contain the property values from the Location object. The only column that is not readable is ZPLACEMARK. Its contents have been encoded as a binary “blob” of data. That is because it’s a Transformable attribute and the NSCoding protocol has converted its fields into a binary chunk of data.

If you don’t have Liya or are a command line junkie, then there is another way to examine the contents of the database. You can use the Terminal app and the sqlite3 tool, but you’d better know your SQL’s from your ABC’s if you want to go that route:

Examining the database from the Terminal
Examining the database from the Terminal

Handle Core Data errors

To save the contents of the context to the data store, you did:

do {
  try managedObjectContext.save()
  . . .
} catch {
  fatalError("Error: \(error)")
}

What if something goes wrong with the save? In that case, code execution jumps to the catch branch and you call the fatalError() function. That will immediately kill the app and return the user to the iPhone’s Springboard. That’s a nasty surprise for the user, and therefore, not recommended.

The good news is that Core Data only gives an error if you’re trying to save something that is not valid. In other words, when there is some bug in your app.

Of course, you’ll get all the bugs out during development so users will never experience any, right? The sad truth is that you’ll never catch all your bugs. Some always manage to slip through.

Unfortunately, there isn’t much else to do but crash when Core Data does give an error. Something went horribly wrong somewhere and now you’re stuck with invalid data. If the app were allowed to continue, things would likely only get worse, as there is no telling what state the app is in. The last thing you want to do is to corrupt the user’s data.

However, instead of making the app crash hard with fatalError(), it might be nice to tell the user about the issue first so at least they know what is happening. The crash is still inevitable, but now your users will know why the app suddenly stopped working.

In this section, you’ll add a popup alert for handling such situations. Again, these errors should happen only during development, but just in case they do occur to an actual user, you’ll try to handle it with at least a little bit of grace.

Fake errors for testing purposes

Here’s a way to fake such a fatal error, just to illustrate what happens.

➤ Open the data model MyLocations.xcdatamodeld and select the placemark attribute. In the Data Model inspector, uncheck the Optional flag.

Making the placemark attribute non-optional
Making the placemark attribute non-optional

That means location.placemark can never be nil. This is a constraint that Core Data will enforce. When you try to save a Location object to the data store whose placemark property is nil, Core Data will throw a tantrum. So that’s exactly what you’re going to do here, just to test your error handling code and to make sure the app fails gracefully.

➤ Run the app. It is possible that the app crashes right away…

What happens is that you have just changed the data model by making changes to the placemark attribute. When you launch the app, the NSPersistentContainer notices this and tries to perform a “migration” of the SQLite database to the new, updated data model.

The migration may succeed… or not… depending on what is currently in your data store. If you previously tagged a location that did not have a valid address — i.e. whose placemark is nil — then the migration to the new data model fails. After all, the new data model does not allow for placemarks that are nil.

If the app crashed for you, then the debug area says why:

reason=Validation error missing attribute values on mandatory destination attribute, . . .

The MyLocations.sqlite file is out of date with respect to the changed data model, and Core Data can’t automatically resolve this issue. There are two ways to fix this:

  1. Simply throw away the MyLocations.sqlite file from the Library directory.
  2. Remove the entire app from the Simulator.

➤ Remove the MyLocations.sqlite file, as well as the –shm and –wal files, and run the app again.

That wasn’t actually the crash I wanted to show you, but it’s important to know that changing the data model may require you to throw away the database file or Core Data cannot be initialized properly.

Note: Not all is lost if NSPersistentContainer’s migration fails. Core Data allows you to perform your own migrations when you release an update to your app with a new data model. Instead of crashing, this mechanism allows you to convert the contents of the user’s existing data store to the new model. However, during development, it is just as easy to toss out the old database.

➤ Now here’s the trick. Tap the Get My Location button and then tap immediately on Tag Location. If you do that quickly enough, you can beat the reverse geocoder to it and the Tag Location screen will say: “No Address Found”. It only says that when placemark is nil.

If geocoding happens too fast, you can fake this by temporarily commenting out the line self.placemark = p.last! in locationManager(_:didUpdateLocations:) inside CurrentLocationViewController.swift. This will make it seem as if no address was found and the value of placemark stays nil.

➤ Tap the Done button to save the new Location object.

The app will crash:

The app crashes after a Core Data error
The app crashes after a Core Data error

At the very end of that error message — the above doesn’t show the full error message, but the Xcode console will —, you can see that it says:

NSValidationErrorKey=placemark

This means the placemark attribute did not validate properly. Because you set it to non-optional, Core Data does not accept a placemark value that is nil.

Of course, what you’ve just seen only happens when you run the app from Xcode — when it crashes, the debugger takes over and points at the line with the error. But that’s not what the user sees.

➤ Stop the app. Now tap the app’s icon in the Simulator to launch the app outside of Xcode. Repeat the same procedure to make the app crash. The app will simply cease functioning and disappear from the screen.

Imagine this happening to a user who just paid 99 cents (or more) for your app. They’ll be horribly confused, “What just happened?!” They may even ask for their money back.

It’s better to show an alert when this happens. After the user dismisses that alert, you’ll still make the app crash, but at least the user knows the reason why.

The alert message should probably ask them to contact you and explain what they did, so you can fix that bug in the next version of your app.

Alert the user about crashes

➤ Add the following code to Functions.swift:

let dataSaveFailedNotification = Notification.Name(
  rawValue: "DataSaveFailedNotification")

func fatalCoreDataError(_ error: Error) {
  print("*** Fatal error: \(error)")
  NotificationCenter.default.post(
    name: dataSaveFailedNotification, 
    object: nil)
}

This defines a new global function for handling fatal Core Data errors.

➤ Replace the error handling code in the done() action in LocationDetailsViewController.swift with:

. . .
} catch {
  fatalCoreDataError(error)
}

The call to fatalCoreDataError() has taken the place of fatalError(). So what does that new function do, actually?

It first outputs the error message to the Console using print() because it’s always useful to log such errors. After dumping the debug info, the function does the following:

NotificationCenter.default.post(name: dataSaveFailedNotification, object: nil)

I’ve been using the term “notification” to mean any generic event or message being delivered, but the iOS SDK also has an object called the NotificationCenter — not to be confused with Notification Center on your iOS device.

The code above uses NotificationCenter to post a notification. Any object in your app can subscribe to such notifications and when these occur, NotificationCenter will call a certain method in those listener objects.

Using this official notification system is yet another way that your objects can communicate with each other. The handy thing is that the object that sends the notification and the object that receives the notification don’t need to know anything about each other. The sender just broadcasts the notification to all and doesn’t really care what happens to it. If anyone is listening, great. If not, then that’s cool too.

UIKit defines a lot of standard notifications that you can subscribe to. For example, there is a notification that lets you know that the app is about to be suspended after the user taps the Home button.

You can also define your own notifications, and that is what you’ve done here. The new notification is called dataSaveFailedNotification.

The idea is that there is one place in the app that listens for this notification, pops up an alert view, and terminates. The great thing about using NotificationCenter is that your Core Data code does not need to care about any of this.

Whenever a saving error occurs, no matter at which point in the app, the fatalCoreDataError(_:) function sends out this notification, safe in the belief that some other object is listening for the notification and will handle the error.

So who will actually handle the error? The scene delegate is a good place for this. It’s the top-level object in the app and it’s always guaranteed to exist.

➤ Add the following method to SceneDelegate.swift:

// MARK: - Helper methods
func listenForFatalCoreDataNotifications() {
  // 1
  NotificationCenter.default.addObserver(
    forName: dataSaveFailedNotification,
    object: nil, 
    queue: OperationQueue.main
  ) { _ in
      // 2
      let message = """
      There was a fatal error in the app and it cannot continue.

      Press OK to terminate the app. Sorry for the inconvenience.
      """
      // 3
      let alert = UIAlertController(
        title: "Internal Error", 
        message: message,
        preferredStyle: .alert)

      // 4
      let action = UIAlertAction(title: "OK", style: .default) { _ in
        let exception = NSException(
          name: NSExceptionName.internalInconsistencyException,
          reason: "Fatal Core Data error", 
          userInfo: nil)
        exception.raise()
      }      
      alert.addAction(action)

      // 5
      let tabController = self.window!.rootViewController!
      tabController.present(
        alert, 
        animated: true, 
        completion: nil)
  }
}

Here’s how this works step-by-step:

  1. Tell NotificationCenter that you want to be notified whenever a dataSaveFailedNotification is posted. The actual code that is performed when that happens sits in a trailing closure.

  2. Set up the error message to display. This could have been done using a normal string by inserting new lines (\n) as you’ve seen done before, but this shows another way to do this — using multiline strings.

    Note that the multiline string starts and ends with a triple quote (""") and that the first line of the string has to start on a new line and the closing triple quotes have to be on a new line as well. You can include new lines and other special characters like quotes within the string. So it can be really handy, even if it looks a little weird :]

  3. Create a UIAlertController to show the error message and use the multiline string from earlier as the message.

  4. Add an action for the alert’s OK button. The code for handling the button press is again a closure — these things are everywhere! Instead of calling fatalError(), the closure creates an NSException object to terminate the app. That’s a bit nicer and it provides more information to the crash log.

  5. To show the alert with present(animated:completion:) you need a view controller that is currently visible. You simply use the window’s rootViewController — in this app that is the tab bar controller — since it will be visible at all times as per the current navigation flow of the app.

All that remains is calling this new method so that the notification handler is registered with NotificationCenter.

➤ Add the following to the end of scene(_:willConnectTo:options:):

listenForFatalCoreDataNotifications()

➤ Run the app again and try to tag a location before the street address has been obtained. Even though the app still crashes when you tap the OK button on the alert, at least now it tells the user what’s going on:

The app crashes with a message
The app crashes with a message

Again, I should stress that you test your app thoroughly to make sure you’re not giving Core Data any objects that do not validate. You want to avoid these save errors at all costs!

Ideally, users should never have to see that alert view, but it’s good to have it in place because there are no guarantees your app won’t have bugs.

Note: You can legitimately use managedObjectContext.save() to let Core Data validate user input. There is no requirement that you make your app crash after an unsuccessful save, only if the error was unexpected and definitely shouldn’t have happened!

Besides the “optional” flag, there are many more validation settings you can set for your entities. If you let users enter data that needs to go into these attributes, then it’s perfectly acceptable to use save() to validate input. If it throws an error, then a user input is invalid and you need to handle it.

➤ In the data model, set the placemark attribute back to optional (and uncomment the code in CurrentLocationViewController.swift, if you did comment out the placemark line).

Run the app just to make sure everything works as it should.

You can find the project files for this chapter under 27-Saving-locations in the Source Code folder.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.