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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Integrating Facebook and Parse Tutorial: Part 2
45 mins
- Getting Started
- Retrieving and Storing the Friends List
- Retrieving Images from Parse
- Going to the Background
- Displaying Images
- Asynchronous Downloads and Notifications
- Retrieving User Profile Pictures
- Adding Comments to Images
- Saving Comments to Parse
- Using the New Comment Field
- Uploaded Image Notifications
- Polishing Your App
- Where To Go From Here?
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:
- This is your collection of friends’ Facebook user IDs. This is a key part of your query to Parse.
- 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.
- 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. - Loop through the returned WallImage objects.
- 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.
- Create the WallImage object defined in the DataStore. This contains all the relevant information for the image.
- 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.
- 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.
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.
[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:
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]