Integrating Facebook and Parse Tutorial: Part 2

In part 2 of our Parse + Facebook tutorial series, you’ll wrap up the image sharing app with image wall and comments features. By Toby Stephens.

Leave a rating/review
Save for later
Share

Welcome to the second part of this two-part Parse tutorial series that focuses on integrating both Facebook and Parse into your apps.

  • In Part 1, you learned how how to set up a project with Facebook and Parse SDKs, authenticate a user with Facebook using Parse’s Facebook toolkit and retrieve Facebook user information using the Facebook Graph API.
  • In Part 2 (you are here!), you’ll build a data store for your images and comments in Parse, display uploaded images on an image wall and add a comment system to your app.

The completed project from the first half is available here. If you’re going to use this starter project rather than using your own project from Part 1, there are two things to note:

  • Make sure to modify the FacebookAppID in the project’s plist file as well as the Parse App ID and Parse Client Key in the AppDelegate to match the keys you generated for your Facebook and Parse apps.
  • Be aware that URL Scheme section for adding your Facebook URL will be in a different location in the Info tab.

Let’s get back to wrapping up this app!

Getting Started

The first requirement for your image wall is a data structure for the images and their respective comments.

You’ll store the data in a singleton class named DataStore. Create a new Objective-C class that is a subclass of NSObject. Name the class DataStore.

This class will operate as a singleton, so therefore you need some code to retrieve a single instance of this class. Replace the code in DataStore.h with the following:

@interface DataStore : NSObject

@property (nonatomic, strong) NSMutableDictionary *fbFriends;
@property (nonatomic, strong) NSMutableArray *wallImages;
@property (nonatomic, strong) NSMutableDictionary *wallImageMap;

+ (DataStore *) instance;
- (void) reset;

@end

Now replace the contents of DataStore.m with the following code:

#import "DataStore.h"

@implementation DataStore

static DataStore *instance = nil;
+ (DataStore *) instance
{
    @synchronized (self) {
        if (instance == nil) {
            instance = [[DataStore alloc] init];
        }
    }
    return instance;
}

- (id) init
{
    self = [super init];
    if (self) {
        _fbFriends = [[NSMutableDictionary alloc] init];
        _wallImages = [[NSMutableArray alloc] init];
        _wallImageMap = [[NSMutableDictionary alloc] init];
    }
    return self;
}

- (void) reset
{
    [_fbFriends removeAllObjects];
    [_wallImages removeAllObjects];
    [_wallImageMap removeAllObjects];
}

@end

The code above serves as your singleton DataStore. Calling [DataStore instance] anywhere in your code will return the shared instance of DataStore so everyone is working with the same data.

There are three collections in the DataStore above:

  1. fbFriends is a dictionary containing the FBGraphUser objects of you and all your friends, keyed on Facebook user ID.
  2. wallImage is populated with the Wall’s image objects that are returned from Parse.
  3. wallImageMap is a dictionary of Wall images, keyed on the object ID returned from Parse. This allows you to look up a specific Wall Image and update the comments if Parse notifies you of a new comment on an image.

Finally, the init and reset methods initialize and empty the collections.

Now it’s time to define your data objects. Add the following two new interface definitions at the top of DataStore.h:

@interface WallImage : NSObject
@property (nonatomic, strong) UIImage *image;
@property (nonatomic, strong) id objectId;
@property (nonatomic, strong) NSDictionary<FBGraphUser> *user;
@property (nonatomic, strong) NSDate *createdDate;
@property (nonatomic, strong) NSMutableArray *comments;
@end

@interface WallImageComment : NSObject
@property (nonatomic, strong) NSString *comment;
@property (nonatomic, strong) NSDictionary<FBGraphUser> *user;
@property (nonatomic, strong) NSDate *createdDate;
@end

These are your objects for the images and comments to display in the Image Wall.

Here’s a quick look at the WallImage properties:

  • image: Contains the uploaded image.
  • objectId: Contains the objectId generated by Parse to uniquely identify each image.
  • user: A dictionary that contains all pertinent Facebook data about the user that uploaded this image; it conforms to Facebook’s FBGraphUser protocol.
  • createdDate: Hold the timestamp of when the image was uploaded.
  • comments: A mutable collection of WallImageComments.

Now would be a perfect time to discuss the properties of WallImageComment:

  • comment: A string containing the submitted comment.
  • user: A dictionary of the user information detailing who submitted the comment; again, it conforms to FBGraphUser.
  • createdDate: Holds the timestamp of when the comment was submitted.

Open DataStore.m and add the implementation for both classes at the top of the file, just below the #import statement:

@implementation WallImage
- (id) init
{
    self = [super init];
    if (self) {
        // Init array of comments
        _comments = [[NSMutableArray alloc] init];
    }
    return self;
}
@end

@implementation WallImageComment

@end

As you can see, both of these classes provide simple ways to store data, and require very little implementation. All you’ve done is add the instantiation and initialization of the comments collection in WallImage.

You want your DataStore class to be accessible from anywhere in the app, so once again, it makes sense for the import to be in the pre-compile header. Open Supporting Files\FBParse-Prefix.pch and add the following import just before the import statement for Comms.h:

