Monthly Archives: January 2015

Objective-c tutorial – adding coredata to save posts and comments

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:

Adding coredata to the project

Adding coredata to the project

After this a dialog opens, search for coredata, select CoreData.framework and click “Add”:

Adding coredata to the project - step 2

Adding coredata to the project – step 2

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”:

Add the coredata data model

Add the coredata 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:

Add the model classes via coredata

Add the model classes via coredata

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:

  1. The application has two tables, one displays the posts and the other displays comments on posts.
  2. We have two buttons: One for syncing the coredata database with the blog, and another one to wipe the coredata database.
  3. When the application start, show all posts which are currently available in the coredata database.
  4. 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).
  5. 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:

Add buttons to the view (coredata part)

Add buttons to the view

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:

Connecting the properties with the buttons of the storyboard (coredata tutorial part)

Connecting the properties with the buttons of the storyboard

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.

Objective-c tutorial – Second part – add Rest JSON API

This part of the tutorial explains, how to add a Rest JSON API to an already created application, which shows posts and comments for a blog. In this tutorial we will connect the application with the rest API of an actual WordPress blog, so we can view the posts and comments of the blog rather than the mock data.

It requires you to be done with the first step of the tutorial.

Adding Rest JSON API to a wordpress blog

A wordpress blog has a native XMLRPC API. I didn’t want ot use it, because consuming XMLRPC services doesn’t seem straight forward in objective-c without using additional libraries. That’s why I decided to install a plugin on my blog, which adds a Rest JSON API. You can find the plugin here (and some extended documentation here).

After installation, you should be able to open /wp-json/posts/ at your blog – this will return the existing posts (latest 10 entries). Additionally, you can use /wp-json/posts/131/comments to view the comments of a post. The number 131 represents the ID of a certain post.

Objective-c implementation

Let’s implement the services in the application, so we can view real posts and comments instead of the mock-data. Open the implementation of AppDelegate and do some objective-c coding.

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(getCommentsForPostId:) name:@"getComments" object:nil];

    NSMutableArray *tempPosts = [NSMutableArray array];

    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) {
                NSInteger postId = [[key objectForKey:@"ID"] integerValue];
                NSString *postTitle = [key objectForKey:@"title"];
                Post *tempPost = [[Post alloc] initWithIdAndTitle:postId title:postTitle];
                [tempPosts addObject:tempPost];
            }
        }
        [[NSNotificationCenter defaultCenter] postNotificationName:@"showPosts" object:tempPosts];
    }];

    }

- (void)applicationWillTerminate:(NSNotification *)aNotification {
    // Insert code here to tear down your application
}

- (void)getCommentsForPostId:(NSNotification*)notification {
    NSMutableArray *commentsToShow = [NSMutableArray array];
    NSNumber *postId = [notification object];

    NSString *restUrl = [NSString stringWithFormat:@"http://meberhard.me/wp-json/posts/%ld/comments", [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) {
                NSInteger commentId = [[key objectForKey:@"ID"] integerValue];
                NSMutableString *commentText = [[NSMutableString alloc] init];
                [commentText appendString:[key objectForKey:@"content"]];
                PostComment *pcom = [[PostComment alloc] initPostCommentWithComIdAndPostId:commentId postId:[postId integerValue] text:commentText];
                [commentsToShow addObject:pcom];
            }
        }
        [[NSNotificationCenter defaultCenter] postNotificationName:@"showComments" object:commentsToShow];
    }];
}

@end

All instructions to create mock-data are now removed. Instead, we added a variable “tempPosts”, which will store the posts as we receive them from the Rest JSON API. The following lines request the posts from the Rest JSON API, here it is used with my blog. The JSON response is stored in a NSDictionary. We iterate over that dictionary and create an object of “Post” for every entry, the “Post” object is added after to the “tempPosts” array. After iterating, we send the notification to show the posts, we use the array containing the posts as object for the notification.

The method “getCommentsForPostId” works in a similar way. We just use a different URL and also add the id of the post to the Rest JSON API call. After iterating over the response, we send a notification and use the comments as object.

After running the application, you should be able to see the posts of your blog. If you select a post, the comments should appear in the second table:

Application after implementing the Rest JSON API

Application after implementing the Rest JSON API.

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.

Thid step: Implementing core data so posts and comments may be saved locally. Click here to view the post.

Application after launching it

Objective-c tutorial – First part

The first part of the objective-c tutorial focuses on the basic creation of the application. In XCode, add a new project. Select “Cocoa Application” under “OS X”.

Xcode add new project (objective-c tutorial)

Add a new project in Xcode.

The next step requires you to enter a product name. I choose “WordPressConnect”, but the name really does not matter. Everything else can be left like it is.

Add project in Xcode (objective-c tutorial)

Second step of adding the project in Xcode.

The last step requires you to select a folder, where all the sources will be saved. Click “Create” after.

Creating the View

The basic idea is to have two tables in the view (objective-c coding will follow after). The first table shows the id and the title of the post, the second table shows the id and text of a comment. After creating the project, this view can be created using the storyboard. Select the Storyboard from the project files. A view will open which shows the window.

Xcode Storyboard (objective-c tutorial)

The storyboard in the Xcode project.

Search for “table” in the right bottom. All elements, which can be used for creating views are listed here. Drag two “Table View” elements to the area which says “View Controller”. After adding the tables, you may double click the table header area to write some headings like “Post Id”, “Post title”, “Comment Id” and “Comment Text”. After doing so, we need to add identifiers, so we know which tables is which. Select a “Table View” (very easy using the tree structure on the left side called “View Controller Scene”), select the first “Table View” and add an identifier using the inspector on the right side (third register). I used “TablePosts” as identifier.

Adding table views to the storyboard (objective-c tutorial)

Adding table views to the storyboard.

For the second table, use “TableComments” as identifier. After that, select the columns and also give them identifiers. I used the following identifiers: “ColumnPostId” and “ColumnPostTile” for the two columns in the first table. “ColumnCommentId” and “ColumnCommentText” for the two columns of the second table. The identifiers give us the possibility, to identify correct tables and columns later in the objective-c implementation.

Now we need to connect the two tables with the ViewController, in order to fill them. Open the file “ViewController.h” and add the following objective-c properties between “@interface” and “@end”:

@property (nonatomic, strong) IBOutlet NSTableView *tablePosts;
@property (nonatomic, strong) IBOutlet NSTableView *tableComments;

In Xcode you might see two circles next to these properties. We can use them to make the connection between the view and the variables. There is a possibility to show two files next to each other in the IDE (two circles). Click it, choose for the left side the storyboard and for the right side the file ViewController.h.

Select in the tree view on the left side of the storyboard the first “Table View”, which represents the tables for the posts. After, click within the circle of the variable “tablePosts” and drag the line onto the table of the storyboard you just selected.

Connect the storyboard with ViewController variables (objective-c tutorial)

Connect the storyboard with ViewController variables.

Do the same with the second table – used to display comments – and connect the variable “tableComments”. Run the application using the keys “cmd” and “R”, the following window should open:

Sample window after first run (objective-c tutorial)

Sample window after first run.

 Add the models – some objective-c coding

Since we want to deal with posts and comments, we are going to add very simple models for this purpose. The objective-c code will follow. In your project, add a group called “Model”. Right-click the folder, select “New File” and choose “Cocoa Class”. Call the class “Post” and select “NSObject” as “Subclass of”.

Add a class for the Posts (objective-c tutorial)

Add a class for the Posts.

Add another class the same way and call it “PostComment”, it will be used for the comments. After doing so, the structure of your project should look like this:

Project structure after adding model classes (objective-c tutorial)

Project structure after adding model classes.

The properties and methods for the classes are rather easy. We will just store the postId, the post title, the comment id and comment text. We also need to know, to which post a comment belongs. See the objective-c code here:

Post.h

#import <Foundation/Foundation.h>

@interface Post : NSObject

@property (nonatomic, assign) NSInteger postId;
@property (strong) NSString *title;

- (id)initWithIdAndTitle:(NSInteger)postId title:(NSString*)title;

@end

Post.m

#import "Post.h"

@implementation Post

