Fetching Remote Data with core data in iOS app

Core Data is an object graph & persistence framework offered by Apple for developing iOS Apps. It holds object life cycle, object graph management, and persistence. It sustains various features for handling model layer inside an app such as:

  • Relationship management among objects.
  • Alter tracking with Undo Manager
  • Idle loading for objects and properties
  • Validation
  • By using NSPredicate Grouping, filtering, querying
  • Schema migration
  • Exploit SQLite as one of its option for backing store.

With lot of advanced features offered automatically out of the box by Core Data, it has sheer learning curve for developer to study and use for the first time. Earlier iOS 10, to setup Core Data in our application there are several configuration and boilerplate code we want to execute to build a Core Data Stack. Fortunately in iOS 10, Apple introduced NSPersistentContainer that we can use to initialize every stack and find the NSManagedObject context with very small code.

In this blog, we will construct a simple demo app that obtain list of films from the remote Star Wars API and sync the data inside the Core Data store using background queue naively without synchronization approach. What we will build:

  • Handle Object Model Schema and Film Entity.
  • Handle Object for Film Entity.

CoreDataStack: dependable for building the NSPersistentContainer using the plan.

ApiRepository: A class liable for fetching list of films data from StarWars API using URL Session Data Task.

DataProvider: A class that offer interface to fetch list of film from data repository and sync it to the Core Data store using NSManagedObjectContext in background thread.

FilmsViewController: View Controller that converses with data provider and uses NSFetchedResultsController to fetch and examine alter from Core Data View Context, then show list of films in a UITableView.

 

Managed Object Model Schema and Film Entity

Firstly, we will execute is to build the Managed Object Model Schema that holds a Film Entity. Build New File from Xcode and choose Data Model from Core Data Template. Name the file as StarWars, it will be saved with the .xcdatamodeld as the filename extension.

Select on the Data Model file we just formed, Xcode will open Data Model Editor where we can include Entity to the Managed Object Model Schema. Click includes Entity and Set the name of the latest Entity as Film. Ensure to set the codegen is set to Manual/None so Xcode does not automatically create the Model class. Then include the entire attributes with the type like the image below:

Create Managed Object for Film Entity

Once we have formed the schema with Film Entity, we want to create new file for Film class with NSManagedObject as the superclass. This class will be used when we insert Film Entity into NSManagedObjectContext. Within we state all the properties related to the entity with associated type, the property also want to be declared with @NSManaged keyword for the compiler to recognize that this property will use Core Data at its backing store. We want to use NSNumber for primitive type like Int, Double, or Float to store the value in a ManagedObject. We also build a simple function that maps a JSON Dictionary property and allocate it to the properties of Film Managed Object.

import CoreData

class Film: NSManagedObject {
 
 @NSManaged var director: String
 @NSManaged var episodeId: NSNumber
 @NSManaged var openingCrawl: String
 @NSManaged var producer: String
 @NSManaged var releaseDate: Date
 @NSManaged var title: String
 
 static let dateFormatter: DateFormatter = {
 let df = DateFormatter()
 df.dateFormat = "YYYY-MM-dd"
 return df
 }()
 
 func update(with jsonDictionary: [String: Any]) throws {
 guard let director = jsonDictionary["director"] as? String,
 let episodeId = jsonDictionary["episode_id"] as? Int,
 let openingCrawl = jsonDictionary["opening_crawl"] as? String,
 let producer = jsonDictionary["producer"] as? String,
 let releaseDate = jsonDictionary["release_date"] as? String,
 let title = jsonDictionary["title"] as? String
 else {
 throw NSError(domain: "", code: 100, userInfo: nil)
 }
 
 self.director = director
 self.episodeId = NSNumber(value: episodeId)
 self.openingCrawl = openingCrawl
 self.producer = producer
 self.releaseDate = Film.dateFormatter.date(from: releaseDate) ?? Date(timeIntervalSince1970: 0)
 self.title = title
 }

}

Setup Core Data Stack

To setup our Core Data Stack that utilizes the Managed Object Model Schema we have formed, create a new file called CoreDataStack. It will be a Singleton class that descriptions NSPersistentContainer public variable. To initialize the container, we just pass the filename of the Managed Object Model schema which is StarWars. We also set the view NSManagedObjectContext of the container to automatically combine changes from parent, so when we utilize the background context to save the data, the changes will also be proliferated to the View Context.

import CoreData

class CoreDataStack {
 
 private init() {}
 static let shared = CoreDataStack()
 
