Sign up for a Parse account to implement this tutorial and more!

Sign Up

Anypic_icon
Anypic

Anypic is a full fledged photo sharing app that explores the challenges behind real world applications with complex relational models.

Anypic
iOS
PFUser
PFObject
PFFile
photos
PFQueryTableViewController
Facebook
social network

Download code for this tutorial:

.zip File GitHub

Anypic is full featured photo sharing app built on Parse. Much like the popular app Instagram, you can share, comment and like pictures with your network of Facebook friends. Anypic is not only available on the app store, but also fully open source and ready for you to tinker with and explore.

In this tutorial we'll take a look at some of the more challenging problems developers face when building complex social networking applications. We'll focus on how Parse is used to interact with the server-side data model and the intricacies associated with having robust networking and caching capabilities. Wether you are a new developer looking to build your first real app, or a mobile veteran, this tutorial will help you understand how Parse can be used in production quality apps. You can read this tutorial in any order and jump around to the sections that interest you the most. However, it is a good idea to read the first two sections first to gain a solid understanding of the architecture and the data model.

1. Anypic Overview Get familiar with the myriad of classes in Anypic and the recurring patterns you will find throughout the app.

2. Data Model Here we'll take a look at the small but powerful data classes used in Anypic and the rationale behind the design decisions made.

3. Posting a New Photo Photos are at the heart of Anypic and sections 4 through 7 will take an in depth tour of this feature. We'll start off in this section by taking a look at creating and saving new photos.

4. Photo Timeline Armed with the PFQueryTableViewController and the PFImageView classes, this section will demonstrate how easy it is to display remote images.

5. Commenting on a Photo The ability to share your opinion and interact with your friends is the crux of social networks. Here, we'll take a look at commenting.

6. Liking a Photo In this section, we'll delve into the liking mechanism used in Anypic and explore the associated challenges.

7. Activity Feed A key component of social networks is keeping up to date with what's going on. Anypic's activity feed is surprisingly simple to implement due to the data model decisions made.

8. Following Friends Discover how Anypic leverages a user's Facebook profile to create a follow relationship.

9. Push Notification In this last section of the tutorial, we'll take a look at inner workings of the push notification flow.

1. Overview of Anypic

Anypic uses Facebook as its login mechanism to quickly get users up and running with the app. During the sign up onboarding process, we'll store some of the user's Facebook information in Parse and automatically follow all of their friends who are already using the app. The user can then explore and interact with photos on the timeline view and look at what's been happening in the activity feed. Adding new photos is as simple as using the middle tab bar button and details about a given photo can be found by tapping on it. Names and profile pictures throughout the app also respond to touch and will display to the corresponding user's account timeline.

Anypic's classes are organized into a "Controller" group and a "View" group. Each class in the "Controller" group is a subclass of UIViewController and manages a single screenful of content. The only exception is the PAPPhotoTimelineViewController which is never displayed on its own, but rather inherited by the main timeline and the user account view controllers. As you might expect, the "View" group contains UIView subclasses used throughout the app. These are cells, footers or headers of UITableViews or individual components reused by view controllers.

There are three support classes used by the several of the app's components: PAPConstants, PAPUtility and PAPCache. The first two simply contain constants and static methods used throughout the app. The third is a little more complex. It provides Anypic with custom caching behavior used to quickly display up to date information about photos. When the current user likes or comments on a given photo, we manually update the local cache as soon as the request is sent. This allows us to propagate the model change throughout the app without needing to wait for the initial save request to finish and then refreshing the content.

A commonly used pattern in Anypic is delegation. Several views will communicate interaction related information, such as button touch events, through a delegate method. The view will define a protocol in its header file and the controller will implement this protocol. When the button is tapped the view simply calls the appropriate protocol method.

2. Data Model

Anypic's data model contains only three classes: User, Photo and Activity. While it may appear fairly simplistic, as we'll see in this section, there are several caveats that need to be taken into account when designing a relationship-heavy model. We'll start by taking a look at the Photo and User classes and then discuss some alternatives to creating the required relationships in the Activity section.

Photo

image : File

thumbnail : File

user : User

User

displayName : String

email : String

profilePictureMedium : File

profilePictureSmall : File

facebookId : String

facebookFriends : Array

channel : String

Activity

fromUser : User

toUser : User

type : String

content : String

photo : Pointer















2.1. Photo Class

The Photo class is the simplest model object. It has an image field of type File where the photo is stored and a user field of type _User for the photographer. We've also added a second File field to store a thumbnail of the photo. This is to improve performance in the activity feed table view where several thumbnail images are displayed simultaneously. As we'll see in Section 3, the resizing is done on the client side.

The ACLs set on this model class have an interesting side-effect. As you would expect, we give the photographer read and write permissions and everybody else read permission. This allows us to keep the photo secure while allowing friends to view it. However, this means that we cannot store information such as "likes" directly on the photo since other users do not have write access. Section 2.3 and Section 6 will discuss how to properly handle this issue, but keep this in mind when designing your own models.

2.2. User Class

The User class contains several interesting properties that help simplify the client side code and improve performance. During the onboarding process of Anypic, several Facebook property are accessed and stored on the User. This allows us to later access these properties without making unnecessary requests to Facebook. Since these values are cached, it is possible for them to become stale if the user changes his Facebook information. We don't currently handle this issue in Anypic,but we could query Facebook when the app launches and try update the cache if necessary.

2.3. Activity Class

While the Photo and User classes discussed above accurately model the objects seen in Anypic, the relational information is still missing. We have three types of relationships possible: a User liking a Photo, a User commenting on a Photo and a User following another User. To accomplish this, we could have created three separate many-to-many relationships by using PFRelation. The issue with this approach is that the relational information is dispersed throughout our model. This might not appear to be a problem at first glance, but when trying to build a feed type screen such as the Activity Feed, the required query becomes inefficient and impossible to scale.

To mitigate this issue in Anypic, we modeled all three of these relationships in a single join table called Activity. Every time a user follows, likes or comments, a new activity is created to represent the relationship. As we'll see in Section 5 and Section 6, this makes the unliking and unfollowing actions slightly more complicated since we need to remove the previously created activities.

