iOS App Development iOS Swift Tutorial iOS8 Xcode App Design YikYak

Parse backed UITableView in iOS 8 : Build Yik Yak Clone Part 2

This is the Part 2 of the Building Yik Yak Clone. If you haven’t looked at Part1 I suggest you to look at Building Yik Yak Clone Part 1 before continuing further.

In the part one we left at the point where the data in the UITableView was static and hardcoded in the app. In the real world application we normally tend to fetch the data from a REST based server or Mobile backend as a service like Parse. In this tutorial series we will use Parse for storing the data. Find more on how to Integrate Parse in your app

Lets get started.

  • Add a bar button item on to the top right of the table view and change the identifier to Compose
  • Drag a view controller next to the existing table view controller.
  • CTRL+Drag from the compose button on to the view controller you just added and select present modally
  • Now embed the view controller you just selected in a navigation controller using Editor -> Embed in -> navigation controller
  • Drag a text view on to the new view controller you just added and have the settings/attributes as mentioned below.
    images
  • In this particular example I am not adding autolayout constraints and getting scrollviews to work with Autolayouts is a bit tricky.. I will have a seperate post just for that. For now set these values(or appropriate values) in the size inspector.
    imagesimages

Now lets drag two bar button items on the navigation bar and call them Cancel and Post. CTRL+Drag from these button onto PostViewController.swift by opening them side by side using Assistant Editor and call them cancelPressed and postPressed accordingly.

Now we have the skeleton for the app setup.

Parse provides an implementation of UITableViewController called PFQueryTableViewController which provides many features like pulltorefresh, pagination and loading data from Parse. So to use this feature we need to make sure that our implementation of tableview controller TableViewController.swift should extend PFQueryTableViewController instead of UITableViewController similarly our TableViewCell.swift should extend PFTableViewCell.
Once you are done with changing those files. Let go ahead and create the upvote and downvote functionality

We need to have the IBAction for the topButton and buttonButton. Create two new IBAction from these button to TableViewController file and name them topButton and bottomButton accordingly.

To identify which yak was upvoted or downvoted we need to find which cell these upvote and downvote belong to. There is a simple way to do that, first identify the point in the tableview the touch happened and convert it to indexPath using indexPathForRowAtPoint function.

 let hitPoint = sender.convertPoint(CGPointZero, toView: self.tableView)
 let hitIndex = self.tableView.indexPathForRowAtPoint(hitPoint)

At this point the updated code would look like this

//
//  TableViewController.swift
//  YikYak
//
//  Created by Shrikar Archak on 12/31/14.
//  Copyright (c) 2014 Shrikar Archak. All rights reserved.
//

import UIKit
import CoreLocation

class TableViewController: PFQueryTableViewController, CLLocationManagerDelegate {

    var yaks = ["Getting Started with building a Yik Yak Clone in Swift","Xcode 6 Tutorial using Autolayouts",
        "In this tutorial you will also learn how to talk to Parse Backend", "Learning Swift by building real world applications", "Test"]

    let locationManager = CLLocationManager()
    var currLocation : CLLocationCoordinate2D?

