Please note, this blog entry is from a previous course. You might want to check out the current one.
Lecture thirteen is named “13. Core Data (November 8, 2011)” and can be found at iTunes. Its slides are available at Stanford.
This lecture covers Core Data and Documents, NSNotificationCenter and Objective-C Categories.
Core Data in object-oriented API to store data in a database based usually on SQL. Once a visual data mapping is created between database and objects, objects are created and queried using an object-oriented API. The columns in the database table are accessed vie @properties.
A visual map is created in Xcode using New File -> Data Model. It holds entities which map to classes, attributes which map to properties and relationships which are properties pointing to other objects in the databases.
The data model is accessed via an UIManagedObject which manages the storage and provides a container of the database. It is created via
UIManagedDocument *document = [[UIManagedDocument] initWithFileURL:(URL *)url];
Because the given url has to exist this must be checked in advance using the NSFileManager:
[[NSFileManager defaultManager] fileExistsAtPath:[url path]];
If the file exists, it can be opened directly:
- (void)openWithCompletionHandler:(void (^)(BOOL success))completionHandler;
Otherwise it must be created first:
- (void)saveToURL:(NSURL *)url forSaveOperation:(UIDocumentSaveOperation)operation competionHandler:(void (^)(BOOL success))completionHandler;
The completionHandler is a block which gets executed when the operation completes. The operation itself is asynchronous. For example in the following code the document will be usable in documentIsReady – not before:
self.document = [[UIManagedDocument] initWithFileURL:(URL *)url]; if ([[NSFileManager defaultManager] fileExistsAtPath:[url path]]) { [document openWithCompletionHandler:^(BOOL success) { if (success) [self documentIsReady]; if (!success) NSLog(@“couldn’t open document at %@”, url); }]; } else { [document saveToURL:url forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) { if (success) [self documentIsReady]; if (!success) NSLog(@“couldn’t create document at %@”, url); }]; }
However, when actually using the document its state must be checked:
- (void)documentIsReady { if (self.document.documentState == UIDocumentStateNormal) { NSManagedObjectContext *context = self.document.managedObjectContext; // do something with the Core Data context } }
UIDocumentStateClosed, UIDocumentStateSavingError, UIDocumentStateEditingDisabled, or UIDocumentStateInConflict would mean that the document currently is not usable.
The state of the document can be observed using NSNotifications. Meaning the NSNotificationCenter is told to notify an observer to call a certain method when something specific happens with the document. e.g.:
- (void)addObserver:(id)observer // who gets notfied selector:(SEL)methodToSendIfSomethingHappens name:(NSString *)name // what to observing object:(id)sender; // who will be observed
The called method will receive a notification object with some information about what has happened. e.g.:
- (void)methodToSendIfSomethingHappens:(NSNotification *)notification { notification.name // the name passed above notification.object // the object sending you the notification notification.userInfo // what happened }
Its important to remove the observer when not used anymore!
Saving and closing a document are also asynchronous operations:
[self.document saveToURL:self.document.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) { if (!success) NSLog(@“failed to save document %@”, self.document.localizedName); }]; [self.document closeWithCompletionHandler:^(BOOL success) { if (!success) NSLog(@“failed to close document %@”, self.document.localizedName); }];
Multiple instances of an document is possible but they do not share the same context, meaning if one changes the other instances will not know about the change till the refetch the changed document from file.
Objects are inserted into the database via e.g.:
NSManagedObject *photo = [NSEntityDescription insertNewObjectForEntityForName:@“Photo” inManagedObjectContext:(NSManagedObjectContext *)context];
Its attributes are accessed vie valueForKey: and setValue:ForKey:. A simpler way is to create a subclass using the visual map (Create NSManagedObject Subclass …). Note because the first time this function is used not all needed information might be available, thus this function should be run twice. Now dot notation can be used, e.g.:
Photo *photo = [NSEntityDescription insertNewObjectForEntityForName:@“Photo” inManagedObj...]; NSString *myThumbnail = photo.thumbnailURL; photo.thumbnailData = [FlickrFetcher urlForPhoto:photoDictionary format:FlickrPhotoFormat...]; photo.whoTook = ...; // a Photographer object created or got by querying photo.whoTook.name = @“CS193p Instructor”; // multiple dots follow relationships
The created .h and .m files will be overwritten every time Create NSManagedObject Subclass … is used. Thus to add code to these classes categories should be used.
Categories are an Objective-C syntax for adding a class without having to subclass it or having to have access to the code of the class. e.g.:
@interface Photo (AddOn) - (UIImage *)image; @property (readonly) BOOL isOld; @end @implementation Photo (AddOn) - (UIImage *)image { ... } - (BOOL)isOld { ... } @end
Categories cannot have instance variables, thus @synthesize is not allowed in its implementation.
When an object is deleted from the database, e.g.
[self.document.managedObjectContext deleteObject:photo];
a category can be used to implement a method which handles circumstances which are effected by this deletion, e.g.
@implementation Photo (Deletion) - (void)prepareForDeletion { // but if Photographer had, for example, // a “number of photos taken” attribute, // we might adjust it down by one here self.whoTook.photoCount; } @end
Querying a database is done by NSFetchRequests, which consists of four steps:
- an entity to fetch,
- an NSPredicate specifying which entities to fetch,
- NSSortDescriptors specifying the order,
- and the maximum number of fetched objects.
e.g. the following would fetch 100 Photo entities in batch sizes of 20, sorted and limited using a filter:
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@“Photo”]; request.fetchBatchSize = 20; request.fetchLimit = 100; request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor]; request.predicate = ...;
Setting a sorting descriptor requires a key for which attribute should be sorted, an order, an an selector with the sorting algorithm, e.g.:
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@“thumbnailURL” ascending:YES selector:@selector(localizedCaseInsensitiveCompare:)];
Creating a predicate looks like creating a string, e.g.:
NSPredicate *predicate = [NSPredicate predicateWithFormat:@“thumbnailURL contains %@”, value]; // further examples @“uniqueId = %@”, [flickrInfo objectForKey:@“id”] @“name contains %@”, (NSString *) // case insensitively @“viewed > %@”, (NSDate *) @“whoTook.name = %@”, (NSString *) @“any photos.title contains %@”, (NSString *)
NSCompoundPredicate for intersecting multiple predicates.
Finally the request must be executed, e.g.:
NSManagedObjectContext *moc = self.document.managedObjectContext; NSError *error; NSArray *photographers = [moc executeFetchRequest:request error:&error];