The Activity class contains a fromUser and a toUser field to represent the initiator and recipient of an activity (ex. user A commented on user B's photo). To identify the type of activity performed, the aptly named type field is used. It is worth mentioning that Anypic also uses a fourth type of activity called join which is created upon sign up and sent to all of the user's Facebook friends who are already on Anypic.

As we'll see in Section 7, this table renders the Activity Feed's query almost trivial.

2.4. Wrapping Up

In this section, we covered Anypic's small but powerful data model. The Photo and User class are fairly straightforward but included some clever caching to help with the client side rendering. The Activity class contains the core of Anypic's social networking capabilities and is modeled to be compatible with the popular "feed" feature. In the next section we'll jump into Anypic's source code by looking at how a user posts a new photo.

3. Posting a new Photo

PAPEditPhotoViewController

The PAPEditPhotoViewController

Apple provides developers with a great set of libraries for interacting with the user's photo collection and on-device camera. Anypic makes full use of these libraries in the PAPTabBarController. We won't go through this class here, but feel free to browse the code and check out Apple's Camera Programming Guide.

The user starts by pressing the middle tab bar button, implemented in PAPTabBarController. The controller responds by showing a UIActionSheet with the option to take a new picture or select one from the photo library. UIKit takes over from here and the user is presented with the PAPEditPhotoViewController once an image is selected. This is the class we'll be exploring in this section of the tutorial. We'll see how Anypic handles processing the image, uploading the file, creating a PFObject and associating it with the image file.

3.1. Preparing the Photo

Since image files can be sizable, we can save precious seconds by starting the upload while the user is entering a comment. However, before uploading the photo, we have a few preparations to do.

Once the user selects a new photo, the PAPTabBarController presents the PAPEditPhotoViewController and we find ourselves in the viewDidLoad method.

// PAPEditPhotoViewController.m

- (void)viewDidLoad {
    . . .
    [self shouldUploadImage:self.image];
}

- (BOOL)shouldUploadImage:(UIImage *)anImage { 
    // Resize the image to be square (what is shown in the preview)  
    UIImage *resizedImage = [anImage resizedImageWithContentMode:UIViewContentModeScaleAspectFit 
                                              bounds:CGSizeMake(560.0f, 560.0f) 
                                interpolationQuality:kCGInterpolationHigh];
    // Create a thumbnail and add a corner radius for use in table views
    UIImage *thumbnailImage = [anImage thumbnailImage:86.0f 
                                    transparentBorder:0.0f 
                                         cornerRadius:10.0f              
                                 interpolationQuality:kCGInterpolationDefault];
    
    // Get an NSData representation of our images. We use JPEG for the larger image
    // for better compression and PNG for the thumbnail to keep the corner radius transparency
    NSData *imageData = UIImageJPEGRepresentation(resizedImage, 0.8f);
    NSData *thumbnailImageData = UIImageJPEGRepresentation(thumbnailImage, 0.8f);
    
    if (!imageData || !thumbnailImageData) {    
        return NO;
    }
    . . .
}

We start by cropping the image into a square and creating a second copy of the image that we'll use as a thumbnail. As we discussed in Section 2.1 we store two versions of the photo in order to boost the performance of UITableViews. We then convert our images to their NSData representation so they are ready to be saved.

3.2. Saving the PFFile

Saving our two images is quite straightforward. We simply create our PFFile objects and call the saveInBackground method. Since we don't need to know when the upload is done, we don't add a callback. Note that at this point the two image files are not tied to any PFObject so they cannot be retrieved from the server.

// PAPEditPhotoViewController.m

- (BOOL)shouldUploadImage:(UIImage *)anImage {
    . . .   

    // Create the PFFiles and store them in properties since we'll need them later
    self.photoFile = [PFFile fileWithData:imageData];
    self.thumbnailFile = [PFFile fileWithData:thumbnailImageData];

    // Request a background execution task to allow us to finish uploading the photo even if the app is backgrounded
    self.fileUploadBackgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
        [[UIApplication sharedApplication] endBackgroundTask:self.fileUploadBackgroundTaskId];
    }];
    
    [self.photoFile saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
        if (succeeded) {
            [self.thumbnailFile saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
                [[UIApplication sharedApplication] endBackgroundTask:self.fileUploadBackgroundTaskId];
            }];
        } else {
            [[UIApplication sharedApplication] endBackgroundTask:self.fileUploadBackgroundTaskId];
        }
    }];
    
    return YES;
}

3.3. Saving the PFObject

After we initiate the upload procedure for our image files, the user can preview the photo and optionally add a comment. If the user hits the cancel button, we simply dismiss the view controller and our PFFiles become orphans (which we can clean up later).

// PAPEditPhotoViewController.m

- (void)cancelButtonAction:(id)sender {
    [self.parentViewController dismissViewControllerAnimated:YES completion:nil];
}

If the user hits the "Publish" button, we need to attach our two PFFiles to a PFObject and create a new comment activity if the user entered one. The logic could be as simple as creating and saving our PFObjects, but we've taken a more robust approach in Anypic. Because of this, the button handler method doneButtonAction: is quite lengthy, so we'll look at it one section at a time. The outline of this method is as follows:

  1. If needed, trim and store the comment text for later
  2. Create (and save) the Photo PFObject using the image files
  3. After the Photo is saved, create and save a new comment if necessary

We start by extracting the comment and trimming it to see if the user left a non-whitespace comment for his new photo. Take a look.

// PAPEditPhotoViewController.m

- (void)doneButtonAction:(id)sender {
    // Trim comment and save it in a dictionary for use later in our callback block
    NSDictionary *userInfo = [NSDictionary dictionary];
    NSString *trimmedComment = [self.commentTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
    if (trimmedComment.length != 0) {
        userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
                                  trimmedComment, 
                                  kPAPEditPhotoViewControllerUserInfoCommentKey,
                                  nil];
    }
    . . .
}

We start by creating a userInfo dictionary and use it to store the comment if the user added one. As you'll see in a bit, we'll use this dictionary to create a PFObject of type Activity for this comment.

Next, we create the Photo PFObject, but use a few nifty tricks to handle some edge cases.

// PAPEditPhotoViewController.m

- (void)doneButtonAction:(id)sender {
    . . .
    // Make sure there were no errors creating the image files
    if (!self.photoFile || !self.thumbnailFile) {
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Couldn't post your photo" 
                                                        message:nil 
                                                       delegate:nil 
                                              cancelButtonTitle:nil 
                                              otherButtonTitles:@"Dismiss",nil];
        [alert show];
        return;
    }
        
    // Create a Photo object
    PFObject *photo = [PFObject objectWithClassName:kPAPPhotoClassKey];
    [photo setObject:[PFUser currentUser] forKey:kPAPPhotoUserKey];
    [photo setObject:self.photoFile forKey:kPAPPhotoPictureKey];
    [photo setObject:self.thumbnailFile forKey:kPAPPhotoThumbnailKey];
    
    // Photos are public, but may only be modified by the user who uploaded them
    PFACL *photoACL = [PFACL ACLWithUser:[PFUser currentUser]];
    [photoACL setPublicReadAccess:YES];
    photo.ACL = photoACL;
    
    // Request a background execution task to allow us to finish uploading 
    // the photo even if the app is sent to the background
    self.photoPostBackgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
        [[UIApplication sharedApplication] endBackgroundTask:self.photoPostBackgroundTaskId];
    }];
    
    // Save the Photo PFObject
    [photo saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
        . . .
    }];
    
    // Dismiss this screen
    [self.parentViewController dismissViewControllerAnimated:YES completion:nil];
}

Before creating our PFObject, we do some basic error checking to ensure there was no problem creating our image files earlier. We then create a new PFObject and set the two images files and the current user on the proper fields. In Anypic, photos are public, but can only be modified or deleted by the owner, so we also set the publicReadAccess ACL on the new object.

