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

33. The Locations Tab
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

You’ve set up the data model and given the app the ability to save new locations to the data store. Next, you’ll show these saved locations in a table view in the second tab.

The completed Locations screen will look like this:

The Locations screen
The Locations screen

This chapter covers the following:

  • The locations tab: Set up the second tab to display a list of saved locations.
  • Create a custom table view cell subclass: Create a custom table view cell subclass to handle displaying location information.
  • Edit locations: Add functionality to allow editing of items in the locations list.
  • Use NSFetchedResultsController: How do you use NSFetchedResultsController to fetch data from your Core Data store?
  • Delete Locations: Add the ability to the UI to delete locations, thus removing them from the Core Data store as well.
  • Table view sections: Use built-in Core Data functionality to add the ability to display separate sections based on the location category.

The Locations tab

➤ Open the storyboard editor and delete the Second Scene. This is a leftover from the project template and you don’t need it.

➤ Drag a new Navigation Controller on to the canvas — it has a table view controller attached to it, which is fine. You’ll use that in a second.

Control-drag from the Tab Bar Controller to this new Navigation Controller and select Relationship Segue — view controllers. This adds the navigation controller to the tab bar.

➤ The Navigation Controller now has a Tab Bar Item that is named “Item.” Rename it to Locations.

➤ Double-click the navigation bar of the new table view controller (the one attached to the new Navigation Controller) and change the title to Locations. (If Xcode gives you trouble, use the Attributes inspector on the Navigation Item instead.)

The storyboard now looks like this:

The storyboard after adding the Locations screen
The storyboard after adding the Locations screen

➤ Run the app and activate the Locations tab. It doesn’t show anything useful yet:

The Locations screen in the second tab
The Locations screen in the second tab

Designing the table view cell

Before you can show any data in the table, you first have to design the prototype cell.

The prototype cell
Yni jhapuhbqu cogw

The basic table view controller

Let’s write the code for the view controller. You’ve seen table view controllers several times now, so this should be easy.

import UIKit
import CoreData
import CoreLocation

class LocationsViewController: UITableViewController {
  var managedObjectContext: NSManagedObjectContext!

  // MARK: - Table View Delegates
  override func tableView(_ tableView: UITableView, 
        numberOfRowsInSection section: Int) -> Int {
    return 1
  }

  override func tableView(_ tableView: UITableView,
               cellForRowAt indexPath: IndexPath) -> 
               UITableViewCell {
    let cell = tableView.dequeueReusableCell( 
                         withIdentifier: "LocationCell", 
                                    for: indexPath)

    let descriptionLabel = cell.viewWithTag(100) as! UILabel
    descriptionLabel.text = "If you can see this"

    let addressLabel = cell.viewWithTag(101) as! UILabel
    addressLabel.text = "Then it works!"

    return cell
  }
}
The table view with fake data
Bvo rudce mouf yiqq vefu xamu

Displaying Locations from data store

➤ Run the app and tag a handful of locations. If there is no data in the data store, then the app doesn’t have much to show…

var locations = [Location]()
override func viewDidLoad() {
  super.viewDidLoad()
  // 1
  let fetchRequest = NSFetchRequest<Location>()
  // 2
  let entity = Location.entity()
  fetchRequest.entity = entity
  // 3
  let sortDescriptor = NSSortDescriptor(key: "date", 
                                  ascending: true)
  fetchRequest.sortDescriptors = [sortDescriptor]
  do {
    // 4
    locations = try managedObjectContext.fetch(fetchRequest)
  } catch {
    fatalCoreDataError(error)
  }
}
override func tableView(_ tableView: UITableView, 
      numberOfRowsInSection section: Int) -> Int {
  return locations.count
}
override func tableView(_ tableView: UITableView,
             cellForRowAt indexPath: IndexPath) -> 
             UITableViewCell {
  let cell = tableView.dequeueReusableCell(
                       withIdentifier: "LocationCell", 
                                  for: indexPath)

  let location = locations[indexPath.row]

  let descriptionLabel = cell.viewWithTag(100) as! UILabel
  descriptionLabel.text = location.locationDescription

  let addressLabel = cell.viewWithTag(101) as! UILabel
  if let placemark = location.placemark {
    var text = ""
    if let s = placemark.subThoroughfare {
      text += s + " "
    }
    if let s = placemark.thoroughfare {
      text += s + ", "
    }
    if let s = placemark.locality {
      text += s
    }
    addressLabel.text = text
  } else {
    addressLabel.text = ""
  }
  return cell
}
fatal error: unexpectedly found nil while unwrapping an Optional value
if let tabViewControllers = tabController.viewControllers {
  // First tab
  var navController = tabViewControllers[0]            
                      as! UINavigationController
  let controller1 = navController.viewControllers.first 
                    as! CurrentLocationViewController
  controller1.managedObjectContext = managedObjectContext
  // Second tab
  navController = tabViewControllers[1] 
                  as! UINavigationController
  let controller2 = navController.viewControllers.first 
                    as! LocationsViewController
  controller2.managedObjectContext = managedObjectContext  
}
The list of Locations
Vyi buhb os Faviqoutl