 lazy var persistentContainer: NSPersistentContainer = {
 let container = NSPersistentContainer(name: "StarWars")
 
 container.loadPersistentStores(completionHandler: { (_, error) in
 guard let error = error as NSError? else { return }
 fatalError("Unresolved error: \(error), \(error.userInfo)")
 })
 
 container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
 container.viewContext.undoManager = nil
 container.viewContext.shouldDeleteInaccessibleFaults = true
 
 container.viewContext.automaticallyMergesChangesFromParent = true
 
 return container
 }()
 
}

ApiRepository

Then, build a new file with the name of ApiRepository. This Singleton class acts as a Networking Coordinator that attach to the SWAPI to fetch list of films from the network. It offers public method to obtain films with finishing point closure as the parameter. The closure will be raised with either of Array JSON Dictionary or an error in case of an error happens when fetching or parsing the JSON data from the response.

import Foundation

class ApiRepository {
 
 private init() {}
 static let shared = ApiRepository()
 
 private let urlSession = URLSession.shared
 private let baseURL = URL(string: "https://swapi.co/api/")!
 
 func getFilms(completion: @escaping(_ filmsDict: [[String: Any]]?, _ error: Error?) -> ()) {
 let filmURL = baseURL.appendingPathComponent("films")
 urlSession.dataTask(with: filmURL) { (data, response, error) in
 if let error = error {
 completion(nil, error)
 return
 }
 
 guard let data = data else {
 let error = NSError(domain: dataErrorDomain, code: DataErrorCode.networkUnavailable.rawValue, userInfo: nil)
 completion(nil, error)
 return
 }
 
 do {
 let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
 guard let jsonDictionary = jsonObject as? [String: Any], let result = jsonDictionary["results"] as? [[String: Any]] else {
 throw NSError(domain: dataErrorDomain, code: DataErrorCode.wrongDataFormat.rawValue, userInfo: nil)
 }
 completion(result, nil)
 } catch {
 completion(nil, error)
 }
 }.resume()
 }
 
}

DataProvider

The next file we want to create is the DataProvider class. This class responsibility is to act as Sync Coordinator to get data using the ApiRepository and store the data to the Core Data Store. It admits the repository and NSPersistent container as the initializer parameters and stores it inside the instance variable. It also exposes a public variable for the View NSManagedObjectContext that uses the NSPersistetContainer View Context.

The fetchFilms function can be used by the customer of the class to generate the synchronization to the API repository to obtain the films. After the data has been received, we initialize a Background NSManagedObjectContext using the NSPersistentContainer newBackgroundContext method.

We utilize the NSManagedObjectContext synchronous perform And Wait function to execute our data synchronization. The synchronization just performs a naive synchronization technique by:

Discover every film that match all the episode id we recover from the network inside our current Core Data Store using NSPredicate. To be proficient, we are not retrieving the actual object only the NSManagedObjectID.

Delete the films found in our store using NSBatchDeleteRequest.

Add all the films using the response from the repository.

Update the property of the films using the JSON Dictionary.

Save the result to the Core Data Store.

Changes will be automatically merged to the View Context

Integration with View Controller (UI)

class DataProvider {
 
 private let persistentContainer: NSPersistentContainer
 private let repository: ApiRepository
 
 var viewContext: NSManagedObjectContext {
 return persistentContainer.viewContext
 }
 
 init(persistentContainer: NSPersistentContainer, repository: ApiRepository) {
 self.persistentContainer = persistentContainer
 self.repository = repository
 }
 
 func fetchFilms(completion: @escaping(Error?) -> Void) {
 repository.getFilms() { jsonDictionary, error in
 if let error = error {
 completion(error)
 return
 }
 
 guard let jsonDictionary = jsonDictionary else {
 let error = NSError(domain: dataErrorDomain, code: DataErrorCode.wrongDataFormat.rawValue, userInfo: nil)
 completion(error)
 return
 }
 
 let taskContext = self.persistentContainer.newBackgroundContext()
 taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
 taskContext.undoManager = nil
 
 _ = self.syncFilms(jsonDictionary: jsonDictionary, taskContext: taskContext)
 
 completion(nil)
 }
 }
 