With the object created we are now ready to save it. Note that the two PFFiles may still be uploading at this time. That is perfectly fine since Parse will ensure that the associated files have been uploaded before saving the PFObject. However, prior to calling saveInBackground:, we make use of the useful background tasks feature of iOS. This allows us to keep our app from being suspended until we are done uploading and saving all of the necessary files. For this tutorial, it is enough for you to know that we use beginBackgroundTaskWithExpirationHandler: to register our request for background processing and that we later use the endBackgroundTask: method to cancel our request or to terminate the background task if the app was closed. You can read more about background tasks in Apple's App Programming Guide.

After requesting permission for background processing, we call saveInBackground: on our new PFObject and dismiss the view controller.

The last part of this method is the block called after the Photo PFObject has been saved. Here, we create the comment activity if necessary and make use of Anypic's custom caching functionality.

// PAPEditPhotoViewController.m

- (void)doneButtonAction:(id)sender {
. . .
    [photo saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
        if (succeeded) {
            [[PAPCache sharedCache] setAttributesForPhoto:photo likers:[NSArray array] commenters:[NSArray array] likedByCurrentUser:NO];
            
            // userInfo might contain any caption which might have been posted by the uploader
            if (userInfo) {
                NSString *commentText = [userInfo objectForKey:kPAPEditPhotoViewControllerUserInfoCommentKey];
                
                if (commentText && commentText.length != 0) {
                    // create and save photo caption
                    PFObject *comment = [PFObject objectWithClassName:kPAPActivityClassKey];
                    [comment setObject:kPAPActivityTypeComment forKey:kPAPActivityTypeKey];
                    [comment setObject:photo forKey:kPAPActivityPhotoKey];
                    [comment setObject:[PFUser currentUser] forKey:kPAPActivityFromUserKey];
                    [comment setObject:[PFUser currentUser] forKey:kPAPActivityToUserKey];
                    [comment setObject:commentText forKey:kPAPActivityContentKey];
                    
                    PFACL *ACL = [PFACL ACLWithUser:[PFUser currentUser]];
                    [ACL setPublicReadAccess:YES];
                    comment.ACL = ACL;
                    
                    [comment saveEventually];
                    [[PAPCache sharedCache] incrementCommentCountForPhoto:photo];
                }
            }
            
            [[NSNotificationCenter defaultCenter] postNotificationName:PAPTabBarControllerDidFinishEditingPhotoNotification object:photo];
        } else {
            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Couldn't post your photo" message:nil delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Dismiss", nil];
            [alert show];
        }
        [[UIApplication sharedApplication] endBackgroundTask:self.photoPostBackgroundTaskId];
    }];
. . .
}

We start by adding this newly saved image to our cache. Remember that the PAPCache is used to manually manage the like and comment information of images so that the UI stays responsive and accurate when the user likes and comments on photos. We then check if we saved a comment in the userInfo dictionary earlier and if necessary, create a new Activity PFObject of type "comment". Much like Photos, Activities are public but can only be modified by their author so we use the publicReadAccess ACL. After saving the new comment, we increment the photo's comment count in the PAPCache.

Since the PAPEditPhotoViewController was most likely dismissed before this block is called, a user could be looking at the photo timeline. To save them the trouble of manually refreshing, we use the NSNotification to inform this time line view controller that a new photo has been posted. This view controller can then take the necessary steps to refresh itself.

The last thing we do in this block is to call the endBackgroundTask: method. As mentioned earlier, this will either suspend the application if it was currently in the background or cancel the request for background processing since everything was completed while the app was in the foreground.

3.4. Wrapping Up

In this section, we saw some of the challenges encountered when creating a file upload workflow and some interesting optimizations we can make. Since Parse can handle attaching unsaved or even partially uploaded PFFiles, we can start the upload process at anytime and take care of attaching it to a PFObject later. We also discussed how to create background tasks to ensure files aren't lost if the user closes the app before the upload is complete.

In the next section we'll move our attention to the slick photo timeline feature of Anypic.

4. Displaying the Photo timeline

The photo timeline is at the front and center of Anypic. It displays your friends recent photos and allows you to like and comment on them. Due to the similarities between the main timeline screen (the PAPHomeViewController) and the single user profile screen (the PAPAccountViewController), both of these share the same super class, the PAPPhotoTimelineViewController.

PAPHomeViewController PAPAccountViewController

The PAPHomeViewController (left) and the PAPAccountViewController (right),
both subclasses of PAPPhotoTimelineViewController

In this section, we'll focus on the query used by these view controller and briefly peek into the some of the view related mechanisms used to asynchronously load and display the images. We'll only talk about at the PAPHomeViewController and it's super class since the PAPAccountViewController is so similar. If you look at the full source code, you'll notice that the account view controller simply overrides some of its super class's methods to add a header and modify the query.

4.1. The Query

Like many of the view controllers in Anypic, the photo timeline controllers are based on the PFQueryTableViewController. Let's start by looking at the query run by this table.

// PAPPhotoTimelineViewController.m

// *this method is not overridden in the PAPHomeViewController

- (PFQuery *)queryForTable {
    // Query for the friends the current user is following
    PFQuery *followingActivitiesQuery = [PFQuery queryWithClassName:kPAPActivityClassKey];
    [followingActivitiesQuery whereKey:kPAPActivityTypeKey equalTo:kPAPActivityTypeFollow];
    [followingActivitiesQuery whereKey:kPAPActivityFromUserKey equalTo:[PFUser currentUser]];

    // Using the activities from the query above, we find all of the photos taken by
    // the friends the current user is following
    PFQuery *photosFromFollowedUsersQuery = [PFQuery queryWithClassName:self.className];
    [photosFromFollowedUsersQuery whereKey:kPAPPhotoUserKey matchesKey:kPAPActivityToUserKey inQuery:followingActivitiesQuery];
    [photosFromFollowedUsersQuery whereKeyExists:kPAPPhotoPictureKey];

    // We create a second query for the current user's photos
    PFQuery *photosFromCurrentUserQuery = [PFQuery queryWithClassName:self.className];
    [photosFromCurrentUserQuery whereKey:kPAPPhotoUserKey equalTo:[PFUser currentUser]];
    [photosFromCurrentUserQuery whereKeyExists:kPAPPhotoPictureKey];

    // We create a final compound query that will find all of the photos that were
    // taken by the user's friends or by the user
    PFQuery *query = [PFQuery orQueryWithSubqueries:[NSArray arrayWithObjects:photosFromFollowedUsersQuery, photosFromCurrentUserQuery, nil]];
    [query includeKey:kPAPPhotoUserKey];
    [query orderByDescending:@"createdAt"];
    . . .
    return query;
}

The query is quite long and may appear quite complex at first glance, but it is fairly straightforward if we look at it part by part. Remember that the queryForTable method returns a PFQuery that is later run by the table view controller. All this code is simply creating the query object, not executing any queries.

There are two main parts to this query, the first finds all Photos taken by someone the user follows (photosFromFollowedUsersQuery) and the second finds all of the Photos taken by the user (photosFromCurrentUserQuery). The final query (the one returned by the above method) uses these two PFQuerys and creates a compound query using the orQueryWithSubqueries: method.

