Product College starts on September 5. Submit your application today. Apply now.

Implementing a Friend Search in Parse

Implementing a Friend Search in Parse

May 26, 2015

In this chapter, we will add a feature that will allow users to find & follow friends. We will start by setting up the UI in Interface Builder, then we'll fill in the implementation in code.

The explanations and instructions in this step will not be as detailed as in the previous ones - the view controller we are about to build behaves very similar to the main view controller in the notes app. Therefore it won't be necessary to discuss all of the involved concepts.

If you are incorporating any kind of search into your app, the code in this step should serve as a very good template!

Setting up the Friend Search UI

This is what the final UI will look like, once the entire app is complete:

image

At the root level we only have two components: a search Bar and a table view.

Adding a Search Bar

Add a search bar to the FriendSearchViewController as shown in the video below:

Remember that the last step is to use the shortkey ⌘⌥= to update the frame according to its new constraints. Do this each time you finish adding a component in the steps below.

Adding a Table View

Add a table view to the FriendSearchViewController as shown in the video below:

Adding a Custom Table View Cell

Add a table view cell to the FriendSearchViewController as shown in the video below:

Creating Code Connections

We'll need multiple code connections to generate the cells from code and to implement the follow button. We'll also need code connections for the search bar.

Cell identifier

Let's start by setting up an identifier for our new table view cell.

Set the identifier for the FriendSearchViewController's table view cell to UserCell: image

Custom Cell Class

Next, create a new class for this cell.

Create a new UITableViewCell subclass called FriendSearchTableViewCell and add it to the View group as shown below. Remember to first add a folder on the filesystem, then add that folder to Xcode. That way groups and folders stay in sync: image

Referencing Outlets and Button Callback

Then connect the new class to the table view cell.

Set the custom class of the table view cell to FriendSearchTableViewCell: image

Next, set up referencing outlets for the label and the button on the table view cell. Also, add a callback for button taps:

Create the three code connections outlined below with the FriendSearchTableViewCell: image

Table View Data Source

Now, set the table view's data source to be the FriendSearchViewController.

Set the Table View Data Source as as shown below: image

Referencing Outlets for FriendSearchViewController

Next, set up referencing outlets to the FriendSearchViewController from the table view and the search bar.

Set up the following referencing outlets: image

Search Bar Delegate

Finally, set the delegate of the UISearchBarDelegate protocol to be the FriendSearchViewController.

Set up the search bar delegate as shown below: image

Adding the Friend Search Code

As discussed at the beginning of this step, we won't discuss the code in detail. The source code has comments in all the relevant places. We will provide you with the full source code that you need for each class - you should take time to read through it and make sure that you understand it. We'll then discuss a few interesting, high-level details about the solution.

Addding Parse Requests

First we are going to add 5 different Parse requests.

Add the following methods to the ParseHelper class:

 // MARK: Following

/**
  Fetches all users that the provided user is following.

  :param: user The user whose followees you want to retrieve
  :param: completionBlock The completion block that is called when the query completes
*/
static func getFollowingUsersForUser(user: PFUser, completionBlock: PFArrayResultBlock) {
  let query = PFQuery(className: ParseFollowClass)

  query.whereKey(ParseFollowFromUser, equalTo:user)
  query.findObjectsInBackgroundWithBlock(completionBlock)
}

/**
  Establishes a follow relationship between two users.

  :param: user    The user that is following
  :param: toUser  The user that is being followed
*/
static func addFollowRelationshipFromUser(user: PFUser, toUser: PFUser) {
  let followObject = PFObject(className: ParseFollowClass)
  followObject.setObject(user, forKey: ParseFollowFromUser)
  followObject.setObject(toUser, forKey: ParseFollowToUser)

  followObject.saveInBackgroundWithBlock(nil)
}

/**
  Deletes a follow relationship between two users.

  :param: user    The user that is following
  :param: toUser  The user that is being followed
*/
static func removeFollowRelationshipFromUser(user: PFUser, toUser: PFUser) {
  let query = PFQuery(className: ParseFollowClass)
  query.whereKey(ParseFollowFromUser, equalTo:user)
  query.whereKey(ParseFollowToUser, equalTo: toUser)

  query.findObjectsInBackgroundWithBlock {
    (results: [AnyObject]?, error: NSError?) -> Void in

      let results = results as? [PFObject] ?? []

      for follow in results {
        follow.deleteInBackgroundWithBlock(nil)
      }
  }
}

// MARK: Users

/**
  Fetch all users, except the one that's currently signed in.
  Limits the amount of users returned to 20.

  :param: completionBlock The completion block that is called when the query completes

  :returns: The generated PFQuery
*/
static func allUsers(completionBlock: PFArrayResultBlock) -> PFQuery {
  let query = PFUser.query()!
  // exclude the current user
  query.whereKey(ParseHelper.ParseUserUsername,
    notEqualTo: PFUser.currentUser()!.username!)
  query.orderByAscending(ParseHelper.ParseUserUsername)
  query.limit = 20

  query.findObjectsInBackgroundWithBlock(completionBlock)

  return query
}