 private func syncFilms(jsonDictionary: [[String: Any]], taskContext: NSManagedObjectContext) -> Bool {
 var successfull = false
 taskContext.performAndWait {
 let matchingEpisodeRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Film")
 let episodeIds = jsonDictionary.map { $0["episode_id"] as? Int }.compactMap { $0 }
 matchingEpisodeRequest.predicate = NSPredicate(format: "episodeId in %@", argumentArray: [episodeIds])
 
 let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: matchingEpisodeRequest)
 batchDeleteRequest.resultType = .resultTypeObjectIDs
 
 // Execute the request to de batch delete and merge the changes to viewContext, which triggers the UI update
 do {
 let batchDeleteResult = try taskContext.execute(batchDeleteRequest) as? NSBatchDeleteResult
 
 if let deletedObjectIDs = batchDeleteResult?.result as? [NSManagedObjectID] {
 NSManagedObjectContext.mergeChanges(fromRemoteContextSave: [NSDeletedObjectsKey: deletedObjectIDs],
 into: [self.persistentContainer.viewContext])
 }
 } catch {
 print("Error: \(error)\nCould not batch delete existing records.")
 return
 }
 
 // Create new records.
 for filmDictionary in jsonDictionary {
 
 guard let film = NSEntityDescription.insertNewObject(forEntityName: "Film", into: taskContext) as? Film else {
 print("Error: Failed to create a new Film object!")
 return
 }
 
 do {
 try film.update(with: filmDictionary)
 } catch {
 print("Error: \(error)\nThe quake object will be deleted.")
 taskContext.delete(film)
 }
 }
 
 // Save all the changes just made and reset the taskContext to free the cache.
 if taskContext.hasChanges {
 do {
 try taskContext.save()
 } catch {
 print("Error: \(error)\nCould not save Core Data context.")
 }
 taskContext.reset() // Reset the context to clean up the cache and low the memory footprint.
 }
 successfull = true
 }
 return successfull
 }
}

Inside the Main.storyboard drag a UITableViewController and build one prototype table view cell with “Cell” as the identifier and Subtitle as the style. Ensure to implant it inside UINavigationController and set it as the initial view controller.

Build a new File with the name of FilmListViewController. The FilmListViewController inherits from UITableViewController as the superclass. Inside there are 2 instance properties we want to state:

DataProvider: The DataProvider class will utilize to trigger the synchronization of the films. It will be inserted from the AppDelegate when the application begin.

NSFetchedResultsController: NSFetchedResultsController is Apple Core Data class that performs a controller that you use to handle the results of a Core Data fetch request and exhibit data to the user. It also offers delegation for the delegate to receive and react to the modifies when the related entity in the store modifies. In our case we use NSFetchRequest to fetch the Film entity, then explains it sort the result by episodic in ascending order. We initialize the NSFetchedResultController with the FetchRequest and the DataProvider View Context. The FilmListViewController will also be allocated as the delegate so it can react and update the TableView when the underlying data changes.

The TableViewDataSource methods will inquire the NSFetchedResultsController for its section, number of rows in a section, and the actual data for the table view cell at specified IndexPath. We set the text label and detail text label of the cell with the title of the film and director of the film from the Film object.

For the NSFetchedResultController delegate we overrule the controllerDidChangeObject to just refill the TableView naively for the sake of this example. You can execute fine grained TableView update with animation here if you want using the indexPaths given.

Finally, Ensure to set the class of the UITableViewController inside the storyboard to use the FilmListViewController class. Build and run the project to test.

import UIKit
import CoreData

class FilmListViewController: UITableViewController {
 
 var dataProvider: DataProvider!
 lazy var fetchedResultsController: NSFetchedResultsController<Film> = {
 let fetchRequest = NSFetchRequest<Film>(entityName:"Film")
 fetchRequest.sortDescriptors = [NSSortDescriptor(key: "episodeId", ascending:true)]
 
 let controller = NSFetchedResultsController(fetchRequest: fetchRequest,
 managedObjectContext: dataProvider.viewContext,
 sectionNameKeyPath: nil, cacheName: nil)
 controller.delegate = self
 
 do {
 try controller.performFetch()
 } catch {
 let nserror = error as NSError
 fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
 }
 
 return controller
 }()

func viewDidLoad() {
 super.viewDidLoad()
 dataProvider.fetchFilms { (error) in
 
 }
 }
 
 func numberOfSections(in tableView: UITableView) -> Int {
 return fetchedResultsController.sections?.count ?? 0
 }
 
 func tableView_default(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
 return fetchedResultsController.sections?[section].numberOfObjects ?? 0
 }
 
 func tableView_default(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
 let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
 let film = fetchedResultsController.object(at: indexPath)
 cell.textLabel?.text = film.title
 cell.detailTextLabel?.text = film.director
 return cell
 }

}

extension FilmListViewController: NSFetchedResultsControllerDelegate {
 
 func controlleraction(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
 tableView.reloadData()
 }
}

 

Last Update: September 28, 2018  

September 28, 2018   117   Nandini R    Core Data    
Total 0 Votes:
0

Tell us how can we improve this post?

+ = Verify Human or Spambot ?

Leave a Reply

Your email address will not be published. Required fields are marked *

Facebook
Twitter
INSTAGRAM
LinkedIn