Let's start with the first part; the photosFromFollowedUsersQuery. The goal here is to query the Photo class and get all of the objects where the user key is in the list of people the current user is following. However, this list is not readily available. We need to query the Activity table to obtain the list based on the "follow" activities. Thus, we begin by creating a query that will find all activities "from" the current user of type follow. Armed with this query we can use the handy whereKey: matchesKey: inQuery: method to create a query on the Photo table and find all objects where the user key matches the user key of the objects returned by our query on the Activity table. Parse will handle all of the logistics behind running these two queries for us.

Next, we create the photosFromCurrentUserQuery. This one is much simpler. We simply query the Photo table for all photos taken by the current user. As mentioned above, once we have our two queries, we combine them with an "or" to create the final query that the table will run.

4.2. Wrapping up

As we've seen in this section, the timeline feature of Anypic is very powerful but quite straightforward to implement. If you check out the full source, you'll notice that we've spent a lot on the views, but that the heavy-lifting is all done by the PFQueryTableViewController and Parse's query and caching features.

5. Displaying and Adding Comments

Commenting on a photo

The PAPPhotoDetailsViewController

The commenting feature of Anypic is accessible from the PAPPhotoDetailsViewController. This view controller displays a selected photo along with the associated likes and comments. It uses a table view to list the comments and displays the photo itself along with the likes in the header of the table. In this section of the tutorial, we'll explore the query used to fetch existing comments and the mechanism used to add a new one.

5.1. Displaying Comments

The photo details controller is based on the useful PFQueryTableViewController, so the table's query is set the in queryForTable: method. Getting the comments relevant to a particular photo is quite simple. As mentioned in Section 2, we use the Activity table as a join table for the various interactions between users and photos. To get a particular photo's comments, we query the Activity table for the objects where the type field is "comment" and the photo field relates to the target image. In order to display the commenter's name and profile picture we use the includeKey: method to receive the commenter's data along with the query results.

// PAPPhotoDetailsViewController.m

- (PFQuery *)queryForTable {
    PFQuery *query = [PFQuery queryWithClassName:self.className];
    [query whereKey:kPAPActivityPhotoKey equalTo:self.photo];
    [query whereKey:kPAPActivityTypeKey equalTo:kPAPActivityTypeComment];
    [query includeKey:kPAPActivityFromUserKey];
    [query orderByAscending:@"createdAt"]; 

    [query setCachePolicy:kPFCachePolicyNetworkOnly];

    // If no objects are loaded in memory, we look to the cache first to fill the table
    // and then subsequently do a query against the network.
    //
    // If there is no network connection, we will hit the cache first.
    if (self.objects.count == 0 || ![[UIApplication sharedApplication].delegate performSelector:@selector(isParseReachable)]) {
        [query setCachePolicy:kPFCachePolicyCacheThenNetwork];
    }
    
    return query;
}

Using the PAPBaseTextCell, the comments are displayed in the table. You may notice however, that there is a lot more in this view than the comments. Everything around the list of comments is either in the table's header or footer. These two views can be found in the PAPPhotoDetailsHeaderView and PAPPhotoDetailsFooterView classes. We won't cover the header in this tutorial, but next up, we're going to take a look at how the footer is used to add a new comment.

5.2. Adding new Comments

The PAPPhotoDetailsFooterView is a rather small class that wraps a UITextField. When we set this view as the footer of the table, we also set the view controller as the text field's delegate.

// PAPPhotoDetailsViewController.m

- (void)viewDidLoad {
    . . .
    // Set table footer
    PAPPhotoDetailsFooterView *footerView = 
        [[PAPPhotoDetailsFooterView alloc] initWithFrame:[PAPPhotoDetailsFooterView rectForView]];
    commentTextField = footerView.commentField;
    [commentTextField setDelegate:self]; // set delegate
    self.tableView.tableFooterView = footerView;
    . . .
}

When the user submits their comment, the textFieldShouldReturn: delegate method is called. Much like in Section 3.3, our implementation is more complex than simply creating and saving the a new PFObject. While that is definitely an option, Anypic demonstrates a more robust approach to this type action. The implementation is quite lengthy so we will go through it section by section. The general outline of this method is:

  1. Create and save a new PFObject
  2. Handle the case where a photo was deleted
  3. Send push notifications
  4. Update the local cache

The code to create and save the new object is mostly straightforward, but we do few extra tricks along the way.

// PAPPhotoDetailsViewController.m

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    // Trim the comment text
    NSString *trimmedComment = [textField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
    
    if (trimmedComment.length != 0 && [self.photo objectForKey:kPAPPhotoUserKey]) {
        // Create the comment activity object
        PFObject *comment = [PFObject objectWithClassName:kPAPActivityClassKey];
        [comment setValue:trimmedComment forKey:kPAPActivityContentKey]; // Set comment text
        [comment setValue:[self.photo objectForKey:kPAPPhotoUserKey] forKey:kPAPActivityToUserKey]; // Set toUser
        [comment setValue:[PFUser currentUser] forKey:kPAPActivityFromUserKey]; // Set fromUser
        [comment setValue:kPAPActivityTypeComment forKey:kPAPActivityTypeKey];
        [comment setValue:self.photo forKey:kPAPActivityPhotoKey];
        
        // Set the proper ACLs
        PFACL *ACL = [PFACL ACLWithUser:[PFUser currentUser]];
        [ACL setPublicReadAccess:YES];
        comment.ACL = ACL;

        // Assume the save will work and increment the comment count cache
        [[PAPCache sharedCache] incrementCommentCountForPhoto:self.photo];
        
        // Show HUD view
        [MBProgressHUD showHUDAddedTo:self.view.superview animated:YES];
        
        // If more than 5 seconds pass since we post a comment, 
        // stop waiting for the server to respond
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:5.0f 
                                                          target:self 
                                                        selector:@selector(handleCommentTimeout:) 
                                                        userInfo:[NSDictionary
                                            dictionaryWithObject:comment 
                                                          forKey:@"comment"] repeats:NO];

        [comment saveEventually:^(BOOL succeeded, NSError *error) {
            . . .
        }
    }
    [textField setText:@""];
    return [textField resignFirstResponder];
}

As we do with most user inputted text, we trim it for whitespace and ensure a message was actually written. We then set the appropriate fields and add an ACL to allow anyone to read the object, but only the creator to modify or delete it. As we discussed in Section 1, we use the PAPCache class as custom caching layer in Anypic. It allows us to track the like and comment attributes of photos. Since we are using saveEventually in this case, we assume there will be no errors and update the cache's comment count before saving the object.

The last steps we take before saving the object, is displaying a loading HUD and starting a 5 second NSTimer. Since saveEventually may not save the object immediately if the server is unreachable, we use this timer to dismiss the HUD and inform the user that the comment will be saved later.

Before moving on to the implementation of the completion callback of the save method, notice the last few lines of the above code snippet. Regardless of the actions taken in the first part of the method, it is a good idea to reset the text field and to dismiss the keyboard.