#import "DataStore.h"

The DataStore.h import MUST come before the Comms.h import since you’ll use the DataStore class in your Comms class later on.

Before you go any further, you need to ensure you reset the DataStore each time the user logs in, since you don’t want a user to have stale data from a previous user’s login.

Open FBLoginViewController.m and add the following code to loginPressed:, just before you issue the login: call to the Comms class:

// Reset the DataStore so that we are starting from a fresh Login
// as we could have come to this screen from the Logout navigation
[[DataStore instance] reset];

Build and run the project to confirm that you have no compilation issues with your new DataStore singleton. Nothing has changed with the UI — but you’ll get to that real soon!

Retrieving and Storing the Friends List

Before you can retrieve the images for the Image Wall from Parse, you need to store the FBGraphUser details of you and your friends in the DataStore. This is so you can know who wall images should be shared with.

Open Comms.m add the following code to login:, directly after the line that saves the PFUser object with saveInBackground:

// Add the User to the list of friends in the DataStore
[[DataStore instance].fbFriends setObject:me forKey:me.id];

This stores the user’s FBGraphUser object into the DataStore’s fbFriends list. Yes, you’re not really your own friend, but it’s much easier to retrieve all the images from Parse when everyone is grouped into one list.

Now to add your list of friends. Remove the following lines from the end of login:

// Callback - login successful
if ([delegate respondsToSelector:@selector(commsDidLogin:)]) {
	[delegate commsDidLogin:YES];
}

Now, add the following lines immediately after the point where you save the PFUser object:

// 1
FBRequest *friendsRequest = [FBRequest requestForMyFriends];
[friendsRequest startWithCompletionHandler: ^(FBRequestConnection *connection,
		NSDictionary* result,
		NSError *error) {
	// 2
	NSArray *friends = result[@"data"];
	for (NSDictionary<FBGraphUser>* friend in friends) {
		NSLog(@"Found a friend: %@", friend.name);
		// 3
		// Add the friend to the list of friends in the DataStore
		[[DataStore instance].fbFriends setObject:friend forKey:friend.id];
	}
						
	// 4
	// Callback - login successful
	if ([delegate respondsToSelector:@selector(commsDidLogin:)]) {
		[delegate commsDidLogin:YES];
	}
}];

Take a look at the code above in detail, comment by comment:

  1. Build a Facebook Request object to retrieve your friends from Facebook.
  2. Loop through the array of FBGraphUser objects data returned from the Facebook request.
  3. Add each friend’s FBGraphUser object to the friends list in the DataStore.
  4. The success callback to the delegate is now only called once the friends request has been made.

Build and run your project; login as usual and you’ll see a list of your friends in the log output, as shown below:

FBParse[2171:907] Found a friend: Steve Anthony
FBParse[2171:907] Found a friend: Swetha Shekhar
FBParse[2171:907] Found a friend: David Edi
FBParse[2171:907] Found a friend: Julian Meyer

Retrieving Images from Parse

To show the images in the Image Wall you will need to first retrieve the images from Parse and then show them in the Image Wall table view.

Open Comms.h and add the following method declaration to Comms:

+ (void) getWallImagesSince:(NSDate *)lastUpdate forDelegate:(id<CommsDelegate>)delegate;

Next, add the following callback to the CommsDelegate protocol:

- (void) commsDidGetNewWallImages:(NSDate *)updated;

Open Comms.m and add the following method implementation:

+ (void) getWallImagesSince:(NSDate *)lastUpdate forDelegate:(id<CommsDelegate>)delegate
{
	// 1
	// Get the complete list of friend ids
	NSArray *meAndMyFriends = [DataStore instance].fbFriends.allKeys;
	
	// 2
	// Create a PFQuery, Parse Query object
	PFQuery *imageQuery = [PFQuery queryWithClassName:@"WallImage"];
	[imageQuery orderByAscending:@"createdAt"];
	[imageQuery whereKey:@"updatedAt" greaterThan:lastUpdate];
	[imageQuery whereKey:@"userFBId" containedIn:meAndMyFriends];
	[imageQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
		// 3
		__block NSDate *newLastUpdate = lastUpdate;
		
		if (error) {
			NSLog(@"Objects error: %@", error.localizedDescription);
		} else {
			// 4
			// Go through the returned PFObjects
			[objects enumerateObjectsUsingBlock:^(PFObject *wallImageObject, NSUInteger idx, BOOL *stop) {
				// 5
				// Get the Facebook User Id of the user that uploaded the image
				NSDictionary<FBGraphUser> *user = [[DataStore instance].fbFriends objectForKey:wallImageObject[@"userFBId"]];
				
				// 6
				// Construct a WallImage object
				WallImage *wallImage = [[WallImage alloc] init];
				wallImage.objectId = wallImageObject.objectId;
				wallImage.user = user;
				wallImage.createdDate = wallImageObject.updatedAt;

				wallImage.image = [UIImage imageWithData:[(PFFile *)wallImageObject[@"image"] getData]];

				// 7
				// Update the last update timestamp with the most recent update
				if ([wallImageObject.updatedAt compare:newLastUpdate] == NSOrderedDescending) {
					newLastUpdate = wallImageObject.updatedAt;
				}
				
				// 8
				// Store the WallImage object in the DataStore collections
				[[DataStore instance].wallImages insertObject:wallImage atIndex:0];
				[[DataStore instance].wallImageMap setObject:wallImage forKey:wallImage.objectId];
			}];
		}
		
		// Callback
		if ([delegate respondsToSelector:@selector(commsDidGetNewWallImages:)]) {
			[delegate commsDidGetNewWallImages:newLastUpdate];
		}
	}];
}

