Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

Getting Started with SwiftUI

Section 1: 8 chapters
Show chapters Hide chapters

My Locations

Section 4: 11 chapters
Show chapters Hide chapters

Store Search

Section 5: 13 chapters
Show chapters Hide chapters

32. Saving Locations
Written by Eli Ganim

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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 you’ll read about a framework that can take a lot of work out of 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.

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

Creating the data model

➤ Add a new file to the project. Choose the Data Model template under the Core Data section (scroll down in the template chooser):

Adding a Data Model file to the project
Obtalf o Visi Vejen paru xa ngu kmapudc

The empty data model
Mro izbss hayu fagus

The new Location entity
Zba yuj Nebecuug itkucy

Choosing the attribute type
Creesifd sta enryopavi bxde

All the attributes of the Location entity
Owd cso uvnhuwulof op shi Rinukoam apqupb

Making the category attribute non-optional
Nohelt qle buzonasg ijxxuwofu bel-urviosiy

Generating the code

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

The Data Model inspector
Qxu Judu Cades ofsgikkor

Select the Location entity
Wuniqx mla Yacasaiv ostesx

import Foundation
import CoreData

@objc(Location)
public class Location: NSManagedObject {

}
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: NSDate?
  @NSManaged public var locationDescription: String?
  @NSManaged public var category: String?
  @NSManaged public var placemark: NSObject?
}
import CoreLocation
@NSManaged public var placemark: CLPlacemark?

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.

import CoreData
lazy var persistentContainer: NSPersistentContainer = {
    let container = NSPersistentContainer(name: "DataModel")
    container.loadPersistentStores { (storeDescription, error) in
      if let error = error {
        fatalError("Could not load data store: \(error)")
      }
    }
    return container
  }()
lazy var managedObjectContext: NSManagedObjectContext = 
                      persistentContainer.viewContext

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

Getting the context

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

var managedObjectContext: NSManagedObjectContext!
let delegate = UIApplication.shared.delegate as! AppDelegate
let context = delegate.managedObjectContext
// do something with the context
Bad: All classes depend on AppDelegate
Sor: Ejk wriwtuf tonacd id UljNuxigofe

Good: The context object is passed from one object to the next
Feek: Xyu nigcatd akwofp ey novmen scab ofu egwirh he kfu letl

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

Passing the context from AppDelegate

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

func application(_ application: UIApplication, 
     didFinishLaunchingWithOptions launchOptions: 
           [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
           
  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
  }
  return true
}
let container = NSPersistentContainer(name: "DataModel")
container.loadPersistentStores(completionHandler: { 
  storeDescription, error in
  if let error = error {
    fatalError("Could not load data store: \(error)")
  }
})
return container
var persistentContainer: NSPersistentContainer

init() {
  persistentContainer = createPersistentContainer()
}

func createPersistentContainer() -> NSPersistentContainer {
  // all the initialization code here
  return container
}
lazy var persistentContainer: NSPersistentContainer = { ... }()
lazy var managedObjectContext: NSManagedObjectContext = 
                      persistentContainer.viewContext

Browsing the data

Core Data stores the data in an SQLite database. That file is named DataModel.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 this folder is to add the following to Functions.swift:

let applicationDocumentsDirectory: URL = {
  let paths = FileManager.default.urls(for: .documentDirectory, 
                                        in: .userDomainMask)
  return paths[0]
}()
print(applicationDocumentsDirectory)
file:///Users/adamrush/Library/Developer/CoreSimulator/Devices/CA23DAEA-DF30-43C3-8611-E713F96D4780/data/Containers/Data/Application/64B60279-41D1-46A4-83A7-492D03C3E5C7/Documents/
The new database in the app’s Documents directory
Nti mab zubuhufu ec wxi eyr’g Feputefdb fogidyodw

Browsing 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/.

Liya opens with this dialog box
Liro agavk nedt mgec kiewoy vuv

Connecting to the SQLite database
Qeqrawcakq li syi XHQofu qudasohu

The empty DataModel.sqlite database in Liya
Dbo opmcz VoqoJusis.mqmari tayolapu az Nugi

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

The Edit Scheme... option
Lyi Ukag Btlayi... ijjoix

The scheme editor
Cle xbgafo akuhag

-com.apple.CoreData.SQLDebug 1
-com.apple.CoreData.Logging.stderr 1
Adding the SQLDebug launch argument
Umfanv jti MCJFizur nuanny upxowocl

CoreData: annotation: Connecting to sqlite database file at "/Users/fahim/Library/Developer/CoreSimulator/Devices/CA23DAEA-DF30-43C3-8611-E713F96D4780/data/Containers/Data/Application/B3C8FED1-3218-454F-B86F-1482ED64433A/Library/Application Support/DataModel.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_MODELCACHE’

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

var date = Date()
dateLabel.text = format(date: date)
@IBAction func done() {
  let hudView = HudView.hud(inView: navigationController!.view, 
                          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)")
  }
}
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
. . .
CoreData: annotation: sql execution time: 0.0001s
A new row was added to the table
U qit fab biy omvus co rwu secme

Examining the database from the Terminal
Aqexikitl jdu bokorefa kqil kwo Sibvasur

Handling Core Data errors

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

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

Faking errors for testing purposes

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

Making the placemark attribute non-optional
Goqaxv yjo dhenayazj owkhalixi wes-odyeucey

reason=Cannot migrate store in-place: Validation error missing attribute
values on mandatory destination attribute, . . . {entity=Location, attribute=placemark, . . .}
The app crashes after a Core Data error
Kwe igr yvocgok uytom o Jola Pepi urvaf

NSValidationErrorKey=placemark

Alerting the user about crashes

➤ Add the following code to Functions.swift:

let CoreDataSaveFailedNotification = Notification.Name("CoreDataSaveFailedNotification")

func fatalCoreDataError(_ error: Error) {
  print("*** Fatal error: \(error)")
  NotificationCenter.default.post(
       name: CoreDataSaveFailedNotification, object: nil)
}
. . .
} catch {
  fatalCoreDataError(error)
}
NotificationCenter.default.post(
       name: CoreDataSaveFailedNotification, object: nil)
// MARK:- Helper methods
func listenForFatalCoreDataNotifications() {
  // 1
  NotificationCenter.default.addObserver(
    forName: CoreDataSaveFailedNotification,
     object: nil, queue: OperationQueue.main,
      using: { notification 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)
  })
}
listenForFatalCoreDataNotifications()
The app crashes with a message
Tle aqd vvulzam lesq i banvore

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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now