The first thing we do once the completion block is called is invalidate the NSTimer. If the timer hadn't yet terminated, this will ensure that the alert is not displayed. There is also one very important error case we need to handle in this block. It is possible that the the owner of the image has deleted it and the current user's app hasn't yet been notified.

// PAPPhotoDetailsViewController.m

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    . . .
    [comment saveEventually:^(BOOL succeeded, NSError *error) {
        [timer invalidate]; // Stop the timer if it's still running
        
        // Check if the photo was deleted
        if (error && [error code] == kPFErrorObjectNotFound) {
            // Undo cache update and alert user
            [[PAPCache sharedCache] decrementCommentCountForPhoto:self.photo];
            [[[UIAlertView alloc] initWithTitle:@"Could not post comment"
                                        message:@"This photo was deleted by its owner" 
                                       delegate:nil 
                              cancelButtonTitle:nil 
                              otherButtonTitles:@"OK", nil] show];
            [self.navigationController popViewControllerAnimated:YES];
        }

        [[NSNotificationCenter defaultCenter] postNotificationName:PAPPhotoDetailsViewControllerUserCommentedOnPhotoNotification object:self.photo userInfo:@{@"comments": @(self.objects.count + 1)}];
        
        [MBProgressHUD hideHUDForView:self.view.superview animated:YES];
        [self loadObjects];
    }];
}

If the photo was deleted by the photographer prior to adding our comment, we'll receive an error with the code kPFErrorObjectNotFound. To handle this case, we reset our changes to the cache, alert the user and pop this view controller.

If the comment is saved successfully, there is surprisingly little to do. We send a local NSNotification to alert the timeline view controller to update the comment count for this photo, and then we remove the progress HUD and reload the table.

5.3. Wrapping up

While we won't cover them in this tutorial, there are many interesting facets to the various views used in this controller. The header view for example uses an interesting mechanism for displaying the image and the profile pictures of the likers. You are encouraged to read through some of these classes if you are interested.

6. Liking a Photo

Anypic wouldn't be a real social networking app without the popular "like" feature! Liking a photo in Anypic is done by tapping the heart shaped icon found above or below an image. As shown in the screenshots below, there are several screen where this button is accessible, but we'll focus our attention on the PAPPhotoTimelineViewController's implementation (left most screen in the image below). All three are very similar so check them out in the source code too.

Liking a Photo. From left to right we have the PAPPhotoTimelineViewController, the PAPPhotoDetailsViewController and the PAPPhotoDetailsViewController.

6.1. Handling the Event

When a user taps on the heart shaped like button, an event is fired in the PAPPhotoHeaderView which relays it to its delegate, the PAPPhotoTimelineViewController. As we mentioned in Section 1, we use this type of delegation quite extensively in Anypic to pass around events like this one.

When the event gets to the photo timeline view controller, we find ourselves in the photoHeaderView: didTapLikePhotoButton: photo: delegate method. There are two main parts to this method. First we update the button and the local cache, and then we send the Parse request and handle the response. Let's take a look at the first part of this method.

// PAPPhotoTimelineViewController.m

- (void)photoHeaderView:(PAPPhotoHeaderView *)photoHeaderView didTapLikePhotoButton:(UIButton *)button photo:(PFObject *)photo {
    // Disable the button so users cannot send duplicate requests
    [photoHeaderView shouldEnableLikeButton:NO];
    
    // Set the new state of the button (dark or light)
    BOOL liked = !button.selected;
    [photoHeaderView setLikeStatus:liked];
    
    // Get the current number of likes on the photo
    NSString *originalButtonTitle = button.titleLabel.text;
    NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
    [numberFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]];
    
    // Update the like count in the PAPCache
    NSNumber *likeCount = [numberFormatter numberFromString:button.titleLabel.text];
    if (liked) {
        likeCount = [NSNumber numberWithInt:[likeCount intValue] + 1];
        [[PAPCache sharedCache] incrementLikerCountForPhoto:photo];
    } else {
        if ([likeCount intValue] > 0) {
            likeCount = [NSNumber numberWithInt:[likeCount intValue] - 1];
        }
        [[PAPCache sharedCache] decrementLikerCountForPhoto:photo];
    }
    
    // Add the current user as a liker of the photo in PAPCache
    [[PAPCache sharedCache] setPhotoIsLikedByCurrentUser:photo liked:liked];
    
    // Update the button label
    [button setTitle:[numberFormatter stringFromNumber:likeCount] forState:UIControlStateNormal];
    . . . 
}

Here, we mostly focus on bookkeeping. We disabled the button to avoid duplicate requests, update its state and figure out what the new title label should be. To calculate the new number to display, we extract the previous number on the label using the NSNumberFormatter and increment it. while we increment the number we also take the opportunity to increment the like count in PAPCache.

With the more mundane steps out of the way, we can now send the Parse request to create the necessary activity. Since the liking mechanism is available from many parts of the app, the core code for it is located in the PAPUtility class. We use the static methods likePhotoInBackground:block and unlikePhotoInBackground:block to accomplish like or unlike the target photo. We'll start by looking at the callback which is still in the photoHeaderView:didTapLikePhotoButton:photo method and then look at the implementation of these static methods.

// PAPPhotoTimelineViewController.m

- (void)photoHeaderView:(PAPPhotoHeaderView *)photoHeaderView didTapLikePhotoButton:(UIButton *)button photo:(PFObject *)photo {
    . . .
    // Call the appropriate static method to handle creating/deleting the right object
    if (liked) {
        [PAPUtility likePhotoInBackground:photo block:^(BOOL succeeded, NSError *error) {
            PAPPhotoHeaderView *actualHeaderView = (PAPPhotoHeaderView *)[self tableView:self.tableView viewForHeaderInSection:button.tag];
            [actualHeaderView shouldEnableLikeButton:YES];
            [actualHeaderView setLikeStatus:succeeded];
            
            if (!succeeded) {
                // Revert the button title (the number) if the call fails
                [actualHeaderView.likeButton setTitle:originalButtonTitle forState:UIControlStateNormal];
            }
        }];
    } else {
        [PAPUtility unlikePhotoInBackground:photo block:^(BOOL succeeded, NSError *error) {
            PAPPhotoHeaderView *actualHeaderView = (PAPPhotoHeaderView *)[self tableView:self.tableView viewForHeaderInSection:button.tag];
            [actualHeaderView shouldEnableLikeButton:YES];
            [actualHeaderView setLikeStatus:!succeeded];
            
            if (!succeeded) {
                // Revert the button title (the number) if the call fails
                [actualHeaderView.likeButton setTitle:originalButtonTitle forState:UIControlStateNormal];
            }
        }];
    }
}

Depending on the action being taken by the user, we call the like or unlike version of the method. In both callbacks, we re-enable the like button and set its state based on the success of the network call. If it failed, this will make sure we undo the changes made to the button earlier. We also set title back to its original value in the case of a failure.

6.2. Creating the Like Activity