There’s a lot going on here, so take a moment and walk through the commented sections of the code:

  1. This is your collection of friends’ Facebook user IDs. This is a key part of your query to Parse.
  2. Here you build your Parse query, specifying the following arguments:
    • the object you wish to retrieve is WallImage.
    • you wish to have them sorted by created date.
    • you only want those images that have been created since the last update.
    • you only want images for you and your friends.
  3. In the callback to the delegate you provide a new last update timestamp. Since all Parse operations are handled in blocks, you need to use the __block storage type modifier here so that you can update the variable within the block and use it outside of the block.
  4. Loop through the returned WallImage objects.
  5. The WallImage object contains the Facebook user ID of the user that uploaded the image. This statement looks up the user from the list of friends to resolve the full FBGraphUser object.
  6. Create the WallImage object defined in the DataStore. This contains all the relevant information for the image.
  7. If the created date of this image is greater than the current last update date, then set a new last update date so that you always have the most recent timestamp.
  8. Store the new WallImage object in the collections in the DataStore.

To load the images into the ImageWallViewController, you need to call this method from somewhere.

Open ImageWallViewController.m and replace the empty ImageWallViewController class extension with this:

@interface ImageWallViewController () <CommsDelegate> {
	NSDate *_lastImageUpdate;
}
@end

This adds the CommsDelegate protocol to the class and adds a timestamp variable to store the last update time for images. _lastImageUpdate is used to ensure you only request images from Parse that have been created since the last update.

Add the following lines to viewDidLoad in ImageWallViewController:

// Initialize the last updated dates
_lastImageUpdate = [NSDate distantPast];

// Get the Wall Images from Parse
[Comms getWallImagesSince:_lastImageUpdate forDelegate:self];

Here you set the initial last image update timestamp to a long time ago, in a galaxy far, far away … and make the call to the Comms class to get your images.

Build and run the app. Login as usual, but pay close attention to the console output. You’ll see the following warning in the log:

Warning: A long-running Parse operation is being executed on the main thread.

What do you suppose is causing that?

Going to the Background

Here’s the solution to the question above:

[spoiler]The call to [UIImage imageWithData:] in getWallImagesSince: is loading the image from Parse on the main UI thread. This should be done on a separate thread so as not to block the UI thread.[/spoiler]

In order to load the files from Parse asynchronously, you need to download them using a shared NSOperationQueue. You’ll create an NSOperationQueue category that will give you a background queue for all your PFFile downloads from Parse.

Create a New File in Xcode, choosing the Objective-C category as the template. Enter SharedQueue as the Category and NSOperationQueue as the Category on.

ts_FBParse_nsoq

This creates .h and .m files for a new Category class called NSOperationQueue+SharedQueue. Open NSOperationQueue+SharedQueue.h and add the following static method declaration:

+ (NSOperationQueue *) pffileOperationQueue;

Now open NSOperationQueue.m and add the following method:

+ (NSOperationQueue *) pffileOperationQueue {
	static NSOperationQueue *pffileQueue = nil;
	if (pffileQueue == nil) {
		pffileQueue = [[NSOperationQueue alloc] init];
		[pffileQueue setName:@"com.rwtutorial.pffilequeue"];
	}
	return pffileQueue;
}

Now, when you call [NSOperationQueue pffileOperationQueue] you will receive a shared NSOperationQueue which you can use for all of your Parse downloads.

Note: When you initialize a new NSOperationQueue, you are basically creating a new background thread. You can call [NSOperationQueue mainQueue] to run code on the main thread as well.

Now to use the NSOperationQueue to download an image from Parse and onto your image wall.

Open Comms.m and add the following import to the top of the file:

#import "NSOperationQueue+SharedQueue.h"

Now find the following line in getWallImagesSince:forDelegate::

wallImage.image = [UIImage imageWithData:[(PFFile *)wallImageObject[@"image"] getData]];

…and replace it with this:

[[NSOperationQueue pffileOperationQueue] addOperationWithBlock:^ {
	wallImage.image = [UIImage imageWithData:[(PFFile *)wallImageObject[@"image"] getData]];
}];

You are now passing the image download to your shared queue, so that you’re not doing the heavy lifting on the main UI thread.

Build and run your app, login as usual, and the warning should not appear in the log!

Displaying Images

Now that you can retrieve images, you need to display them in the table view. The ImageWallViewController needs to respond to the CommsDelegate callback so that it knows when to update the table view. Add the following method to the ImageWallViewController implementation in ImageWallViewController.m:

- (void) commsDidGetNewWallImages:(NSDate *)updated {
	// Update the update timestamp
	_lastImageUpdate = updated;
	
	// Refresh the table data to show the new images
	[self.tableView reloadData];
}

Here, you update the last image timestamp, ready the app for the next refresh and then reload the table view data.

Of course, the table view isn’t going to show anything unless you tell it to, so you need to update the UITableView methods.

Replace numberOfSectionsInTableView: in ImageWallViewController.m with the following:

- (NSInteger) numberOfSectionsInTableView:(UITableView *)tableView
{
	// One section per WallImage
	return [DataStore instance].wallImages.count;
}

Each image on the wall is a separate section of the table, with a header view showing the image. Therefore the number of sections in the table will be the same as the number of images in the DataStore.

Add the following code to tableView:viewForHeaderInSection: in ImageWallViewController.m, just before you return the cell:

WallImage *wallImage = ([DataStore instance].wallImages[section]);
[imageCell.image setImage:wallImage.image];
[imageCell.lblUploadedBy setText:wallImage.user.name];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"MMM d, h:mm a"];
[imageCell.lblUploadedDate setText:[dateFormatter stringFromDate:wallImage.createdDate]];

In the above code, you simply populate the header cell with all of the separate pieces of WallImage data you retrieved from Parse.

Build and run your app, login, and take a look at the section header for the retrieved image:

ts_FBParse_imagewall1

Depending upon your network speed, the image may or may or may not be displayed. Can you tell why this might be?

[spoiler]
The UITableView is being rendered before the image has been downloaded by the shared NSOperationQueue. As the download of the image is asynchronous it might actually finish after the image wall is rendered, and therefore never show the image.
[/spoiler]

Asynchronous Downloads and Notifications

Since the image is downloaded asynchronously from Parse using your shared NSOperationQueue, you cannot guarantee that the image will be downloaded before you render the table view. To get around this, you need to inform the table view when the image has been downloaded using NSNotificationCenter.

At the top of Comms.h, declare the following constant:

extern NSString * const N_ImageDownloaded;

Then, at the top of Comms.m, define the constant:

NSString * const N_ImageDownloaded = @"N_ImageDownloaded";

Still in Comms.m, go to the section in getWallImagesSince:forDelegate: where you use pffileOperationQueue to download the image from Parse. Add the following code to the end of this block operation:

// Notify - Image Downloaded from Parse
[[NSNotificationCenter defaultCenter] postNotificationName:N_ImageDownloaded object:nil];

This posts a notification to the notification center, informing all interested classes the image download is complete.

Open ImageWallViewController.m and add the following to the bottom of viewDidLoad:

// Listen for image downloads so that we can refresh the image wall
[[NSNotificationCenter defaultCenter] addObserver:self
	selector:@selector(imageDownloaded:)
	name:N_ImageDownloaded
	object:nil];

This lets NSNotificationCenter know that the ImageWallViewController class is interested in knowing when an image is downloaded from Parse.

Now add the following method to ImageWallViewController.m:

- (void) imageDownloaded:(NSNotification *)notification {
	[self.tableView reloadData];
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

When an image has been downloaded from Parse, NSNotificationCenter issues the notification and invokes imageDownloaded: on ImageWallViewController which reloads the table view data.

As well, you remove ImageWallViewController from receiving notifications when it is deallocated so that NSNotificationCenter doesn’t try to send any messages to a destroyed object.

Build and run your project; login and open the Image Wall. This time, the table updates once the image has been downloaded and your image is now displayed:

ts_FBParse_imagewall2

Retrieving User Profile Pictures

Now that you have a way of downloading images asynchronously, simply repeat the process to retrieve the user’s Facebook profile picture.

Open up NSOperationQueue+SharedQueue.h and add the following static method definition:

+ (NSOperationQueue *) profilePictureOperationQueue;

Add the implementation to NSOperationQueue+SharedQueue.m:

+ (NSOperationQueue *) profilePictureOperationQueue {
	static NSOperationQueue *profilePictureQueue = nil;
	if (profilePictureQueue == nil) {
		profilePictureQueue = [[NSOperationQueue alloc] init];
		[profilePictureQueue setName:@"com.rwtutorial.profilepicturequeue"];
	}
	return profilePictureQueue;
}

Now, calling [NSOperationQueue profilePictureOperationQueue] will provide you with a shared NSOperationQueue for downloading Facebook profile pictures.

Open Comms.h and add the following code to the top of the file:

extern NSString * const N_ProfilePictureLoaded;

Open Comms.m and define the string at the top of the file:

NSString * const N_ProfilePictureLoaded = @"N_ProfilePictureLoaded";

The above code sets the notification name for downloaded profile pictures.

Still in Comms.m, add the following code to login: where you save the PFUser object and just before you add the user to the friends list in the DataStore:

// Launch another thread to handle the download of the user's Facebook profile picture
[[NSOperationQueue profilePictureOperationQueue] addOperationWithBlock:^ {
	// Build a profile picture URL from the user's Facebook user id
	NSString *profilePictureURL = [NSString stringWithFormat:@"https://graph.facebook.com/%@/picture", me.id];
	NSData *profilePictureData = [NSData dataWithContentsOfURL:[NSURL URLWithString:profilePictureURL]];
	UIImage *profilePicture = [UIImage imageWithData:profilePictureData];

	// Set the profile picture into the user object
	if (profilePicture) [me setObject:profilePicture forKey:@"fbProfilePicture"];
						
	// Notify that the profile picture has been downloaded, using NSNotificationCenter
	[[NSNotificationCenter defaultCenter] postNotificationName:N_ProfilePictureLoaded object:nil];
}];

The above code constructs a URL for the location of the profile picture and then downloads the file. It then sets the downloaded image as an object in the FBGraphUser object with a key @”fbProfilePicture”. Once the download is complete, a notification is sent to the notification center.

That takes care of your own profile picture; now it’s time to do the same for your friends.

Add the following code to login: in Comms.m where you loop through your friend list and add the following code before you add the friend to the friends list in the DataStore.

// Launch another thread to handle the download of the friend's Facebook profile picture
[[NSOperationQueue profilePictureOperationQueue] addOperationWithBlock:^ {
	// Build a profile picture URL from the friend's Facebook user id
	NSString *profilePictureURL = [NSString stringWithFormat:@"https://graph.facebook.com/%@/picture", friend.id];
	NSData *profilePictureData = [NSData dataWithContentsOfURL:[NSURL URLWithString:profilePictureURL]];
	UIImage *profilePicture = [UIImage imageWithData:profilePictureData];

	// Set the profile picture into the user object
	if (profilePicture) [friend setObject:profilePicture forKey:@"fbProfilePicture"];
								
	// Notify that the profile picture has been downloaded, using NSNotificationCenter
	[[NSNotificationCenter defaultCenter] postNotificationName:N_ProfilePictureLoaded object:nil];
}];

The above code looks quite similar; it does exactly the same thing as as you did for your own profile picture, but does it now for each of your Facebook friends.

Now that you have asynchronous loading of profile images, you need to update the Image Wall table view whenever a profile picture has downloaded.

Open ImageWallViewController.m and add the following code to the end of viewDidLoad:

// Listen for profile picture downloads so that we can refresh the image wall
[[NSNotificationCenter defaultCenter] addObserver:self
	selector:@selector(imageDownloaded:)
	name:N_ProfilePictureLoaded
	object:nil];

The selector imageDownloaded: is the same as the other notification for images downloaded from Parse, since the behavior is the same — it reloads the table data. There’s no sense in duplicating code!

The final step is to add the profile pictures to the section header cell in the table view for the Image Wall.

Add the following code to the bottom of tableView:viewForHeaderInSection: in ImageWallViewController.m just before you return the cell:

// Add the user's profile picture to the header cell
[imageCell.profilePicture setImage:wallImage.user[@"fbProfilePicture"]];

Build and run your project; this time you will see your lovely profile picture in the top right corner of the section header, as shown below:

ts_FBParse_imagewall3

Adding Comments to Images

Your app will be a lot more engaging once you allow the users to comment on each other’s images.

Add the following method declaration to Comms in Comms.h:

+ (void) getWallImageCommentsSince:(NSDate *)lastUpdate forDelegate:(id<CommsDelegate>)delegate;

Next, add the following callback method to the CommsDelegate protocol in Comms.h:

- (void) commsDidGetNewWallImageComments:(NSDate *)updated;

Now open Comms.m and add the following method:

+ (void) getWallImageCommentsSince:(NSDate *)lastUpdate forDelegate:(id<CommsDelegate>)delegate
{
	// Get all the Wall Image object Ids
	NSArray *wallImageObjectIds = [DataStore instance].wallImageMap.allKeys;
	
	// Execute the PFQuery to get the Wall Image Comments for all the Wall Images
	PFQuery *commentQuery = [PFQuery queryWithClassName:@"WallImageComment"];
	[commentQuery orderByAscending:@"createdAt"];
	[commentQuery whereKey:@"updatedAt" greaterThan:lastUpdate];
	[commentQuery whereKey:@"imageObjectId" containedIn:wallImageObjectIds];
	[commentQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
		// In the callback, we will return the latest update timestamp with this request.
		// Default to the current last update.
		__block NSDate *newLastUpdate = lastUpdate;
		
		if (error) {
			NSLog(@"Objects error: %@", error.localizedDescription);
		} else {
			[objects enumerateObjectsUsingBlock:^(PFObject *wallImageCommentObject, NSUInteger idx, BOOL *stop) {
				// Look up the User's Facebook Graph User
				NSDictionary<FBGraphUser> *user = [[DataStore instance].fbFriends objectForKey:wallImageCommentObject[@"userFBId"]];
				
				// 1
				// Look up the Wall Image
				WallImage *wallImage = [[DataStore instance].wallImageMap objectForKey:wallImageCommentObject[@"imageObjectId"]];
				
				// Add the Comment to the Wall Image
				if (wallImage) {
					WallImageComment *wallImageComment = [[WallImageComment alloc] init];
					wallImageComment.user = user;
					wallImageComment.createdDate = wallImageCommentObject.updatedAt;
					wallImageComment.comment = wallImageCommentObject[@"comment"];
					if ([wallImageCommentObject.updatedAt compare:newLastUpdate] == NSOrderedDescending) {
						newLastUpdate = wallImageCommentObject.updatedAt;
					}

					//2
					[wallImage.comments addObject:wallImageComment];
				}
			}];
		}
		
		// Callback
		if ([delegate respondsToSelector:@selector(commsDidGetNewWallImageComments:)]) {
			[delegate commsDidGetNewWallImageComments:newLastUpdate];
		}
	}];	
}