    override init!(style: UITableViewStyle, className: String!) {
        super.init(style: style, className: className)
    }


    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        self.parseClassName = "Yak"
        self.textKey = "text"
        self.pullToRefreshEnabled = true
        self.objectsPerPage = 200

    }

    private func alert(message : String) {
        let alert = UIAlertController(title: "Oops something went wrong.", message: message, preferredStyle: UIAlertControllerStyle.Alert)
        let action = UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: nil)
        let cancel = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel, handler: nil)
        let settings = UIAlertAction(title: "Settings", style: UIAlertActionStyle.Default) { (action) -> Void in
            UIApplication.sharedApplication().openURL(NSURL(string: UIApplicationOpenSettingsURLString)!)
            return
        }
        alert.addAction(settings)
        alert.addAction(action)
        self.presentViewController(alert, animated: true, completion: nil)
    }


    override func viewDidLoad() {
        super.viewDidLoad()
        self.tableView.estimatedRowHeight = 60
        self.tableView.rowHeight = UITableViewAutomaticDimension
        locationManager.desiredAccuracy = 1000
        locationManager.delegate = self
        locationManager.requestWhenInUseAuthorization()
        locationManager.startUpdatingLocation()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    func locationManager(manager: CLLocationManager!, didFailWithError error: NSError!) {
        alert("Cannot fetch your location")
    }

    override func queryForTable() -> PFQuery! {
        let query = PFQuery(className: "Yak")
        if let queryLoc = currLocation {
            query.whereKey("location", nearGeoPoint: PFGeoPoint(latitude: queryLoc.latitude, longitude: queryLoc.longitude), withinMiles: 10)
            query.limit = 200;
            query.orderByDescending("createdAt")
        } else {
            /* Decide on how the application should react if there is no location available */
            query.whereKey("location", nearGeoPoint: PFGeoPoint(latitude: 37.411822, longitude: -121.941125), withinMiles: 10)
            query.limit = 200;
            query.orderByDescending("createdAt")
        }

        return query
    }

    func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
        locationManager.stopUpdatingLocation()
        if(locations.count > 0){
            let location = locations[0] as CLLocation
            println(location.coordinate)
            currLocation = location.coordinate
        } else {
            alert("Cannot fetch your location")
        }
    }


    override func objectAtIndexPath(indexPath: NSIndexPath!) -> PFObject! {
        var obj : PFObject? = nil
        if(indexPath.row < self.objects.count){
            obj = self.objects[indexPath.row] as PFObject
        }

        return obj
    }


    override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!, object: PFObject!) -> PFTableViewCell! {
        let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as TableViewCell
        cell.yakText.text = object.valueForKey("text") as String
        cell.yakText.numberOfLines = 0
        let score = object.valueForKey("count") as Int
        cell.count.text = "\(score)"
        cell.time.text = "\((indexPath.row + 1) * 3)m ago"
        cell.replies.text = "\((indexPath.row + 1) * 1) replies"
        return cell
    }

    @IBAction func topButton(sender: AnyObject) {
        let hitPoint = sender.convertPoint(CGPointZero, toView: self.tableView)
        let hitIndex = self.tableView.indexPathForRowAtPoint(hitPoint)
        let object = objectAtIndexPath(hitIndex)
        object.incrementKey("count")
        object.saveInBackground()
        self.tableView.reloadData()
        NSLog("Top Index Path \(hitIndex?.row)")
    }

    @IBAction func bottomButton(sender: AnyObject) {
        let hitPoint = sender.convertPoint(CGPointZero, toView: self.tableView)
        let hitIndex = self.tableView.indexPathForRowAtPoint(hitPoint)
        let object = objectAtIndexPath(hitIndex)
        object.incrementKey("count", byAmount: -1)
        object.saveInBackground()
        self.tableView.reloadData()
        NSLog("Bottom Index Path \(hitIndex?.row)")
    }

}

And this is the part where we save the object in Parse

//
//  PostViewController.swift
//  YikYak
//
//  Created by Shrikar Archak on 12/31/14.
//  Copyright (c) 2014 Shrikar Archak. All rights reserved.
//

import UIKit

class PostViewController: UIViewController, UITextViewDelegate, CLLocationManagerDelegate {


    @IBOutlet weak var postView: UITextView!
    var currLocation: CLLocationCoordinate2D?
    var reset:Bool = false
    let locationManager = CLLocationManager()