Now let's look at the implementation for the static likePhotoInBackground:block: method. We won't look at the unlike version of this method, but as you can imagine, the implementation is very similar. Check out the full source code if you're interested.

This method is quite lengthy, so we'll split it up in three parts.

  1. Creating and Saving the Activity PFObject
  2. Running the passed completion block
  3. Updating the cache

Creating the new activity is quite simple and done as you would expect using Parse.

// PAPUtility.m

+ (void)likePhotoInBackground:(id)photo block:(void (^)(BOOL succeeded, NSError *error))completionBlock {
    // Create the like activity and save it
    PFObject *likeActivity = [PFObject objectWithClassName:kPAPActivityClassKey];
    [likeActivity setObject:kPAPActivityTypeLike forKey:kPAPActivityTypeKey];
    [likeActivity setObject:[PFUser currentUser] forKey:kPAPActivityFromUserKey];
    [likeActivity setObject:[photo objectForKey:kPAPPhotoUserKey] forKey:kPAPActivityToUserKey];
    [likeActivity setObject:photo forKey:kPAPActivityPhotoKey];
    
    // Don't forget the ACLs! As with other objects in Anypic, we make it public readonly
    PFACL *likeACL = [PFACL ACLWithUser:[PFUser currentUser]];
    [likeACL setPublicReadAccess:YES];
    likeActivity.ACL = likeACL;
    
    [likeActivity saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
    . . .
    }];
}

We set the type of the activity to "like", add the target photo and set the fromUser and toUser fields to be the current user and the photographer. As always, we add a public read ACL on our activity object to make sure others can read it but cannot delete or modify it. We then call saveInBackground: and wait for the callback. Cloud Code will take care of sending the push notification as seen in Section 9.

The last part of this method updates the cached attributes for this photo in PAPCache.

// PAPUtility.m

+ (void)likePhotoInBackground:(id)photo block:(void (^)(BOOL succeeded, NSError *error))completionBlock {
    . . .
    [likeActivity saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
        . . .
        // Refresh our PAPCache values for this photo
        PFQuery *query = [PAPUtility queryForActivitiesOnPhoto:photo cachePolicy:kPFCachePolicyNetworkOnly];
        [query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
            if (!error) {
                NSMutableArray *likers = [NSMutableArray array];
                NSMutableArray *commenters = [NSMutableArray array];
                
                BOOL isLikedByCurrentUser = NO;
                
                for (PFObject *activity in objects) {
                    if ([[activity objectForKey:kPAPActivityTypeKey] isEqualToString:kPAPActivityTypeLike] && [activity objectForKey:kPAPActivityFromUserKey]) {
                        [likers addObject:[activity objectForKey:kPAPActivityFromUserKey]];
                    } else if ([[activity objectForKey:kPAPActivityTypeKey] isEqualToString:kPAPActivityTypeComment] && [activity objectForKey:kPAPActivityFromUserKey]) {
                        [commenters addObject:[activity objectForKey:kPAPActivityFromUserKey]];
                    }
                    
                    if ([[[activity objectForKey:kPAPActivityFromUserKey] objectId] isEqualToString:[[PFUser currentUser] objectId]]) {
                        if ([[activity objectForKey:kPAPActivityTypeKey] isEqualToString:kPAPActivityTypeLike]) {
                            isLikedByCurrentUser = YES;
                        }
                    }
                }
                
                [[PAPCache sharedCache] setAttributesForPhoto:photo likers:likers commenters:commenters likedByCurrentUser:isLikedByCurrentUser];
                
                // Send local NSNotification to alert in memory view controller to reload relevant data
                [[NSNotificationCenter defaultCenter] postNotificationName:PAPUtilityUserLikedUnlikedPhotoCallbackFinishedNotification 
                object:photo 
                userInfo:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:succeeded] forKey:PAPPhotoDetailsViewControllerUserLikedUnlikedPhotoNotificationUserInfoLikedKey]];

            }
        }];

    }];
}

We start by sending a for all activities on this photo. Since it is not critical for us to have all of the updated cached values for the photo, we simply send the request and wait for the callback. When the query is completed, we parse through the returned results and update the PAPCache for the target photo with this new information. Once the cache is updated, we broadcast a local NSNotification to alert view controllers currently in memory to reload the affected views to reflect any changes.

6.3. Wrapping up

Liking a photo has its challenges, but if we take the time to look at it systematically, things fall into place. We started by learning how the event is initially propagated and some of the immediate concerns we have to handle. We then looked at the crux of the feature and how to interact Parse to complete this action. If you've read Section 5 you may have noticed several similarities between how comments and likes are handled. If you are having trouble wrapping your head around some of the concepts we looked at here, take a look at Section 5 for look at similar use case.

7. Activity feed

A key challenge a social networks is promoting and maintaining high user engagement. Features like the activity feed help encourage user participation by centralizing relevant social interactions. However, creating this view of the data is not trivial. In Section 2 we discussed some these obstacles and took some preemptive design decision to make this feed efficient and scalable. The resulting data model greatly simplifies the query required for an activity feed.

PAPFindFriendsViewController

The PAPPhotoDetailsViewController

The implementation for this feed can be found in the PAPActivityFeedViewController. As a subclass of the PFQueryTableViewController, the data is obtained from the PFQuery set in the queryForTable: method. Let's take a look at this query.

// PAPPhotoDetailsViewController.m

- (PFQuery *)queryForTable {
    // Create query
    PFQuery *query = [PFQuery queryWithClassName:@"Activity"];
    [query whereKey:kPAPActivityToUserKey equalTo:[PFUser currentUser]];
    [query whereKey:kPAPActivityFromUserKey notEqualTo:[PFUser currentUser]];
    [query whereKeyExists:kPAPActivityFromUserKey];
    [query includeKey:kPAPActivityFromUserKey];
    [query includeKey:kPAPActivityPhotoKey];
    [query orderByDescending:@"createdAt"];

    [query setCachePolicy:kPFCachePolicyNetworkOnly];

    // If no objects are loaded in memory, we look to the cache first to fill the table
    // and then subsequently do a query against the network.
    if (self.objects.count == 0 || ![[UIApplication sharedApplication].delegate performSelector:@selector(isParseReachable)]) {
        [query setCachePolicy:kPFCachePolicyCacheThenNetwork];
    }
    
    return query;
}

The query is quite straightforward. We add a constraint to find all Activity objects where the recipient is the current user and exclude the activities the user performed on himself (such as liking his own photo). For added robustness, we also ensure that there is a fromUser value present. Since the cells of the activity feed display both the fromUser and the target photo if applicable, we use the includeKey method to fetch those objects at the same time. And that's it! The query will populate our table and the built-in pagination of the PFQueryTableViewController is used to limit the query results to 25 per page.

The last part of the above method helps improve the caching behavior of the table. For the general case where the user is simply refreshing the table, we set the caching policy to go straight to the network and ignore the cache. However, when no objects are currently loaded or when the device has no reception, we change the caching policy to first check the cache and load as many items as possible. While these may be stale results, it is usually preferable to show some results to the user instead of a blank screen.

