Parse backed UITableView in iOS 8 : Build Yik Yak Clone Part 2
<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.