Learn how to fetch remote data with core data in the iOS app and like how it works in this case.
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 the model layer inside an app such as:
We create engaging mobile experiences for Apple smartphones and gadgets, Get consulted today!!!
- 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 options for backing store.
With a lot of advanced features offered automatically out of the box by Core Data, it has a sheer learning curve for developers to study and use for the first time.
Earlier iOS 10, to set up Core Data in our application there are several configurations and boilerplate codes 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.
How fetching Remote Data with core data in the iOS app works?
In this blog, we will construct a simple demo app that obtains a list of films from the remote Star Wars API and sync the data inside the Core Data store using a background queue naively without a 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 the list of film data from StarWars API using URL Session Data Task.
DataProvider: A class that offers an interface to fetch a list of films from the data repository and sync it to the Core Data store using NSManagedObjectContext in a background thread.
FilmsViewController: View Controller that converses with the data provider and uses NSFetchedResultsController to fetch and examine alter from Core Data View Context, then show a 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 a new file for the 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 an associated type, the property also wants to be declared with @NSManaged keyword for the compiler to recognize that this property will use Core Data at its backing store.
We create engaging mobile experiences for Apple smartphones and gadgets, Get consulted today!!!
We want to use NSNumber for primitive types 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 set up 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 a 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 a list of films from the network. It offers a 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 to fetch core data in ios app
The next file we want to create is the DataProvider class. This class’s 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.
We create engaging mobile experiences for Apple smartphones and gadgets, Get consulted today!!!
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 matches 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 begins.
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 explain 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.
We create engaging mobile experiences for Apple smartphones and gadgets, Get consulted today!!!
The TableViewDataSource methods will inquire the NSFetchedResultsController for its section, the number of rows in a section, and the actual data for the table view cell at specified IndexPath. We set the text label and detailed 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 a fine-grained TableView update with animation here if you want using the index paths 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() } }
Final words
Hope the above helped you a better way in fetching core data in the iOS app with few steps.
Leave a Reply