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

January 03, 2015
Shrikar Archak

<Image alt="Parse backed UITableView in iOS 8 : Build Yik Yak Clone Part 2" objectFit="contain" src="/static/images/yik-yak-web-logo.jpg" height={350} width={1000} placeholder="blur" quality={100} />

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.

    <Image alt="Parse backed UITableView in iOS 8 : Build Yik Yak Clone Part 2" objectFit="contain" src="/static/images/yikyak31.png" height={350} width={1000} placeholder="blur" quality={100} />

  • 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. <Image alt="Parse backed UITableView in iOS 8 : Build Yik Yak Clone Part 2" objectFit="contain" src="/static/images/yikyak32.png" height={350} width={1000} placeholder="blur" quality={100} />

<Image alt="Parse backed UITableView in iOS 8 : Build Yik Yak Clone Part 2" objectFit="contain" src="/static/images/yikyak33.png" height={350} width={1000} placeholder="blur" quality={100} />

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.

Subscribe to the newsletter

Get notified when new content or topic is released.

You won't receive any spam! ✌️