NSIncrementalStore
Even for a blog dedicated to obscure APIs, NSIncremental
brings a new meaning to the word “obscure”.
It was introduced in iOS 5, with no more fanfare than the requisite entry in the SDK changelog.
Its programming guide weighs in at a paltry 82 words, making it the shortest by an order of magnitude.
If it weren’t for an offhand remark during WWDC 2011 Session 303, it may have gone completely unnoticed.
And yet, NSIncremental
is arguably the most important thing to come out of iOS 5.
At Last, A Foothold Into Core Data
NSIncremental
is an abstract subclass of NSPersistent
designed to “create persistent stores which load and save data incrementally, allowing for the management of large and/or shared datasets”. And while that may not sound like much, consider that nearly all of the database adapters we rely on load incrementally from large, shared data stores. What we have here is a goddamned miracle.
For those of you not as well-versed in Core Data, here’s some background:
Core Data is Apple’s framework for object relational mapping. It’s used in at least half of all of the first-party apps on Mac and iOS, as well as thousands of other third-party apps. Core Data is complex, but that’s because it solves complex problems, covering a decades-worth of one-offs and edge cases.
This is all to say that Core Data is something you should probably use in your application.
Persistent stores in Core Data are comparable to database adapters in other ORMs, such as Active Record. They respond to changes within managed object contexts and handle fetch requests by reading and writing data to some persistence layer. For most applications, that persistent layer has been a local SQLite database.
With NSIncremental
, developers now have a sanctioned, reasonable means to create a store that connects to whatever underlying backend you like–and rather simply, too. All it takes is to implement a few required methods:
Implementing an NSIncrementalStore Subclass
+type
and +initialize
NSPersistent
instances are not created directly. Instead, they follow a factory pattern similar to NSURLProtocol
or NSValue
, in that they register their classes with the NSPersistent
, which then constructs persistent store instances as necessary when -add
is called. The registered persistent store classes are identified by a unique “store type” string (NSString
is sufficient, but you could be pedantic by specifying a string that follows the convention of ending in -Type
, like NSSQLite
).
+initialize
is automatically called the first time a class is loaded, so this is a good place to register with NSPersistent
:
class Custom Incremental Store: NSIncremental Store {
override class func initialize() {
NSPersistent Store Coordinator.register Store Class(self, for Store Type:self.type)
}
class var type: String {
return NSString From Class(self)
}
}
+ (void)initialize {
[NSPersistent Store Coordinator register Store Class:self for Store Type:[self type]];
}
+ (NSString *)type {
return NSString From Class(self);
}
-load Metadata:
load
is where the incremental store has a chance to configure itself. There is, however, a bit of Kabuki theater boilerplate that’s necessary to get everything set up. Specifically, you need to set a UUID for the store, as well as the store type. Here’s what that looks like:
override func load Metadata(error: NSError Pointer) -> Bool {
self.metadata = [
NSStore UUIDKey: NSProcess Info().globally Unique String,
NSStore Type Key: self.dynamic Type.type
]
return true
}
NSMutable Dictionary *mutable Metadata = [NSMutable Dictionary dictionary];
[mutable Metadata set Value:[[NSProcess Info process Info] globally Unique String] for Key:NSStore UUIDKey];
[mutable Metadata set Value:[[self class] type] for Key:NSStore Type Key];
[self set Metadata:mutable Metadata];
-execute Request:with Context:error:
Here’s where things get interesting, from an implementation standpoint. (And where it all goes to hell, from an API design standpoint)
execute
passes an NSPersistent
, an NSManaged
and an NSError
pointer.
NSPersistent
’s role here is as a sort of abstract subclass. The request parameter will either be of type NSFetch
or an NSSave
. If it has a fetch request type, the request parameter will actually be an instance of NSFetch
, which is a subclass of NSPersistent
. Likewise, if it has a save request type, it will be an instance of NSSave
(this article was originally mistaken by stating that there was no such a class).
This method requires very specific and very different return values depending on the request parameter (and the result
, if it’s an NSFetch
). The only way to explain it is to run through all of the possibilities:
NSFetch Request Type
Request Type: - Result Type:
NSManaged
,Object Result Type NSManaged
, orObject IDResult Type NSDictionary
Result Type
Return:
NSArray
of objects matching request
- Result Type:
NSCount
Result Type
Return:
NSNumber
NSArray
containing oneNSNumber
of count of objects matching request
NSSave Request Type
Request Type: Return: Empty
NSArray
So, one method to do all read and write operations with a persistence backend. At least all of the heavy lifting goes to the same place, right?
-new Values For Object With ID:with Context:error:
This method is called when an object faults, or has its values refreshed by the managed object context.
It returns an NSIncremental
, which is a container for the ID and current values for a particular managed object. The node should include all of the attributes, as well as the managed object IDs of any to-one relationships. There is also a version
property of the node that can be used to determine the current state of an object, but this may not be applicable to all storage implementations.
If an object with the specified object
cannot be found, this method should return nil
.
-new Value For Relationship:for Object With ID: with Context:error:
This one is called when a relationship needs to be refreshed, either from a fault or by the managed object context.
Unlike the previous method, the return value will be just the current value for a single relationship. The expected return type depends on the nature of the relationship:
-
to-one:
NSManaged
Object ID -
to-many:
NSSet
orNSOrdered
Set -
non-existent:
nil
-obtain Permanent IDs For Objects:error:
Finally, this method is called before execute
with a save request, where permanent IDs should be assigned to newly-inserted objects. As you might expect, the array of permanent IDs should match up with the array of objects passed into this method.
This usually corresponds with a write to the persistence layer, such as an INSERT
statement in SQL. If, for example, the row corresponding to the object had an auto-incrementing id
column, you could generate an objectID with:
self.new Object IDFor Entity(entity, reference Object: row ID)
[self new Object IDFor Entity:entity reference Object:[NSNumber number With Unsigned Integer:row ID]];
Roll Your Own Core Data Backend
Going through all of the necessary methods to override in an NSIncremental
subclass, you may have found your mind racing with ideas about how you might implement a SQL or NoSQL store, or maybe something new altogether.
What makes NSIncremental
so exciting is that you can build a store on your favorite technology, and drop that into any existing Core Data stack with little to no additional configuration.
Even though NSIncremental
has been around since iOS 5, we’re still a long way from even beginning to realize its full potential. The future is insanely bright, so you best don your aviators, grab an iced latte and start coding something amazing.