NSIncrementalStore Tutorial for iOS: Getting Started

Learn how you can use the very powerful NSIncrementalStore to provide Core Data with a completely custom data storage option. By Rony Rozen.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Fetching Data

Now that you have everything set up, you can start implementing the fetch and save logic. You’ll start with fetching, even though there will be nothing to fetch until you actually save something. But first, a new definition:

Faults: Fetching a faulted object allows for increased flexibility as it postpones materialization of property values until they’re actually needed. When a property is accessed using the valueForKey: method, Core Data checks if the object is faulted. If so, it fetches the value from storage to the context, which fulfills the fault and returns the requested value. There’s more on the methods involved in this process in the upcoming sections.

Both fetch and save requests from the managed object context result in the persistent store coordinator invoking your persistent store’s execute(_:with:) method.

In most cases, fetch requests will result in an array of NSManagedObject instances. The properties of these objects will be faults and will only be fetched as needed (more on that later). Let’s start with the simplest fetch request: returning an array of every managed object of a single entity type – Bug.

In execute(_:with:) add the following above the return statement:

// 1
if request.requestType == .fetchRequestType {
  // 2
  let fetchRequest = request as! NSFetchRequest<NSManagedObject>
  if fetchRequest.resultType == NSFetchRequestResultType() {
    // 3
    var fetchedObjects = [NSManagedObject]()

    if bugsDB.count > 0 {
      for currentBugID in 1...bugsDB.count {
        // 4
        let objectID = self.newObjectID(for: fetchRequest.entity!, 
                                        referenceObject: currentBugID)
        let curObject = context?.object(with: objectID)
        fetchedObjects.append(curObject!)
      }
    }
    return fetchedObjects
  }

  return []
}

This is what’s happening:

  1. Make sure this is a fetch request first, otherwise you still just return an empty array.
  2. Check the request and result types to verify they indeed match a fetch request, and not, for example, a save request.
  3. Then you get all of the bugs from storage. To remind you, the “storage” you use in this case, for simplicity, is the bugsDB array that’s re-loaded from file on every app launch.
  4. Use the entity of the fetch request and the bug ID to fetch the object from the managed object context and add it to the fetched objects that will be returned. In order to understand the internal logic of the for loop, you need to take a slight detour…

Managed Object IDs

You need to be able to translate between the unique identifiers in your backing data store and the NSManagedObjectID instances you use to identify objects in memory. You will usually want to use a primary key (of type NSString or NSNumber) in your data store for this purpose.

NSIncrementalStore provides two methods for this purpose:

  • newObjectIDForEntity:referenceObject: creates a managed object ID for a given reference object.
  • referenceObjectForObjectID: retrieves reference object for a given managed object ID.

In the for loop above, you create a new managed object ID that the managed object context can use to look up the actual object. You then add this object to fetchedObjects and return that to the caller.

If you build and run your app, you’ll see not much has changed. You can still create new bugs by using either the Add or Refresh buttons, but when you terminate and relaunch the app, the content is no longer there. This makes sense, since you haven’t implemented the save logic yet. You’ll do that next.

Saving Data

When your managed object context receives a save request, it informs the persistent store coordinator, which in turn invokes the incremental store’s executeRequest:withContext:error: with a save request.

This request holds three sets of objects:

  • insertedObjects
  • updatedObject
  • deletedObjects

This NSIncrementalStore tutorial will only cover new objects. But you should know that this is the place to handle update and delete requests as well, once you have a slightly more complex backing data store.

In order to save bugs, add the following method to BugSquasherIncrementalStore.swift:

func saveBugs() {
  if let dir = FileManager.default.urls(for: .documentDirectory, 
                                        in: .userDomainMask).first {
    let path = dir.appendingPathComponent("bugs.txt")
    (bugsDB as NSArray).write(to: path, atomically: true)
  }
}