- (id)initWithIdAndTitle:(NSInteger)postId title:(NSString *)title {
    if (self = [super init]) {
        self.postId = postId;
        self.title = title;
    }
    return self;
}

@end

PostComment.h

#import <Foundation/Foundation.h>

@interface PostComment : NSObject

@property (nonatomic, assign) NSInteger comId;
@property (nonatomic, assign) NSInteger postId;
@property (strong) NSString *text;

- (id)initPostCommentWithComIdAndPostId:(NSInteger)comId postId:(NSInteger)postId text:(NSString*)text;

@end

PostComment.m

#import "PostComment.h"

@implementation PostComment

- (id)initPostCommentWithComIdAndPostId:(NSInteger)comId postId:(NSInteger)postId text:(NSString *)text {
    if (self = [super init]) {
        self.comId = comId;
        self.postId = postId;
        self.text = text;
    }
    return self;
}

@end

The classes contain the properties, which store the required information and method for creating object instances. Let’s add some Posts and Comments at runtime. Open to “AppDelegate.m”. The method “applicationDidFinishLaunching” will be called after the application launched (huh, qu’elle surprise). We can use this method to create some Posts and PostComments as mock-data after the application launched. First, import “Post.h” and “PostComment.h”. After, add some objective-c code to create the required objects:

AppDelegate.h

#import <Cocoa/Cocoa.h>

@interface AppDelegate : NSObject <NSApplicationDelegate>

@property (strong) NSMutableArray *allPosts;
@property (strong) NSMutableArray *allComments;

@end

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

    Post *post1 = [[Post alloc]initWithIdAndTitle:1 title:@"post 1"];
    Post *post2 = [[Post alloc]initWithIdAndTitle:2 title:@"post 2"];

    PostComment *pcom1 = [[PostComment alloc]initPostCommentWithComIdAndPostId:1 postId:1 text:@"post 1 comment 1"];
    PostComment *pcom2 = [[PostComment alloc]initPostCommentWithComIdAndPostId:2 postId:1 text:@"post 1 comment 2"];
    PostComment *pcom3 = [[PostComment alloc]initPostCommentWithComIdAndPostId:3 postId:2 text:@"post 2 comment 2"];

    self.allPosts = [NSMutableArray arrayWithObjects:post1, post2, nil];
    self.AllComments = [NSMutableArray arrayWithObjects:pcom1, pcom2, pcom3, nil];

    NSLog(@"%@", post1.title);
    NSLog(@"%@", pcom1.text);
    NSLog(@"%@", pcom3.text);
    NSLog(@"%ld", (long)pcom2.postId);
    NSLog(@"%ld", (long)pcom3.comId);

    }

- (void)applicationWillTerminate:(NSNotification *)aNotification {
    // Insert code here to tear down your application
}

@end

In the header file – AppDelegate.h – we just added two properties in objective-c. They are used to store all posts and comments. In the implementation file – AppDelegate.m – we add two posts and three comments. After, we output some information of these objects, just to be sure, that everything works fine. If you run the application like it is, you will see the information in the output window of Xcode.

Connecting the data with the view – some more objective-c

The only thing left is to connect the two tables of the view with the data, so we output the posts and comments. The view should work like this: After launching the application, the first table should show all available posts. If the user clicks a row of the table, thus selects a post, the second table should show all comments assigned to this post. In order to work with “NSTableView”, we are required to implemented the protocols NSTableViewDelegate and NSTableViewDataSource (I linked the terms to the official Apple documentation). We just need to tell the interface of the ViewController, that we will implement the methods of these protocols. It looks like this in the header file:

ViewController.h

#import <Cocoa/Cocoa.h>

@interface ViewController : NSViewController<NSTableViewDelegate, NSTableViewDataSource>

@property (nonatomic, strong) IBOutlet NSTableView *tablePosts;
@property (nonatomic, strong) IBOutlet NSTableView *tableComments;

@property (strong) NSMutableArray *displayPosts;
@property (strong) NSMutableArray *displayComments;

@end

We added two properties: “displayPosts” and “displayComments”. We will use them to store the currently displayed posts and comments.
The implementation looks like this (explanation under):

ViewController.m

#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];

    [[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;
        }
        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;
        } 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];
    }
}