This should all look very familiar; it’s almost exactly the same as the getWallImagesSince: method, the only difference is that here you’re setting the comments.

The comments returned from Parse contain a reference to the WallImage objectid. You then use the objectid to look up the WallImage object that is associated with this comment. Once you’ve created the WallImageComment object, you then add it to the comments array of the WallImage

Again, all that’s left is to call this method from ImageWallViewController. Open ImageWallViewController.m and add a new NSDate timestamp to the ImageWallViewController class extension:

NSDate *_lastCommentUpdate;

This stores the timestamp of the latest comment to ensure that you don’t get any historic comments in your request to Parse.

Add the following line to viewDidLoad in ImageWallViewController.m to initialize the timestamp:

_lastCommentUpdate = [NSDate distantPast];

Still in ImageWallViewController.m, add the following line to commsDidGetNewWallImages:, just before you reload the table data:

// Get the latest WallImageComments from Parse
[Comms getWallImageCommentsSince:_lastCommentUpdate forDelegate:self];

Now, once the WallImages have been loaded you’ll fire off a request to get the latest comments.

Add the following method to ImageWallViewController.m:

- (void) commsDidGetNewWallImageComments:(NSDate *)updated {
	// Update the update timestamp
	_lastCommentUpdate = updated;

	// Refresh the image wall table
	[self.tableView reloadData];
}

Here, you update the _lastCommentUpdate timestamp with the new value returned from Parse, then reload the table view to show the comments.

Replace the contents of tableView:numberOfRowsInSection: in ImageWallViewController.m with the following:

// One row per WallImage comment
WallImage *wallImage = ([DataStore instance].wallImages[section]);
return wallImage.comments.count;

In your image wall, each of the sections of your table represents a single image, while each of the rows in that section represents a comment. In the above code, you return the number of rows for each section which is the number of comments on that particular image.

Add the following code to tableView:cellForRowAtIndexPath: in ImageWallViewController.m, at the point just before you return the cell:

// Get the WallImage from the indexPath.section
WallImage *wallImage = ([DataStore instance].wallImages[indexPath.section]);

// Get the associated WallImageComment from the indexPath.row
WallImageComment *wallImageComment = wallImage.comments[indexPath.row];
[commentCell.profilePicture setImage:wallImageComment.user[@"fbProfilePicture"]];
[commentCell.comment setText:wallImageComment.comment];

In the above code, you look up the particular WallImage from the indexPath.section to populate the comments cell. Next, you look up the WallImageComment from the indexPath.row, set the profile picture of the commenting user, and finally set the comment text.

Build and run your project; open up the Image Wall and you’ll see your comment below your image, as shown below:

ts_FBParse_imagewall4

Saving Comments to Parse

Now it’s time to provide your friends with a way to comment on your images.

First, you need to create the Comms method to submit a new comment.

Open Comms.h and the following method declaration:

+ (void) addComment:(NSString *)comment toWallImage:(WallImage *)wallImage;

Since you need to save the new comment to Parse asynchronously and be informed when the the save is complete, NSNotificationCenter will serve the purpose nicely.

Add the following static string to the top of Comms.h:

extern NSString * const N_CommentUploaded;

Now add the following definition to Comms.m:

NSString * const N_CommentUploaded = @"N_CommentUploaded";

Still in Comms.m add the method below:

+ (void) addComment:(NSString *)comment toWallImage:(WallImage *)wallImage
{
	// Save the new Comment to the Wall Image
	PFObject *wallImageCommentObject = [PFObject objectWithClassName:@"WallImageComment"];
	wallImageCommentObject[@"comment"] = comment;
	wallImageCommentObject[@"userFBId"] = [[PFUser currentUser] objectForKey:@"fbId"];
	wallImageCommentObject[@"user"] = [PFUser currentUser].username;

	// Set the object id for the associated WallImage
	wallImageCommentObject[@"imageObjectId"] = wallImage.objectId;
	
	// Save the comment to Parse
	[wallImageCommentObject saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
		// Notify that the Comment has been uploaded, using NSNotificationCenter
		[[NSNotificationCenter defaultCenter] postNotificationName:N_CommentUploaded object:nil];
	}];
}

The above method accepts a string argument — the text of the comment — as well as a WallImage object so that you can link the comment to the WallImage object. Once the saveInBackgroundWithBlock: block has executed, you then post a notification noting that the comment upload is complete.