This method saves the local array to disk. It’s important to stress that this is an oversimplified approach to a database. In a real life situation, you may be using a SQL database, you may be using a distant database and communicate with it via web services, or other persistent stores. The interface remains unchanged, but the underlying backing data store implementation depends on your specific app’s needs.

Next, add this block of code to executeRequest:withContext:error:, as the else section matching if request.requestType == .fetchRequestType:

else if request.requestType == .saveRequestType {
  // 1
  let saveRequest = request as! NSSaveChangesRequest

  // 2
  if saveRequest.insertedObjects != nil {
    for bug in saveRequest.insertedObjects! {
      bugsDB.append((bug as! Bug).title)
    }
  }

  self.saveBugs()

  return [AnyObject]()
}

This is fairly straightforward:

  1. Ensure this is indeed a save request.
  2. Check whether there are any inserted objects. If so, each one of the new Bug objects is added to the bugsDB array. Once the array is up-to-date, you call saveBugs, which ensures that the array is saved to disk. After saving the new objects to your backing data store, you return an empty array to signify success.

Permanent Object IDs

When new objects are created, they’re assigned a temporary object ID. When the context is saved, your incremental store is asked to provide a permanent object ID for each of the new objects. In this simplified implementation, you’ll create a newObjectID based on the new object’s bugID field and return that as the permanent ID.

To do this, add the following method to BugSquasherIncrementalStore:

override func obtainPermanentIDs(for array: [NSManagedObject]) throws -> [NSManagedObjectID] {
  var objectIDs = [NSManagedObjectID]()
  for managedObject in array {
    let objectID = self.newObjectID(for: managedObject.entity,
                                    referenceObject: managedObject.value(forKey: "bugID")!)
    objectIDs.append(objectID)
  }

  return objectIDs
}

Almost there! There’s just one more method you need to implement that will bring it all together and allow you to build and run your app.

First, add a new property to represent the current bug ID to BugSquasherIncrementalStore:

var currentBugID = 0

Then, add this code to BugSquasherIncrementalStore:

override func newValuesForObject(with objectID: NSManagedObjectID,
                                 with context: NSManagedObjectContext) throws -> NSIncrementalStoreNode {

  let values = ["title": bugsDB[currentBugID],"bugID": currentBugID] as [String : Any]
  let node = NSIncrementalStoreNode(objectID: objectID, withValues: values, 
                                    version: UInt64(0.1))

  currentBugID += 1

  return node
}

newValuesForObject(with:with:) is called when the values for the faulted fetched objects are needed. When these values are accessed, this method will be called and asked to provide values for the fields that weren’t needed until now. This is done to allow for faster, more efficient loading.

In this method, based on the objectID received as parameter, you create a new NSIncrementalStoreNode with matching title and bug ID values.

Note: Since this NSIncrementalStore tutorial focuses on NSIncrementalStore concepts, and not a specific backing data store implementation, this method implementation is extremely simplified. It assumes that the fetch logic happens on all objects in the order in which they’re saved in the bugsDB array.

In your real-world apps, this implementation can be more complex and tailor-made to your app’s needs. For the purposes of this NSIncrementalStore tutorial, this simplified version should help you understand all the moving pieces.

Note: Since this NSIncrementalStore tutorial focuses on NSIncrementalStore concepts, and not a specific backing data store implementation, this method implementation is extremely simplified. It assumes that the fetch logic happens on all objects in the order in which they’re saved in the bugsDB array.

In your real-world apps, this implementation can be more complex and tailor-made to your app’s needs. For the purposes of this NSIncrementalStore tutorial, this simplified version should help you understand all the moving pieces.

Build and run your app. Add a few new bug entries, then terminate and relaunch the app. Your bugs should now persist between different app sessions.

You’ve replaced the underlying layer of Core Data with your own custom implementation of NSIncrementalStore and lived to brag about it. Pretty cool, right?

Next you’ll cover some more advanced topics that will be of interest as you work on more complex apps.