/**
Fetch users whose usernames match the provided search term.

:param: searchText The text that should be used to search for users
:param: completionBlock The completion block that is called when the query completes

:returns: The generated PFQuery
*/
static func searchUsers(searchText: String, completionBlock: PFArrayResultBlock)
  -> PFQuery {
  /*
    NOTE: We are using a Regex to allow for a case insensitive compare of usernames.
    Regex can be slow on large datasets. For large amount of data it's better to store
    lowercased username in a separate column and perform a regular string compare.
  */
  let query = PFUser.query()!.whereKey(ParseHelper.ParseUserUsername,
    matchesRegex: searchText, modifiers: "i")

  query.whereKey(ParseHelper.ParseUserUsername,
    notEqualTo: PFUser.currentUser()!.username!)

  query.orderByAscending(ParseHelper.ParseUserUsername)
  query.limit = 20

  query.findObjectsInBackgroundWithBlock(completionBlock)

  return query
}

We've added a total of 5 different queries. All of these queries will be used by the FriendSearchViewController.

Two are used to search for users. One returns all users (except the signed in one) - that query is used when the search bar in the FriendSearchViewController is empty.

The other user search query takes the current search string and returns the users that match it.

It's noteworthy that both of these methods return a PFQuery object. This allows the FriendSearchViewController to keep a reference to the request that is currently going on. When a user types into the search field, we will kick off a new search request every time the text changes; you'll see that later in the code for the FriendSearchViewController. Using the reference to the current query, the FriendSearchViewController will cancel the current request before starting a new one. That way we prevent a fast-typing user from causing many requests to start in parallel. Whenever we start a new search query, the old query is outdated. So if it is still ongoing, we can cancel it since we are no longer interested in these outdated results.

The other three methods are used to add, remove and retrieve followees of the current user. These are pretty standard Parse queries without any noteworthy implementation details.

Using these 5 queries the FriendSearchViewController will be able to display users that we are searching for and to mark whether or not we are following them.

Implementing the FriendSearchTableViewCell

Next, let's discuss the implementation of the FriendSearchTableViewCell. The main features of that cell are displaying a username and a follow button. That follow button can indicate whether or not we are already following a user.

When the button is tapped, we want Makestagram to follow / unfollow the person. However, we won't implement that directly in the FriendSearchTableViewCell. Typically we want to keep more complex functionality outside of our views. Our solution is to define a delegate that will be responsible for performing the follow / unfollow.

The delegate of each cell will be the FriendSearchViewController. When the follow button is tapped, the FriendTableViewCell will inform its delegate.

Replace the contents of FriendSearchTableViewCell.swift with the following one:

import UIKit
import Parse

protocol FriendSearchTableViewCellDelegate: class {
  func cell(cell: FriendSearchTableViewCell, didSelectFollowUser user: PFUser)
  func cell(cell: FriendSearchTableViewCell, didSelectUnfollowUser user: PFUser)
}

class FriendSearchTableViewCell: UITableViewCell {

  @IBOutlet weak var usernameLabel: UILabel!
  @IBOutlet weak var followButton: UIButton!
  weak var delegate: FriendSearchTableViewCellDelegate?

  var user: PFUser? {
    didSet {
      usernameLabel.text = user?.username
    }
  }

  var canFollow: Bool? = true {
    didSet {
      /*
        Change the state of the follow button based on whether or not
        it is possible to follow a user.
      */
      if let canFollow = canFollow {
        followButton.selected = !canFollow
      }
    }
  }

  @IBAction func followButtonTapped(sender: AnyObject) {
    if let canFollow = canFollow where canFollow == true {
      delegate?.cell(self, didSelectFollowUser: user!)
      self.canFollow = false
    } else {
      delegate?.cell(self, didSelectUnfollowUser: user!)
      self.canFollow = true
    }
  }
}

Once again, there aren't too many new concepts in this code. After you took a detailed look at the code, we can move on to the core component: the FriendSearchViewController.

Implementing the FriendSearchViewController

The FriendSearchViewController is very similar to the main View Controller in Make School Notes. It has two different states: searching or not searching. Based on that state it calls one of the two different Parse queries that we defined earlier.

The biggest novelty in the FriendSearchViewController is the concept of a local cache. We create a special property called followingUsers that stores which users the current user is following. When one of the FriendSearchTableViewCells triggers a unfollow / follow, we send a request to Parse, but we also update the followingUsers property immediately. As you will see in the code, this allows us to update the UI immediately, without waiting for the server to respond.

Replace the content of FriendSearchViewController.swift with the following code:

import UIKit
import ConvenienceKit
import Parse

class FriendSearchViewController: UIViewController {

  @IBOutlet weak var searchBar: UISearchBar!
  @IBOutlet weak var tableView: UITableView!

  // stores all the users that match the current search query
  var users: [PFUser]?