To make things easy for the user, you’ll provide an inline text entry for the user, much like you’d see in Facebook. This will occupy another cell in the table, and will always be the last row of each section. Therefore, you need to add an extra row to the bottom of each section to hold the comment entry cell.

Open ImageWallViewController.m and replace the following line in tableView:numberOfRowsInSection:

return wallImage.comments.count;

… with the code below::

return wallImage.comments.count + 1; // Add a row for the New Comment cell

This will return an extra line at the bottom of each section of the table for your new comment cell.

Next, add the following property to the ImageWallTableNewCommentCell class extension at the top of ImageWallViewController.m:

@property (nonatomic, strong) WallImage *wallImage;

The above property tells ImageWallTableNewCommentCell which WallImage is being commented on, so you have it for reference later.

Now, on to render the new comment cell. Add the following code to tableView:cellForRowAtIndexPath: in ImageWallViewController.m, directly after you get the WallImage from the DataStore:

static NSString *NewCommentCellIdentifier = @"NewCommentCell";
if (indexPath.row >= wallImage.comments.count) {
	// If this is the last row in the section, create a NewCommentCell
	ImageWallTableNewCommentCell *newCommentCell = (ImageWallTableNewCommentCell *)[tableView dequeueReusableCellWithIdentifier:NewCommentCellIdentifier];

	// Set the WallImage on the cell so that new comments can be associated with the correct WallImage
	newCommentCell.wallImage = wallImage;
		
	return newCommentCell;
}

In the code above, check if this is the last row of the section; if so, set the WallImage for this cell and then return the new ImageWallTableNewCommentCell.

Build and run your project; below your comment you’ll see the new text field for entering comments:

ts_FBParse_imagewall5

Using the New Comment Field

You’re now going to send a comment about your own image. A little insular perhaps, but this app isn’t public yet, so it’ll have to do for now. :]

Open ImageWallViewController.m and locate the ImageWallTableNewCommentCell implementation. You will notice there is already a textFieldShouldReturn: method set up with no content. This is called when the user hits return on the keyboard – you will implement this method to submit the new comment.

Add the following code to textFieldShouldReturn: in ImageWallViewController.m, just before the return statement at the end:

if (_txtComment.text.length == 0) return YES;

// We have a new comment, so send it off
[_txtComment resignFirstResponder];
[Comms addComment:_txtComment.text toWallImage:_wallImage];
[_txtComment setText:@""];

The above code closes the keyboard by resigning first responder, and then makes the call to your addComment:toWallImage: method in the Comms class so that the comment is stored on the server.

Now, add the following lines to the bottom of viewDidLoad in ImageWallViewController.m:

// Listen for uploaded comments so that we can refresh the image wall table
[[NSNotificationCenter defaultCenter] addObserver:self
					 selector:@selector(commentUploaded:)
					     name:N_CommentUploaded
					   object:nil];

Here you listen for notifications of the submitted comment being loaded into Parse.

Add the following method to ImageWallViewController.m:

- (void) commentUploaded:(NSNotification *)notification
{
	[Comms getWallImagesSince:_lastImageUpdate forDelegate:self];
}

In the above method, once you receive a notification that the comment has been uploaded, you request the WallImages from Parse again so you can download the latest data, which includes your new comment.

Build and run your project; in the text field below your initial comment, make a new comment and press Return. After a short wait, you should see your comment appear below the image and the text field will be empty, ready for your next comment, as demonstrated below:

ts_FBParse_imagewall6

Uploaded Image Notifications

When you upload a new image using the app, there’s currently no way to notify the app that it needs to request and download the new app. You’re going to sort that out now with a new NSNotification.

Open Comms.h and add a new static string at the top:

extern NSString * const N_ImageUploaded;

Then, open Comms.m and define the static string at the top of the file:

NSString * const N_ImageUploaded = @"N_ImageUploaded";

This will be the Notification name to use when the new image has been uploaded successfully.

Open UploadImageViewController.m and find commUploadImageComplete:. Add the following code just after you pop the view controller on a successful upload:

// Notify that a new image has been uploaded
[[NSNotificationCenter defaultCenter] postNotificationName:N_ImageUploaded object:nil];

Now open ImageWallViewController.m and add the following code to the bottom of viewDidLoad:

// Listen for new image uploads so that we can refresh the image wall table
[[NSNotificationCenter defaultCenter] addObserver:self
					 selector:@selector(imageUploaded:)
					     name:N_ImageUploaded
					   object:nil];

Here you tell NSNotificationCenter that you want to listen for notifications of new image uploads; when you receive a new notification of an image upload, you call imageUploaded.

Still working in ImageWallViewController.m, add the following method:

- (void) imageUploaded:(NSNotification *)notification
{
	[Comms getWallImagesSince:_lastImageUpdate forDelegate:self];
}

When a new image is uploaded, this queries Parse for any new images and gets the new image and accompanying comment.

Build and run your project; upload a new image along with a comment and you’ll see your new image appear in the Image Wall once it has successfully been uploaded to Parse.