    private func  alert() {
        let alert = UIAlertController(title: "Cannot fetch your location", message: "Please enable location in the settings menu", preferredStyle: UIAlertControllerStyle.Alert)
        let action = UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: nil)
        let cancel = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel, handler: nil)
        let settings = UIAlertAction(title: "Settings", style: UIAlertActionStyle.Default) { (action) -> Void in
                UIApplication.sharedApplication().openURL(NSURL(string: UIApplicationOpenSettingsURLString)!)
                return
            }
        alert.addAction(settings)
        alert.addAction(action)
        self.presentViewController(alert, animated: true, completion: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        self.postView.selectedRange = NSMakeRange(0, 0);
        self.postView.delegate = self
        self.postView.becomeFirstResponder()
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.delegate = self
        locationManager.requestWhenInUseAuthorization()
        locationManager.startUpdatingLocation()
    }

    func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
        locationManager.stopUpdatingLocation()
        if(locations.count > 0){
            let location = locations[0] as CLLocation
            currLocation = location.coordinate
        } else {
            alert()
        }
    }

    func locationManager(manager: CLLocationManager!, didFailWithError error: NSError!) {
        println(error)
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func cancelPressed(sender: AnyObject) {
        self.dismissViewControllerAnimated(true , completion: nil)
    }

    @IBAction func postPressed(sender: AnyObject) {

        if(currLocation != nil){
            let testObject = PFObject(className: "Yak")
            testObject["text"] = self.postView.text
            testObject["count"] = 0
            testObject["replies"] = 0
            testObject["location"] = PFGeoPoint(latitude: currLocation!.latitude , longitude: currLocation!.longitude)
            testObject.saveInBackground()
            self.dismissViewControllerAnimated(true , completion: nil)
        } else {
            alert()
        }



    }
    func textViewDidChange(textView: UITextView) {
        if(reset == false){
            self.postView.text = String(Array(self.postView.text)[0])
            reset = true
        }
    }

}

Note once you save the object you can pull down the tableview to see the latest post you created.

Here is the link to the github YikYak
Please let me know if you have any questions/feedback.

About the author

Shrikar

Backend/Infrastructure Engineer by Day. iOS Developer for the rest of the time.

  • mrdaco

    Hello, Great tutorial,

    I have an error: TableViewController does not have a member named ‘objects’ in the following ligne:

    if(indexPath.row < self.objects.count){

    I'm using the files available in github repo, any ideas ? Any Help will be appreciated

    Thanks

    • mrdaco

      Actually I cannot find where it is declared anywhere in the files, do I miss something ?

      • http://shrikar.com shrikar

        Hey its provided by PFQueryTableViewController these are the objects that will be loaded by Parse.

        • mrdaco

          Hi shrikar, thanks for the prompt answer,
          That make total sense, awesome … I did miss something !

          As I made some addons and using UITableViewController, PFQueryTableViewController is a subclass of UIViewController, I’ll certainly have to make containment to achieve this, or is it any other options ?

  • Keith Strickling

    Hi Shrikar,

    Thanks for a great tutorial!! I just have one question for you: why did you implement the IBActions for when the user taps the upvote or downvote buttons in TableViewController rather than in TableViewCell? On first glance, it seems like it would be simpler to implement them within TableViewCell as then you would not need to determine which post the user voted on using hitpoints. What is the advantage to doing it the way you have?

    Thank you!

  • Nisse Söderström

    running your stuff i get tons of errors, even when just downloading it from GitHub straight away. could you please update with a second and third video so i can follow along. the video and the text differs quite a lot…

  • PostGab

    I have done all the steps successfully, except at run time my cells have nil values despite the fact I’ve made numerous posts…is there some kind of error going on while fetching the data from Parse? I have successfully sent the yak data to Parse but I get nil values for the score, reply count, and there is no characters on the text. Additionally, the cell is smaller than usual, at a deformed height ruining my constraints

  • charlie

    ello?
    h

  • charlie

    i need help

  • charlie

    it says to CTRL+Drag from these button onto PostViewController.swift by opening them side by side using Assistant Editor and call them cancelPressedand postPressed accordingly. i don’t have a postviewcontroller.swift and i don’t know where to call the names of th buttons cancel and post

  • Chin Up

    when did you save the “location” to the class “Yak”? Thanks!

    • http://shrikar.com shrikar

      The location is stored in the postPressed. You set the location in the PFObject and save in background.

  • Joshua

    I’ve done everything correctly in the code but I can’t get the messages to display on the query. I’m able to post correctly and see it in parse but for some won’t show in the feed. Is there something I’m doing wrong?

  • charlie

    this tutorial just stops at the beginning of part 2 i have no firkin clue what i am doing I’ve tried too many times (about 10) its just too annoying and lacks basic communication skills. i posted a comment for help 3 firkin months ago and got nothing thanks a lot dude u really helped me.

/* ]]> */