Hint (before starting the coredata part of this tutorial): This tutorial is based on the first two parts, see the first part here and the second part here. You should have them done before starting with this part.
Now we are going to add coredata to the project. This way posts and comments will be saved in the coredata database. Additionally, we will add a button to delete the contents of the database and another button to sync the data between the blog and coredata.
Adding coredata to the project
Usually there is an option to use coredata while adding a project in Xcode. Since we already have a project, we need to add coredata the following way: Select your project from the files, click the tab “Build Phases” and open “Link Binary With Libraries”. There is a button representing a “+” which you should click:
After this a dialog opens, search for coredata, select CoreData.framework and click “Add”:
Now you should add the model file of coredata to the project. Click “File” -> “New” -> “File”, select the register “Core Data” and choose the first option called “Data Model”:
Click “Next” and choose a name (I chose “wpconnect.xcdatamodeld”).
Defining the coredata data model
Open the data model you just added to the project. In the bottom right corner you will see two options: “Editor” and “Style”. Open the “Editor” to mode. Use the button “Add Entity” to add two entities, call the first one “Post” and the second one “PostComment”. For the entity “Post”, add an attribute “postId” with the type “Integer 16” and another attribute “title” with the type “String”. Add a relationship called “comments” with the destination “PostComment”. After clicking the relationship “comments”, you will see an relationship inspector on the right side. Set type to “To Many” (because a post has 0..* comments).
For the entity “PostComment”, add an attribute “comId” with the type “Integer 16” and another attribute “text” with the type “String”. Add a relationship “post” and set the destination to “Post”. The type needs to stay “To One”, because a comment always belongs to a post.
New model classes from coredata
During the previous two steps of the tutorial we already created two model classes: Post and PostComment. Delete these four files (both header and implementation). We will generate these classes via the coredata framework. Click on “File” -> “New” -> “File” and choose “NSManagedObject subclass” from the Core Data register:
In the consecutive views, select the data model (wpconnect in our case) and the two classes “Post” and “PostComment”. After generating, the classes should look like this:
Post.h
#import <Foundation/Foundation.h> #import <CoreData/CoreData.h> @class PostComment; @interface Post : NSManagedObject @property (nonatomic, retain) NSNumber * postId; @property (nonatomic, retain) NSString * title; @property (nonatomic, retain) NSSet *comments; @end @interface Post (CoreDataGeneratedAccessors) - (void)addCommentsObject:(PostComment *)value; - (void)removeCommentsObject:(PostComment *)value; - (void)addComments:(NSSet *)values; - (void)removeComments:(NSSet *)values; @end
Post.m
#import "Post.h" #import "PostComment.h" @implementation Post @dynamic postId; @dynamic title; @dynamic comments; @end
PostComment.h
#import <Foundation/Foundation.h> #import <CoreData/CoreData.h> @class Post; @interface PostComment : NSManagedObject @property (nonatomic, retain) NSNumber * comId; @property (nonatomic, retain) NSString * text; @property (nonatomic, retain) Post *post; @end
PostComment.m
#import "PostComment.h" #import "Post.h" @implementation PostComment @dynamic comId; @dynamic text; @dynamic post; @end
Further implementation for coredata: Since we added coredata to an existing project, we need to adjust also both AppDelegate files. In the header file (AppDelegate.h) add the following properties:
@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext; @property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel; @property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;
In the implementation file (AppDelegate.m) add the following lines before “@end”:
#pragma mark - Core Data stack @synthesize persistentStoreCoordinator = _persistentStoreCoordinator; @synthesize managedObjectModel = _managedObjectModel; @synthesize managedObjectContext = _managedObjectContext; - (NSURL *)applicationDocumentsDirectory { // The directory the application uses to store the Core Data store file. This code uses a directory named "me.meberhard.AnotherText" in the user's Application Support directory. NSURL *appSupportURL = [[[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask] lastObject]; return [appSupportURL URLByAppendingPathComponent:@"me.meberhard.WordpressConnect"]; } - (NSManagedObjectModel *)managedObjectModel { // The managed object model for the application. It is a fatal error for the application not to be able to find and load its model. if (_managedObjectModel) { return _managedObjectModel; } NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"wpconnect" withExtension:@"momd"]; _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL]; return _managedObjectModel; } - (NSPersistentStoreCoordinator *)persistentStoreCoordinator { // The persistent store coordinator for the application. This implementation creates and return a coordinator, having added the store for the application to it. (The directory for the store is created, if necessary.) if (_persistentStoreCoordinator) { return _persistentStoreCoordinator; } NSFileManager *fileManager = [NSFileManager defaultManager]; NSURL *applicationDocumentsDirectory = [self applicationDocumentsDirectory]; BOOL shouldFail = NO; NSError *error = nil; NSString *failureReason = @"There was an error creating or loading the application's saved data."; // Make sure the application files directory is there NSDictionary *properties = [applicationDocumentsDirectory resourceValuesForKeys:@[NSURLIsDirectoryKey] error:&error]; if (properties) { if (![properties[NSURLIsDirectoryKey] boolValue]) { failureReason = [NSString stringWithFormat:@"Expected a folder to store application data, found a file (%@).", [applicationDocumentsDirectory path]]; shouldFail = YES; } } else if ([error code] == NSFileReadNoSuchFileError) { error = nil; [fileManager createDirectoryAtPath:[applicationDocumentsDirectory path] withIntermediateDirectories:YES attributes:nil error:&error]; } if (!shouldFail && !error) { NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]]; NSURL *url = [applicationDocumentsDirectory URLByAppendingPathComponent:@"OSXCoreDataObjC.storedata"]; if (![coordinator addPersistentStoreWithType:NSXMLStoreType configuration:nil URL:url options:nil error:&error]) { coordinator = nil; } _persistentStoreCoordinator = coordinator; } if (shouldFail || error) { // Report any error we got. NSMutableDictionary *dict = [NSMutableDictionary dictionary]; dict[NSLocalizedDescriptionKey] = @"Failed to initialize the application's saved data"; dict[NSLocalizedFailureReasonErrorKey] = failureReason; if (error) { dict[NSUnderlyingErrorKey] = error; } error = [NSError errorWithDomain:@"YOUR_ERROR_DOMAIN" code:9999 userInfo:dict]; [[NSApplication sharedApplication] presentError:error]; } return _persistentStoreCoordinator; } - (NSManagedObjectContext *)managedObjectContext { // Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) if (_managedObjectContext) { return _managedObjectContext; } NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; if (!coordinator) { return nil; } _managedObjectContext = [[NSManagedObjectContext alloc] init]; [_managedObjectContext setPersistentStoreCoordinator:coordinator]; return _managedObjectContext; } #pragma mark - Core Data Saving and Undo support - (IBAction)saveAction:(id)sender { // Performs the save action for the application, which is to send the save: message to the application's managed object context. Any encountered errors are presented to the user. if (![[self managedObjectContext] commitEditing]) { NSLog(@"%@:%@ unable to commit editing before saving", [self class], NSStringFromSelector(_cmd)); } NSError *error = nil; if ([[self managedObjectContext] hasChanges] && ![[self managedObjectContext] save:&error]) { [[NSApplication sharedApplication] presentError:error]; } } - (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window { // Returns the NSUndoManager for the application. In this case, the manager returned is that of the managed object context for the application. return [[self managedObjectContext] undoManager]; } - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { // Save changes in the application's managed object context before the application terminates. if (!_managedObjectContext) { return NSTerminateNow; } if (![[self managedObjectContext] commitEditing]) { NSLog(@"%@:%@ unable to commit editing to terminate", [self class], NSStringFromSelector(_cmd)); return NSTerminateCancel; } if (![[self managedObjectContext] hasChanges]) { return NSTerminateNow; } NSError *error = nil; if (![[self managedObjectContext] save:&error]) { // Customize this code block to include application-specific recovery steps. BOOL result = [sender presentError:error]; if (result) { return NSTerminateCancel; } NSString *question = NSLocalizedString(@"Could not save changes while quitting. Quit anyway?", @"Quit without saves error question message"); NSString *info = NSLocalizedString(@"Quitting now will lose any changes you have made since the last successful save", @"Quit without saves error question info"); NSString *quitButton = NSLocalizedString(@"Quit anyway", @"Quit anyway button title"); NSString *cancelButton = NSLocalizedString(@"Cancel", @"Cancel button title"); NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:question]; [alert setInformativeText:info]; [alert addButtonWithTitle:quitButton]; [alert addButtonWithTitle:cancelButton]; NSInteger answer = [alert runModal]; if (answer == NSAlertFirstButtonReturn) { return NSTerminateCancel; } } return NSTerminateNow; }
These lines are usually auto-generated when you add an application with coredata.
Further implementation – post coredata
After taking care of adding the coredata framework, the data model, the model classes and adjusting AppDelegate, we can now adjust the implementation. We assume the following workflow:
- The application has two tables, one displays the posts and the other displays comments on posts.
- We have two buttons: One for syncing the coredata database with the blog, and another one to wipe the coredata database.
- When the application start, show all posts which are currently available in the coredata database.
- When the user clicks syncs, download new posts and comments from the blog (we will not add logic for updating changed posts or for deleting not available posts – in order to keep it simple).
- When the user deletes the contents of the coredata database, the table views will be empty, because there are no posts and comments available anymore.
The logic in AppDelegate.m:
#import "AppDelegate.h" #import "Post.h" #import "PostComment.h" @interface AppDelegate () @end @implementation AppDelegate - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { // Insert code here to initialize your application [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(flushData) name:@"flushData" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(syncData) name:@"syncData" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(getCommentsForPostId:) name:@"getComments" object:nil]; [self showPostsInView]; } - (void)applicationWillTerminate:(NSNotification *)aNotification { // Insert code here to tear down your application } - (void)syncData { [self receivePostsFromBlog]; } - (void)receivePostsFromBlog { NSURL *url = [NSURL URLWithString:@"http://meberhard.me/wp-json/posts"]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *repsone, NSData *data, NSError *connectionError) { if (data.length > 0 && connectionError == nil) { NSDictionary *wpPosts = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL]; for (id key in wpPosts) { NSNumber *postId = [NSNumber numberWithInt:((int)[[key objectForKey:@"ID"] integerValue])]; if (![self checkIfPostIdExists:postId]) { NSString *postTitle = [key objectForKey:@"title"]; NSManagedObjectContext *context = [self managedObjectContext]; Post *post = [NSEntityDescription insertNewObjectForEntityForName:@"Post" inManagedObjectContext:context]; post.postId = postId; post.title = postTitle; NSError *error; if (![context save:&error]) { NSLog(@"Something went wrong: %@", [error localizedDescription]); } else { [self receiveCommentsFromBlogForPost:post]; } } else { NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"Post" inManagedObjectContext:[self managedObjectContext]]; [fetchRequest setEntity:entity]; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"postId == %@", postId]; [fetchRequest setPredicate:predicate]; NSError *error; NSArray *items = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; Post *post = [items objectAtIndex:0]; [self receiveCommentsFromBlogForPost:post]; NSLog(@"Post with id %@ exists in DB, skipping", postId); } } } [self showPostsInView]; }]; } - (void)receiveCommentsFromBlogForPost:(Post *)post { NSString *restUrl = [NSString stringWithFormat:@"http://meberhard.me/wp-json/posts/%ld/comments", [post.postId integerValue]]; NSURL *url = [NSURL URLWithString:restUrl]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) { if (data.length > 0 && connectionError == nil) { NSDictionary *wpComments = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL]; for (id key in wpComments) { NSNumber *commentId = [NSNumber numberWithInt:((int)[[key objectForKey:@"ID"] integerValue])]; if (![self checkIfCommentIdExists:commentId]) { NSMutableString *commentText = [[NSMutableString alloc] init]; [commentText appendString:[key objectForKey:@"content"]]; NSManagedObjectContext *context = [self managedObjectContext]; PostComment *pcom = [NSEntityDescription insertNewObjectForEntityForName:@"PostComment" inManagedObjectContext:context]; pcom.comId = commentId; pcom.text = commentText; pcom.post = post; [post addCommentsObject:pcom]; NSError *error; if (![context save:&error]) { NSLog(@"Something went wrong: %@", [error localizedDescription]); } } else { NSLog(@"Comment with comment id %@ exists, skipping", commentId); } } } }]; } - (void)showPostsInView { NSLog(@"starting with showPostsInView"); NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"Post" inManagedObjectContext:[self managedObjectContext]]; [fetchRequest setEntity:entity]; NSError *error; NSArray *fetchedObjects = [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error]; [[NSNotificationCenter defaultCenter] postNotificationName:@"showPosts" object:fetchedObjects]; } - (void)flushData { NSArray *entities = self.managedObjectModel.entities; for (NSEntityDescription *entityDescription in entities) { [self deleteAllObjectsWithEntityName:entityDescription.name]; } [[NSNotificationCenter defaultCenter] postNotificationName:@"showPosts" object:nil]; [[NSNotificationCenter defaultCenter] postNotificationName:@"showComments" object:nil]; } - (void)deleteAllObjectsWithEntityName:(NSString*)entityName { NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:entityName]; fetchRequest.includesPropertyValues = NO; fetchRequest.includesSubentities = NO; NSError *error; NSArray *items = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; for (NSManagedObject *managedObject in items) { [self.managedObjectContext deleteObject:managedObject]; } } - (void)getCommentsForPostId:(NSNotification *)notification { NSNumber *postId = [notification object]; NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"Post" inManagedObjectContext:[self managedObjectContext]]; [fetchRequest setEntity:entity]; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"postId == %@", postId]; [fetchRequest setPredicate:predicate]; NSError *error; NSArray *items = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; Post *post = [items objectAtIndex:0]; NSArray *comments = [[post comments] allObjects]; [[NSNotificationCenter defaultCenter] postNotificationName:@"showComments" object:comments]; } - (BOOL)checkIfPostIdExists:(NSNumber*)postId { NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"Post" inManagedObjectContext:[self managedObjectContext]]; [fetchRequest setEntity:entity]; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"postId == %@", postId]; [fetchRequest setPredicate:predicate]; NSError *error; NSArray *items = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; return ([items count] > 0); } - (BOOL)checkIfCommentIdExists:(NSNumber*)commentId { NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"PostComment" inManagedObjectContext:[self managedObjectContext]]; [fetchRequest setEntity:entity]; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"comId == %@", commentId]; [fetchRequest setPredicate:predicate]; NSError *error; NSArray *items = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; return ([items count] > 0); } #pragma mark - Core Data stack .....
In applicationDidFinishLaunching we define three observers: The first one should be triggered, if the user clicks on the flushData button in the view, it calls the method “flushData”. The second one is triggered when the user clicks the “sync” button, it calls the method “syncData”. The last observer gets the comments for a certain post, so the ViewController gets the data for the comment table. The call “showPostsInView” starts the application and displays the available posts per method call.
The method “receivePostsFromBlog” gets the posts from the defined URL. I iterates over the posts in the JSON answer. For every id of a post it checks, if there is already such an id in the coredata database. If such an id exists, it skips adding of the posts and calls the next method (which checks comments for a post) directly. If not, it adds the posts to the coredata database and consecutively calls the method which checks for comments.
“receiveCommentsFromBlogForPost” works the same way as “receivePostsFromBlog”. I receives comments for a certain posts, iterates over the results and adds the comment to the coredata database if it does not already exist.
“showPostsInView” queries the coredata database and receives all posts. After, it posts a notification called “showPosts” containing the posts as an object.
“flushData” and “deleteAllObjectsWithEntityName” are both used to flush the coredata database. One call to “flushData” is sufficient. “getCommentsForPostId” queries the coredata database for comments, which are assigned to a post. After, it posts a notification called “showComments”, containing the comments as an object.
“checkIfPostIdExists” and “checkIfCommentIdExists” are both helper methods for the first two methods. They check, if a post/comment with a certain ID exists in the coredata database and return NO/YES accordingly.
Changing the storyboard and the ViewController
Using the storyboard, we will add two buttons under the table views. Add two “Push Buttons” and drag them onto the storyboard:
In ViewController.h, add the following two properties:
@property (nonatomic, strong) IBOutlet NSButton *deleteData; @property (nonatomic, strong) IBOutlet NSButton *syncButton;
Connect these properties with the according button of the storyboard using the drag and drop feature of Xcode:
The implementation of ViewController.m looks like this:
#import "ViewController.h" #import "Post.h" #import "PostComment.h" @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. [self.tablePosts setDelegate:self]; [self.tablePosts setDataSource:self]; [self.tableComments setDelegate:self]; [self.tableComments setDataSource:self]; [self.deleteData setTarget:self]; [self.deleteData setAction:@selector(buttonDeleteDataClick)]; [self.syncButton setTarget:self]; [self.syncButton setAction:@selector(syncButtonClick)]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(setPostsToDisplay:) name:@"showPosts" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(setCommentsToDisplay:) name:@"showComments" object:nil]; } - (void)setRepresentedObject:(id)representedObject { [super setRepresentedObject:representedObject]; // Update the view, if already loaded. } - (void)setPostsToDisplay:(NSNotification*)notification { self.displayPosts = [notification object]; [self.tablePosts reloadData]; } - (void)setCommentsToDisplay:(NSNotification*)notification { self.displayComments = [notification object]; [self.tableComments reloadData]; } - (NSView*)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { NSTableCellView *cellView = [tableView makeViewWithIdentifier:tableColumn.identifier owner:self]; if ([tableView.identifier isEqualToString:@"TablePosts"]) { Post *post = [self.displayPosts objectAtIndex:row]; if ([tableColumn.identifier isEqualToString:@"ColumnPostId"]) { cellView.textField.integerValue = [post.postId integerValue]; } else if ([tableColumn.identifier isEqualToString:@"ColumnPostTitle"]) { cellView.textField.stringValue = post.title; } } else if ([tableView.identifier isEqualToString:@"TableComments"]) { PostComment *pcom = [self.displayComments objectAtIndex:row]; if ([tableColumn.identifier isEqualToString:@"ColumnCommentId"]) { cellView.textField.integerValue = [pcom.comId integerValue]; } else if ([tableColumn.identifier isEqualToString:@"ColumnCommentText"]) { cellView.textField.stringValue = pcom.text; } } return cellView; } - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { if ([tableView.identifier isEqualToString:@"TablePosts"]) { return [self.displayPosts count]; } else if ([tableView.identifier isEqualToString:@"TableComments"]) { return [self.displayComments count]; } return 0; } - (void)tableViewSelectionDidChange:(NSNotification *)notification { if ([[notification.object identifier] isEqualToString:@"TablePosts"]) { NSInteger row = [notification.object selectedRow]; NSTextField *tf = [[[notification.object viewAtColumn:0 row:row makeIfNecessary:NO] subviews] lastObject]; NSNumber *postId = [NSNumber numberWithInt:(int)[tf integerValue]]; [[NSNotificationCenter defaultCenter] postNotificationName:@"getComments" object:postId]; } } - (void)buttonDeleteDataClick { [[NSNotificationCenter defaultCenter] postNotificationName:@"flushData" object:nil]; } - (void)syncButtonClick { [[NSNotificationCenter defaultCenter] postNotificationName:@"syncData" object:nil]; } @end
We added “setDelegate” and “setAction” for the two buttons. Furthermore, there are methods which are handling the clicks on these buttons “buttonDeleteDataClick” and “syncButtonClick”. Both post notifications which will trigger the according action. Another change (compared to the previous step of the tutorial) is in the method “viewForTableColumn”. Since we generated the model classes via coredata, postId is not an NSInteger but a NSNUmber. This means, we should access the id via method “[post.postId integerValue]” rather than “post.postId”.
After running the application, you should be able to sync and delete data. If new posts or comments are available on the blog, one click on sync will get that data! From now you could implement further logic to receive more information on posts and comments, or to update changed contents, or to even regard changed contents on update procedures.
The whole content is available at GitHub, click here.
Other parts of the tutorial:
First step: Implemented the basic application logic, posts and comments exist only as mock-data. Click here to view the post.
Second step: Connecting the application to a wordpress blog so it used the service. Click here to view the post.