Creating a custom table view cell subclass

Using viewWithTag(_:) to find the labels from the table view cell works, but it doesn’t look very object-oriented to me.

@IBOutlet weak var descriptionLabel: UILabel!
@IBOutlet weak var addressLabel: UILabel!
override func tableView(_ tableView: UITableView,
             cellForRowAt indexPath: IndexPath) -> 
             UITableViewCell {
  let cell = tableView.dequeueReusableCell(
                       withIdentifier: "LocationCell", 
                       for: indexPath) as! LocationCell

  let location = locations[indexPath.row]
  cell.configure(for: location)

  return cell
}
// MARK:- Helper Method
func configure(for location: Location) {
  if location.locationDescription.isEmpty {
    descriptionLabel.text = "(No Description)"
  } else {
    descriptionLabel.text = location.locationDescription
  }

  if let placemark = location.placemark {
    var text = ""
    if let s = placemark.subThoroughfare {
      text += s + " "
    }
    if let s = placemark.thoroughfare {
      text += s + ", "
    }
    if let s = placemark.locality {
      text += s
    }
    addressLabel.text = text
  } else {
    addressLabel.text = String(format:
      "Lat: %.8f, Long: %.8f", location.latitude, 
                               location.longitude)
  }
}

Editing locations

You will now connect the LocationsViewController to the Location Details screen, so that when you tap a row in the table, it lets you edit that location’s description and category.

Creating edit segue

➤ Go to the storyboard. Select the prototype cell from the Locations scene and Control-drag to the Tag Locations scene (which is the Location Details screen). Add a Show selection segue and name it EditLocation.

The Location Details screen is now also connected to the Locations screen
Xni Coxacueb Peguogj lyqiaq iv mug eqju lihlecroh jo txu Pesutuuvc tqkoes

// MARK:- Navigation
override func prepare(for segue: UIStoryboardSegue, 
                         sender: Any?) {
  if segue.identifier == "EditLocation" {
    let controller = segue.destination 
                     as! LocationDetailsViewController
    controller.managedObjectContext = managedObjectContext

    if let indexPath = tableView.indexPath(for: sender 
                                         as! UITableViewCell) {
      let location = locations[indexPath.row]
      controller.locationToEdit = location
    }
  }
}

The Any type

The type of the sender parameter is Any. You have seen this type in a few places before. What is it?

Setting up the edit view controller

When editing an existing Location object, you have to do a few things differently in the LocationDetailsViewController. The title of the screen shouldn’t be “Tag Location” but “Edit Location.” You also must put the values from the existing Location object into the various cells.

var locationToEdit: Location?
var descriptionText = ""	
override func viewDidLoad() {
  super.viewDidLoad()
  if let location = locationToEdit {
    title = "Edit Location"
  }
  . . .
}
descriptionTextView.text = descriptionText
var locationToEdit: Location? {
  didSet {
    if let location = locationToEdit {
      descriptionText = location.locationDescription
      categoryName = location.category
      date = location.date
      coordinate = CLLocationCoordinate2DMake(
        location.latitude, location.longitude)
      placemark = location.placemark
    }
  }
}
Editing an existing location
Oyupotv ax arandahp midenaas

