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
You are currently viewing page 2 of 5 of this article. Click here to view the first page.

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:

  • 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.
  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:
  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]

Toby Stephens

Contributors

Toby Stephens

Author

Over 300 content creators. Join our team.