8. Following Friends

PAPFindFriendsViewController

The PAPFindFriendsViewController

Anypic's "following" system allows users to modify which of their friends photos are displayed in the timeline. This feature is accessible in the app by navigating to settings and selecting "Find Friends". We'll spend this section exploring the PAPFindFriendsViewController which control this screen.

8.1. Displaying the Friends List

The friends list is populated with the user's Facebook friends who are currently registered to Anypic. As a bonus, we've also hard coded the Parse employees so you can follow us as well in the live version of the app.

Obtaining this list of friends is a two step process. We first need to fetch all of the user's Facebook friends and then make a query to Parse to find out which of these friends are currently using Anypic. To avoid making two queries, we've decided to cache the list of Facebook friends on the User model class. This allows us to obtain the list of active friends by making a single query to Parse. Since we use the PFQueryTableViewController, let's take a look at the query we implement in thequeryForTableView` method.

// PAPFindFriendsViewController.m

- (PFQuery *)queryForTable {
    // Use cached facebook friend ids
    NSArray *facebookFriends = [[PAPCache sharedCache] facebookFriends];
    
    // Query for all friends you have on facebook and who are using the app
    PFQuery *friendsQuery = [PFUser query];
    [friendsQuery whereKey:kPAPUserFacebookIDKey containedIn:facebookFriends];
    
    // Query for all Parse employees
    NSMutableArray *parseEmployees = [[NSMutableArray alloc] initWithArray:kPAPParseEmployeeAccounts];
    [parseEmployees removeObject:[[PFUser currentUser] objectForKey:kPAPUserFacebookIDKey]];
    PFQuery *parseEmployeeQuery = [PFUser query];
    [parseEmployeeQuery whereKey:kPAPUserFacebookIDKey containedIn:parseEmployees];
        
    // Combine the two queries with an OR
    PFQuery *query = [PFQuery orQueryWithSubqueries:[NSArray arrayWithObjects:friendsQuery, parseEmployeeQuery, nil]];
    query.cachePolicy = kPFCachePolicyNetworkOnly;
    
    if (self.objects.count == 0) {
        query.cachePolicy = kPFCachePolicyCacheThenNetwork;
    }
    
    [query orderByAscending:kPAPUserDisplayNameKey];
    
    return query;
}

We start by making a query to find all of the users who's FacebookId match one of your cached Facebook friends'. In a typical app this simple query would be fine, but for Anypic, we decided to give you the option to follow some of the Parse employees. To enable this, we add a second query which finds all of the users who's FacebookId is in a hard coded list of Parse employees. We then join these two queries with an OR using the orQueryWithSubqueries: method and return it.

8.2. The Follow and Unfollow Actions

To toggle the following status of a given friend we can use the cell button, the navigation bar "Follow All" button, or the "Follow" button located on each user's profile screen. In order to reuse the follow action implementation, each of these buttons trigger a method in the PAPUtility class. In this tutorial, we'll take a look at the cell buttons, but make sure to look at the full source code if you're interested in learning more about how the other ones work.

When a user taps on the cell button, an action is triggered in the PAPFindFriendsCell class which relays the event to its delegate, PAPFindFriendsViewController. Using the button's state, the method determines whether the user needs to be followed or unfollowed and the necessary method is called in PAPUtility. Notice that we also send a local notification (using the NSNotificationCenter not a Push Notification) when a friend is successfully followed. If the user is looking at the PAPPhotoTimelineViewController when the server replies with a success, this notification will be used to refresh the timeline and include the new user's photos.

// PAPFindFriendsViewController.m

// PAPFindFriendsCell delegate method
- (void)cell:(PAPFindFriendsCell *)cellView didTapFollowButton:(PFUser *)aUser {
    [self shouldToggleFollowFriendForCell:cellView];
}

- (void)shouldToggleFollowFriendForCell:(PAPFindFriendsCell*)cell {
    PFUser *cellUser = cell.user;
    if ([cell.followButton isSelected]) {
        // Unfollow
        cell.followButton.selected = NO;
        [PAPUtility unfollowUserEventually:cellUser];
    } else {
        // Follow
        cell.followButton.selected = YES;
        [PAPUtility followUserEventually:cellUser block:^(BOOL succeeded, NSError *error) {
            if (!error) {
                [[NSNotificationCenter defaultCenter] postNotificationName:PAPUtilityUserFollowingChangedNotification object:nil];
            } else {
                cell.followButton.selected = NO;
            }
        }];
    }
}

The followUserEventually: method is quite straightforward. As we discussed in Section 2, the Activity model is used as a feed of all activities such as "follow". Thus, this method simply needs to create a new activity for this table and save it. As you may have noticed in the previous code snippet, we change the button state before receiving confirmation that the server has processed our request. This helps keep the UI responsive and give immediate feedback to the user. However, it can be tricky to ensure that the server eventually receives the request. Thankfully, Parse offers the handy saveEventually method queues up the request a guarantees that it will be sent to the server, even if the user does not currently have reception.

// PAPUtility.m

+ (void)followUserEventually:(PFUser *)user block:(void (^)(BOOL succeeded, NSError *error))completionBlock {
    if ([[user objectId] isEqualToString:[[PFUser currentUser] objectId]]) {
        return;
    }
    
    // Create follow activity
    PFObject *followActivity = [PFObject objectWithClassName:kPAPActivityClassKey];
    [followActivity setObject:[PFUser currentUser] forKey:kPAPActivityFromUserKey];
    [followActivity setObject:user forKey:kPAPActivityToUserKey];
    [followActivity setObject:kPAPActivityTypeFollow forKey:kPAPActivityTypeKey];
    
    // Set the proper ACL
    PFACL *followACL = [PFACL ACLWithUser:[PFUser currentUser]];
    [followACL setPublicReadAccess:YES];
    followActivity.ACL = followACL;
    
    // Save the activity and set the block passed as the completion block
    [followActivity saveEventually:completionBlock];
}

8.3. Wrapping Up

Creating a following/follower model can be tricky but given the proper schema, it is definitely possible. In our implementation, we used a Facebook friends cache on the User class to determine which users you should be allowed to follow. To modify the who a user is following, we relied heavily on the Activity join table. We discuss the rationale behind this decision in Section 2, but there are several alternatives possible. Make sure to explore different solutions if your app has different needs.

9. Push Notifications

PAPFindFriendsViewController

A Push Notification

Push notifications are an incredibly powerful user engagement tool. It not only helps bring your users back to your app but also encourages them to use it. This is especially important in a social networking application like Anypic. Setting up Push in iOS can be challenging, so we've dedicated an entire tutorial to this topic. In this section we'll talk about how Anypic uses advanced targeting to direct push notifications to the right user. Anypic uses some basic Cloud Code to achieve this, so you may want to look at our Cloud Code introduction tutorial first.

9.1. Tracking User's Installations

If you want to follow along with the source, you'll find all of the code for this section in the Anypic-cloud folder.

Anypic uses Installations to track which devices it is installed on. An Installation does not necessarily equal a unique user, so we must add a pointer to the currently logged-in user to our Installation objects. We can automatically do this using Cloud Code.