Fixing the edit screen

There are two problems to solve:

@IBAction func done() {
  let hudView = HudView.hud(inView: . . .)

  let location: Location
  if let temp = locationToEdit {
    hudView.text = "Updated"
    location = temp
  } else {
    hudView.text = "Tagged"
    location = Location(context: managedObjectContext)
  }

  location.locationDescription = descriptionTextView.text
  . . .

Using NSFetchedResultsController

As you are no doubt aware by now, table views are everywhere in iOS apps. A lot of the time when you’re working with Core Data, you want to fetch objects from the data store and show them in a table view. And when those objects change, you want to do a live update of the table view in response, to show the changes to the user.

lazy var fetchedResultsController: 
         NSFetchedResultsController<Location> = {
  let fetchRequest = NSFetchRequest<Location>()

  let entity = Location.entity()
  fetchRequest.entity = entity
  
  let sortDescriptor = NSSortDescriptor(key: "date", 
                                  ascending: true)
  fetchRequest.sortDescriptors = [sortDescriptor]
  
  fetchRequest.fetchBatchSize = 20
  
  let fetchedResultsController = NSFetchedResultsController(
            fetchRequest: fetchRequest, 
    managedObjectContext: self.managedObjectContext,
      sectionNameKeyPath: nil, cacheName: "Locations")
  
  fetchedResultsController.delegate = self
  return fetchedResultsController
}()
fetchRequest.fetchBatchSize = 20
let fetchedResultsController = NSFetchedResultsController(
  fetchRequest: fetchRequest,
  managedObjectContext: self.managedObjectContext,
  sectionNameKeyPath: nil, cacheName: "Locations")
override func viewDidLoad() {
  super.viewDidLoad()
  performFetch()
}

// MARK:- Helper methods
func performFetch() {
  do {
    try fetchedResultsController.performFetch()
  } catch {
    fatalCoreDataError(error)
  }
}
deinit {
  fetchedResultsController.delegate = nil
}
override func tableView(_ tableView: UITableView, 
      numberOfRowsInSection section: Int) -> Int {
  let sectionInfo = fetchedResultsController.sections![section] 
  return sectionInfo.numberOfObjects
}
override func tableView(_ tableView: UITableView, 
             cellForRowAt indexPath: IndexPath) -> 
             UITableViewCell {
  let cell = tableView.dequeueReusableCell(
        withIdentifier: "LocationCell", 
                   for: indexPath) as! LocationCell

  let location = fetchedResultsController.object(at: indexPath)
  cell.configure(for: location)
  
  return cell
}

Organizing the code using extensions

An extension lets you add code to an existing class, without having to modify the original class source code. When you make an extension you say, “here are a bunch of extra methods that also need to go into that class,” and you can do that even if you didn’t write the original class to begin with.

// MARK:- NSFetchedResultsController Delegate Extension
extension LocationsViewController: 
          NSFetchedResultsControllerDelegate {
  
  func controllerWillChangeContent(_ controller: 
          NSFetchedResultsController<NSFetchRequestResult>) {
    print("*** controllerWillChangeContent")
    tableView.beginUpdates()
  }
  
  func controller(_ controller: 
          NSFetchedResultsController<NSFetchRequestResult>, 
          didChange anObject: Any, at indexPath: IndexPath?, 
          for type: NSFetchedResultsChangeType, 
          newIndexPath: IndexPath?) {

    switch type {
    case .insert:
      print("*** NSFetchedResultsChangeInsert (object)")
      tableView.insertRows(at: [newIndexPath!], with: .fade)
      
    case .delete:
      print("*** NSFetchedResultsChangeDelete (object)")
      tableView.deleteRows(at: [indexPath!], with: .fade)
      
    case .update:
      print("*** NSFetchedResultsChangeUpdate (object)")
      if let cell = tableView.cellForRow(at: indexPath!) 
                                      as? LocationCell {
        let location = controller.object(at: indexPath!) 
                       as! Location
        cell.configure(for: location)
      }

    case .move:
      print("*** NSFetchedResultsChangeMove (object)")
      tableView.deleteRows(at: [indexPath!], with: .fade)
      tableView.insertRows(at: [newIndexPath!], with: .fade)
    }
    
    @unknown default:
      fatalError("Unhandled switch case of NSFetchedResultsChangeType")
    }
  }
  
  func controller(_ controller: 
          NSFetchedResultsController<NSFetchRequestResult>, 
          didChange sectionInfo: NSFetchedResultsSectionInfo, 
          atSectionIndex sectionIndex: Int, 
          for type: NSFetchedResultsChangeType) {
    switch type {
    case .insert:
      print("*** NSFetchedResultsChangeInsert (section)")
      tableView.insertSections(IndexSet(integer: sectionIndex), 
                                           with: .fade)
    case .delete:
      print("*** NSFetchedResultsChangeDelete (section)")
      tableView.deleteSections(IndexSet(integer: sectionIndex),
                                           with: .fade)
    case .update:
      print("*** NSFetchedResultsChangeUpdate (section)")    
    case .move:
      print("*** NSFetchedResultsChangeMove (section)")
    }
    @unknown default:
      fatalError("Unhandled switch case of NSFetchedResultsChangeType")
    }
  }
  
  func controllerDidChangeContent(_ controller:
          NSFetchedResultsController<NSFetchRequestResult>) {
    print("*** controllerDidChangeContent")
    tableView.endUpdates()
  }
}
*** controllerWillChangeContent
*** NSFetchedResultsChangeUpdate (object)
*** controllerDidChangeContent
*** controllerWillChangeContent
*** NSFetchedResultsChangeInsert (object)
*** controllerDidChangeContent