@end

Alright – here we have some more things. The first method “viewDidLoad” sets the delegate and datasource to itself. Like this we define, that the methods of the protocols for the tableviews are implemented in this class. After, there are two observers. They listen to the methods “showPosts” and “showComments”, which we will later send from the implementation of AppDelegate. Short explanation: When we send a notification with the name “showPosts”, it will call the method “setPostsToDisplay”. If we send a notification with the name “ShowComments”, it will call the method “setCommentsToDisplay”. This is an easy way to communicate between AppDelegate and ViewController, without the need to call methods on explicit instances.

Ignore the next method “setRepresentedObject”. The two methods after are “setPostsToDisplay” and “setCommentsToDisplay”. Here we receive the posts or comments from the notification object, set “displayPosts” or “displayComments” and reload accordingly the table view.

The method after is called “viewForTableColumn” is the actual implementation of the protocol “NSTableViewDelegate” (click here for the documentation). This method returns a cell view (it will be called for every row). We have several if-statements here. We use them in order to find out, which table and column is currently requested. Therefore we use the identifiers, which we declared earlier using the storyboard.

numberOfRowsInTableView” is an implementation of the protocol called “NSTableViewDataSource” (click here for the documentation). It returns the number of rows we want to create in the table. For the first table this would be the amount of posts and for the second table the amount comments.

The last method “tableViewSelectionDidChange” is another implementation of “NSTableViewDelegate”. It is called whenever the user selects a row in the table. In our case, we can use it if the user selects a certain post in the first table, to display the comments accordingly in the second table. For this reason, we get the ID of the post which the user selected and send a notification which contains this id. In appDelegate we can look for the comments for this postId and give them back to the ViewController.

The last step is to update AppDelegate:

 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

    Post *post1 = [[Post alloc]initWithIdAndTitle:1 title:@"post 1"];
    Post *post2 = [[Post alloc]initWithIdAndTitle:2 title:@"post 2"];

    PostComment *pcom1 = [[PostComment alloc]initPostCommentWithComIdAndPostId:1 postId:1 text:@"post 1 comment 1"];
    PostComment *pcom2 = [[PostComment alloc]initPostCommentWithComIdAndPostId:2 postId:1 text:@"post 1 comment 2"];
    PostComment *pcom3 = [[PostComment alloc]initPostCommentWithComIdAndPostId:3 postId:2 text:@"post 2 comment 2"];

    self.allPosts = [NSMutableArray arrayWithObjects:post1, post2, nil];
    self.allComments = [NSMutableArray arrayWithObjects:pcom1, pcom2, pcom3, nil];

    [[NSNotificationCenter defaultCenter] postNotificationName:@"showPosts" object:self.allPosts];

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(getCommentsForPostId:) name:@"getComments" object:nil];

    }

- (void)applicationWillTerminate:(NSNotification *)aNotification {
    // Insert code here to tear down your application
}

- (void)getCommentsForPostId:(NSNotification*)notification {
    NSMutableArray *commentsToShow = [NSMutableArray array];
    NSNumber *postId = [notification object];
    for (PostComment *pcom in self.allComments) {
        if (pcom.postId == [postId integerValue]) {
            [commentsToShow addObject:pcom];
        }
    }
    [[NSNotificationCenter defaultCenter] postNotificationName:@"showComments" object:commentsToShow];
}

@end

I deleted the NSLog instructions which were there previously. Instead, we have now to instructions for notifications. The first one sends a notification using the name “showPosts” and as object all posts, because we want to display all posts when the application launches. The second one is an observer, which listens for a notification called “getComments”. This notification will be sent from the ViewController, when the user selects a row. In AppDelegate, this will result in the method “getCommentsForPostId”. This method gets all comments which are assigned to the current postId. It will then send a notification called “showComments” and the selected comments as an object. This notification will cause ViewController to display these comments in the second table.

If everything went right you should see the posts and comments after launching the application.

Application after launching it (objective-c tutorial)

Application after launching it

Other parts of the tutorial:

Second step: Connecting the application to a wordpress blog so it used the service. Click here to view the post.

Thid step: Implementing core data so posts and comments may be saved locally. Click here to view the post.