Polishing Your App

Your app works, and looks great. However, as with any app, there’s always a couple of ways to make it shine.

The initialization of NSDateFormatter is a rather expensive operation. It’s not a terribly good idea to initialize it in a table view cell; instead you will create a single instance of NSDateFormatter to use in the ImageWallViewController.

Open ImageWallViewController.m and declare a new variable in the class extension:

NSDateFormatter *_dateFormatter;

Next, add the following code to viewDidLoad in ImageWallViewController.m, just below the call to [super viewDidLoad]:

// Create a re-usable NSDateFormatter
_dateFormatter = [[NSDateFormatter alloc] init];
[_dateFormatter setDateFormat:@"MMM d, h:mm a"];

Still in the same file, update tableView:viewForHeaderInSection: by replacing the following lines:

NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"MMM d, h:mm a"];
[imageCell.lblUploadedDate setText:[dateFormatter stringFromDate:wallImage.createdDate]];

with the code below:

[imageCell.lblUploadedDate setText:[_dateFormatter stringFromDate:wallImage.createdDate]];

At the moment, the Image Wall refreshes only when you launch the app or upload images or comments yourself. A pull-to-refresh would allow the user to request an update of the Image Wall whenever they wish.

Note:UIRefreshControl is only available on iOS6 or better, but there are a huge number of open source libraries for adding pull-to-refresh functionality to older OS versions. One favourite is CKRefreshControl; give it a whirl if you need to support iOS 5 in your app.

Open ImageWallViewController.m and add the following code to viewDidLoad just before you call getWallImagesSince:delegate::

// If we are using iOS 6+, put a pull to refresh control in the table
if (NSClassFromString(@"UIRefreshControl") != Nil) {
	UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
	refreshControl.attributedTitle = [[NSAttributedString alloc] initWithString:@"Pull to Refresh"];
	[refreshControl addTarget:self action:@selector(refreshImageWall:) forControlEvents:UIControlEventValueChanged];
	self.refreshControl = refreshControl;
}

This checks to see if UIRefreshControl is supported in the OS, and if so, instantiates the UIRefreshControl and sets action selector to refreshImageWall:.

Still in ImageWallViewController.m, add the following refresh method:

- (void) refreshImageWall:(UIRefreshControl *)refreshControl
{
	if (refreshControl) {
		[refreshControl setAttributedTitle:[[NSAttributedString alloc] initWithString:@"Refreshing data..."]];
		[refreshControl setEnabled:NO];
	}

	// Get any new Wall Images since the last update
	[Comms getWallImagesSince:_lastImageUpdate forDelegate:self];
}

The above method checks if the caller passed in a UIRefreshControl. This check is necessary since you’ll use refreshImageWall to refresh the table view in other situations that don’t use the refresh control, such as after uploading an image or comment.Then you call getWallImagesSince:forDelegate as you have before.

Now you can tidy up all the calls to getWallImagesSince:forDelegate in the rest of your code.

Still in ImageWallViewController.m, find the call to getWallImagesSince:forDelegate in viewDidLoad and replace it with:

[self refreshImageWall:nil];

This will have the same effect, but use your new method instead to refresh the table.

Now, find imageUploaded: in the same file, and replace the contents of the method with:

[self refreshImageWall:nil];

Finally, find commentUploaded: replace the contents of the method with:

[self refreshImageWall:nil];

There — that looks a lot cleaner. All that’s left is to clean up the UIRefreshControl after the refresh is complete.

Find commsDidGetNewWallImageComments: in ImageWallViewController.m and add the following code to the end of the method, just after you have reloaded the table data:

// Update the refresh control if we have one
if (self.refreshControl) {
	NSString *lastUpdated = [NSString stringWithFormat:@"Last updated on %@", [_dateFormatter stringFromDate:[NSDate date]]];
	[self.refreshControl setAttributedTitle:[[NSAttributedString alloc] initWithString:lastUpdated]];
	[self.refreshControl endRefreshing];
}

Build and run your project. If you’re on iOS6 or better, you can now pull down the Image Wall to refresh it.

ts_FBParse_imagewall7

Where To Go From Here?

Astute readers will have noticed that the app has a Facebook Share button sitting on the section headers in the Image Wall. This button is all wired up in the Storyboard for you to share your image on Facebook. To learn a bit more about the Facebook APIs, try to add the code that takes care of publishing your images to your Facebook feed.

Another neat offering from Parse is their new app-driven push notification. If you’re familiar with setting up push notifications, then it should be a simple manner to update your app to listen for new comments on your Image Wall and respond to the push notification appropriately.

If this is something you’d like to try, take a look at Apple Push Notification Services in iOS 6. Then, read through Parse Push Notifications for more detail on how to hook up push notification Parse.

You can download the completed project for this Parse tutorial series here, including an implementation of the Facebook share noted above.

I hope you’ve enjoyed learning about Facebook and Parse; feel free to post your comments or questions about integrating Facebook and/or Parse, along with any suggestions for related tutorials in the future!

Toby Stephens

Contributors

Toby Stephens

Author

Over 300 content creators. Join our team.