“It’s not a bug, it’s an undocumented feature”

There is a nasty Core Data bug that has been there for the last few iOS versions. Here is how you can reproduce it:

CoreData: FATAL ERROR: The persistent cache of section information does not match the current configuration.  You have illegally mutated the NSFetchedResultsController’s fetch request, its predicate, or its sort descriptor without either disabling caching or using +deleteCacheWithName:
NSFetchedResultsController<Location>.deleteCache(withName: "Locations")
let _ = controller2.view

Deleting locations

Everyone makes mistakes. So, it’s likely that users will want to delete locations from their list at some point. This is a very easy feature to add: you just have to remove the Location object from the data store and the NSFetchedResultsController will make sure it gets dropped from the table — again, through its delegate methods.

override func tableView(_ tableView: UITableView, 
              commit editingStyle: UITableViewCell.EditingStyle, 
              forRowAt indexPath: IndexPath) {
  if editingStyle == .delete {
    let location = fetchedResultsController.object(at: 
                                             indexPath)
    managedObjectContext.delete(location)
    do {
      try managedObjectContext.save()
    } catch {
      fatalCoreDataError(error)
    }
  }
}
Swipe to delete rows from the table
Hpuwa ye wovelu qusn btiy jxa miwze

navigationItem.rightBarButtonItem = editButtonItem
The table view in edit mode
Yki foxro gauy en oqer daye

Table view sections

The Location objects have a category field. It would be nice to group the locations by category in the table. The table view supports organizing rows into sections and each of these sections can have its own header. Putting your rows into sections is a lot of work if you’re doing it by hand, but NSFetchedResultsController practically gives you section support for free.

lazy var fetchedResultsController: . . . = {
  . . .
  let sort1 = NSSortDescriptor(key: "category", ascending: true)
  let sort2 = NSSortDescriptor(key: "date", ascending: true)
  fetchRequest.sortDescriptors = [sort1, sort2]
  . . .
  let fetchedResultsController = NSFetchedResultsController(
    fetchRequest: fetchRequest,
    managedObjectContext: self.managedObjectContext,
    sectionNameKeyPath: "category",              // change this
    cacheName: "Locations")
override func numberOfSections(in tableView: UITableView) 
              -> Int {
  return fetchedResultsController.sections!.count
}

override func tableView(_ tableView: UITableView, 
    titleForHeaderInSection section: Int) -> String? {
  let sectionInfo = fetchedResultsController.sections![section]
  return sectionInfo.name
}
The locations are now grouped in sections
Zso nagazoobj ezi lug ptuihow uf kawciulx

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