In Cloud Code, when an object is about to be saved, any beforeSave hooks on the object will be executed. The object being saved is available at request.object. If a user was loggedin when the call was made, their object will be available through request.user. Put two and two together, and you can ensure that installations always point to the current user:

// Make sure all installations point to the current user.
Parse.Cloud.beforeSave(Parse.Installation, function(request, response) {
  Parse.Cloud.useMasterKey();
  if (request.user) {
    request.object.set('user', request.user);
  } else {
    request.object.unset('user');
  }
  response.success();
});

Installations are updated whenever the deviceToken changes, the user updates the app, or their timeZone information changes, for example. Always make sure to set the device token on your instalations to ensure your users keep receiving push notifications even after they restore Anypic on a new device.

// AppDelegate.m

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)newDeviceToken {
    ...
    PFInstallation *currentInstallation = [PFInstallation currentInstallation];
    [currentInstallation setDeviceTokenFromData:deviceToken];
    [currentInstallation saveInBackground];
}

Now, what happens if the user logs out? We don't want them to keep receiving push notifications, so we must remove the link to this user when logging out. Note that our Cloud Code already handles this scenario by unsetting the user link when there is no longer a valid user session associated with the installation.

// AppDelegate.m

- (void)logOut { 
    . . .
    // Unsubscribe from push notifications by removing the user association from the current installation.
    [[PFInstallation currentInstallation] removeObjectForKey:kPAPInstallationUserKey];
    [[PFInstallation currentInstallation] saveInBackground];
}

9.2. Sending Push Notifications

The simplest way to send a push notification from an app is to use the PFPush convenience method sendPushMessageToChannelInBackground. This is useful during development when "client side push" is enabled and you're using a small amount of channels.

[PFPush sendPushMessageToChannelInBackground:@"someChannel"
                                 withMessage:@"Mattieu: That is a nice pic of you!"];

In Anypic, however, we use advanced targeting with installations. Instead of a channel, we construct a query that targets all the installations associated with the user who posted the original photo. This is how we would send such a push notification:

// Create our Installation query
PFQuery *pushQuery = [PFInstallation query];
[pushQuery whereKey:@"user" equalTo:photoOwner];
 
// Send push notification to query
PFPush *push = [[PFPush alloc] init];
[push setQuery:pushQuery]; // Set our Installation query
[push setMessage:@"Mattieu: That is a nice pic of you!"];
[push sendPushInBackground];

In Anypic, however, we don't allow users to send arbitrary push notifications to other users from the app itself, as we disable the option to send pushes from the client side. Instead, we use Cloud Code. This is an example of how we would send the same push notification using the JavaScript SDK:

var query = new Parse.Query(Parse.Installation);
query.equalTo('user', photoOwner);
 
Parse.Push.send({
  where: query, // Set our Installation query
  data: {
    alert: "Mattieu: That is a nice pic of you!"
  }
});

Of course, we do this automatically whenever a new comment is added, or whenever someone likes a photo. Both actions generate an Activity object, so we can use an afterSave hook to automate this:

Parse.Cloud.afterSave('Activity', function(request) {
  if (request.object.get("type") === "comment") {
    var query = new Parse.Query(Parse.Installation);
    query.equalTo('user', request.object.get("toUser"));

    Parse.Push.send({
      where: query, // Set our Installation query.
      data: {
        alert: request.object.get("content")
      }
    });
  }
});

With this in place, any new comments will trigger a push notification to be sent to the owner of the photo, with the contents of the comment within the push. This is a simplified version of the actual hook used by Anypic, and you can see the full source code in activity.js.

Push can do a lot more than just send a message, however. Anypic uses a more complex approach that allows us to display a different view controller based on the notification opened by the user. For example, if a user opens the notification "Mattieu: That is a nice pic of you!", Anypic will display the details view controller for this photo. This is achieved by sending some additional metadata as part of the push notificaiton, which can then be used by Anypic to load the right view controller when the push is acknowledged.

An example JSON payload would look like:

{
  "alert": "This is the string that is displayed by the push notification.",
  "badge": "Increment", // this tells Parse to autoincrement the red badge icon by 1.
  "p": "a", // Payload Type: "a" for Activity.
  "t": "c", // Payload Type Modifier: "c" for Comment, "l" for Like, "f" for Follow.
  "fu": "objectId", // From User's Object Id
  "pid": "objectId" // Photo's Object Id
}

Payloads on iOS are limited to 256 bytes, but there is no such limit when targeting Android devices. Keep this in mind when writing a cross-platform app. You can either send a single push to all devices that observes the minimum constraints set forth by iOS, or you can instead send different push notifications to iOS and Android each with their own payload.

9.3. Responding to Incoming Push Notifications

Now that we've seen the type of data we can send in a push notification, let's take a look at how we handle receiving this notification in the app. When the app is opened from a push notification, the payload is given as part of the launch options in the famous application:didFinishLaunchingWithOptions: method. By inspecting the payload, we'll direct the user to photo indicated in the notification.

// AppDelegate.m

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    . . .
    [self handlePush:launchOptions]; // Call the handle push method with the payload
    return YES;
}

- (void)handlePush:(NSDictionary *)launchOptions {
    // Extract the notification payload dictionary
    NSDictionary *remoteNotificationPayload = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
    
    // Check if the app was open from a notification and a user is logged in
    if (remoteNotificationPayload && [PFUser currentUser]) {
        
        // Push the referenced photo into view
        NSString *photoObjectId = [remoteNotificationPayload objectForKey:kPAPPushPayloadPhotoObjectIdKey];
        if (photoObjectId && photoObjectId.length != 0) {
            PFQuery *query = [PFQuery queryWithClassName:kPAPPhotoClassKey];
            [query getObjectInBackgroundWithId:photoObjectId block:^(PFObject *photo, NSError *error) {
                if (!error) {
                    PAPPhotoDetailsViewController *detailViewController = [[PAPPhotoDetailsViewController alloc] initWithPhoto:photo];
                    UINavigationController *homeNavigationController = [[self.tabBarController viewControllers] objectAtIndex:PAPHomeTabBarItemIndex];
                    [self.tabBarController setSelectedViewController:homeNavigationController];
                    [homeNavigationController pushViewController:detailViewController animated:YES];
                }
            }];
        }            
    }
}

If we determine the app was opened from a notification and that we have a user logged in, we try to extract a photo id. It is possible that this is simply a marketing message so no photo is available in the payload. In such a case, the app simply opens normally. If we find an image, we send a query to obtain the object, and push a new instance of the PAPPhotoDetailsViewController on the navigation stack when the query returns.

9.4. Wrapping up

And there you have it! An end to end example of creating a more complex push notification flow. There's a lot you can do with push, so check out our iOS Guide for up to date information on the push capabilities!


Anypic is a wealth of mobile knowledge and we can't possibly cover the entire app in this tutorial. So if you are interested in any topic that is not covered here, simply grab the code from the link above and let your curiosity run free!