  /*
    This is a local cache. It stores all the users this user is following.
    It is used to update the UI immediately upon user interaction, instead of waiting
    for a server response.
  */
  var followingUsers: [PFUser]? {
    didSet {
      /**
        the list of following users may be fetched after the tableView has displayed
        cells. In this case, we reload the data to reflect "following" status
      */
      tableView.reloadData()
    }
  }

  // the current parse query
  var query: PFQuery? {
    didSet {
      // whenever we assign a new query, cancel any previous requests
      oldValue?.cancel()
    }
  }

  // this view can be in two different states
  enum State {
    case DefaultMode
    case SearchMode
  }

  // whenever the state changes, perform one of the two queries and update the list
  var state: State = .DefaultMode {
    didSet {
      switch (state) {
      case .DefaultMode:
        query = ParseHelper.allUsers(updateList)

      case .SearchMode:
        let searchText = searchBar?.text ?? ""
         query = ParseHelper.searchUsers(searchText, completionBlock:updateList)
      }
    }
  }

  // MARK: Update userlist

  /**
    Is called as the completion block of all queries.
    As soon as a query completes, this method updates the Table View.
  */
  func updateList(results: [AnyObject]?, error: NSError?) {
    self.users = results as? [PFUser] ?? []
    self.tableView.reloadData()

  }

  // MARK: View Lifecycle

  override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)

    state = .DefaultMode

    // fill the cache of a user's followees
    ParseHelper.getFollowingUsersForUser(PFUser.currentUser()!) {
      (results: [AnyObject]?, error: NSError?) -> Void in
        let relations = results as? [PFObject] ?? []
        // use map to extract the User from a Follow object
        self.followingUsers = relations.map {
          $0.objectForKey(ParseHelper.ParseFollowToUser) as! PFUser
        }

    }
  }

}

// MARK: TableView Data Source

extension FriendSearchViewController: UITableViewDataSource {

  func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return self.users?.count ?? 0
  }

  func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("UserCell") as! FriendSearchTableViewCell

    let user = users![indexPath.row]
    cell.user = user

    if let followingUsers = followingUsers {
      // check if current user is already following displayed user
      // change button appereance based on result
      cell.canFollow = !followingUsers.contains(user)
    }

    cell.delegate = self

    return cell
  }
}

// MARK: Searchbar Delegate

extension FriendSearchViewController: UISearchBarDelegate {

  func searchBarTextDidBeginEditing(searchBar: UISearchBar) {
    searchBar.setShowsCancelButton(true, animated: true)
    state = .SearchMode
  }

  func searchBarCancelButtonClicked(searchBar: UISearchBar) {
    searchBar.resignFirstResponder()
    searchBar.text = ""
    searchBar.setShowsCancelButton(false, animated: true)
    state = .DefaultMode
  }

  func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
    ParseHelper.searchUsers(searchText, completionBlock:updateList)
  }

}

// MARK: FriendSearchTableViewCell Delegate

extension FriendSearchViewController: FriendSearchTableViewCellDelegate {

  func cell(cell: FriendSearchTableViewCell, didSelectFollowUser user: PFUser) {
    ParseHelper.addFollowRelationshipFromUser(PFUser.currentUser()!, toUser: user)
    // update local cache
    followingUsers?.append(user)
  }

  func cell(cell: FriendSearchTableViewCell, didSelectUnfollowUser user: PFUser) {
    if var followingUsers = followingUsers {
      ParseHelper.removeFollowRelationshipFromUser(PFUser.currentUser()!, toUser: user)
      // update local cache
      removeObject(user, fromArray: &followingUsers)
      self.followingUsers = followingUsers
    }
  }

}

Take your time to read through this implementation and the comments in the source code! The implementation of the FriendSearchViewController completes the Friend Search feature.

Once you are done you can move on to the next step: importing test data.

Getting Additional Users into Makestagram

To test all of this new functionality we need multiple users with multiple posts stored on our server. There are two ways you can accomplish this:

  1. Create new users in the Parse Data Browser, then log in with these users and create posts.
  2. Download the data that we have prepared for you.

The downloaded data contains multiple users and a few posts. You can import them into your server through the Parse data browser.

1. Unzip the Parse data that you downloaded 2. Use the import functionality in the Parse Data Browser to select the two Parse .json files and upload them:

Now you should be able to try out the new feature. Follow another user, then refresh the timelime:

You should see our posts show up on the timeline! This is very exciting. Now you can use the app with multiple users!

Don't worry if you do not see any images for the imported posts. It's due to limitations of Parse export/import functionality.

Conclusion

This step serves as a nice template for implementing a search screen in Parse. It was mostly a re-iteration of things you have learned earlier on. Hopefully this re-iteration made you more comfortable in working with Interface Builder and building view controllers from scratch!

In the next step we will discuss how to add a signup and login screen to Makestagram!

Feedback

If you have feedback on this tutorial or find any mistakes, please open issues on the GitHub Repository.

Summer academy

An iOS Development Summer Course

Design, code and launch your own app. Locations across the USA and Asia

Find your location

Product College

A computer science college

Graduate into a successful career as a founder or software engineer.

Learn more