PFQuery inside cellForRowAtIndexPath run multiple times - ios

I'm using Parse and I'm building social networking app, user can comment on a post and another users could like that post just like Facebook App.
This is a piece of my code
override func viewDidLoad() {
super.viewDidLoad()
// Get all comments
var query = PFQuery(className: "Comment")
query.findObjectsInBackgroundWithBlock { comments, error in
if error == nil {
self.comments = comments
}
}
}
override func tableView(tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let commentForThisRow = self.comments[indexPath.row] // contains array of PFObject
// Get all activities which like this comment
var likeQuery = PFQuery(className: "Activity")
likeQuery.whereKey(kSFActivityType, equalTo: kSFActivityTypeLike)
likeQuery.whereKey(kFSActivityComment, equalTo: commentForThisRow)
likeQuery.findObjectsInBackground {
// Store array of users who like this comment to the cell
}
}
The problem is, because cell for row at indexpath called everytime user scrolls the tableView, it makes the likeQuery run everytime and do request.
How to make the likeQuery request only run one time?

Related

How to reload tableview after adding new entry?

I am creating a cloudkit tableview. I load the app and my tableview appears with my entries from cloud kit.
I then use my add method insertNewObject which adds the record to cloud kit but this does not show up in my tableview. It will only show up on my next run of the app.
func insertNewObject(sender: AnyObject) {
let record = CKRecord(recordType: "CloudNote")
record.setObject("New Note", forKey: "Notes")
MyClipManager.SaveMethod(Database!, myRecord:record)
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
This is my add method. I am calling tableview reload as you can see but nothing is happening.
My tableview creation code:
// Tableview stuff --- Done
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
/////// Get number of rows
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count
}
//// FIll the table
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
let object = objects[indexPath.row]
cell.textLabel!.text = object.objectForKey("Notes") as? String
return cell
}
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
As requested: Method that saves to CloudDB
func SaveMethod(publicDatabase: CKDatabase, myRecord: CKRecord ) -> CKRecord {
publicDatabase.saveRecord(myRecord, completionHandler:
({returnRecord, error in
if let err = error {
self.notifyUser("Save Error", message:
err.localizedDescription)
} else {
dispatch_async(dispatch_get_main_queue()) {
self.notifyUser("Success",
message: "Record saved successfully")
}
}
}))
return myRecord
}
My viewdidload method in masterview:
override func viewDidLoad() {
super.viewDidLoad()
// Database loading on runtime
Database = container.privateCloudDatabase
///Build Query
let query = CKQuery(recordType: "CloudNote", predicate: NSPredicate(format: "TRUEPREDICATE"))
///Perform query on DB
Database!.performQuery(query, inZoneWithID: nil) { (records, error) -> Void in
if (error != nil) {
NSLog("Error performing query. \(error.debugDescription)")
return
}
self.objects = records!
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
You should not reload your entire tableView when you insert a single object. Only do that when you know ALL the data has changed.
To do what you want, this is the order:
Insert a new data object into your datasource (self.objects). Make sure you get the index of where it ends up in the array.
Call insertRowAtIndexPath: with the correct indexPath on your tableView. This will make sure your data and tableView are in sync again, and tableView:cellForRowAtIndexPath: is called for at least your new data object (and possible others, as certain cells might now be reused to display other data).
Note that the order is always: update your data first, then update your UI (the only place I know of that his is hairy is when using a UISwitch).

Swift 2 + Parse: Array index out of range

SOMETIMES THE REFRESH WORKS SOMETIMES IT DOESN'T
I have a UITableViewController which is basically a news feed. I have also implemented a pull to refresh feature. However sometimes when I pull to refresh it gives me the error
'Array index out of range'.
I know this means an item it is trying to get does not exist but can you tell me why? Here is my code:
override func viewDidLoad() {
super.viewDidLoad()
refresher = UIRefreshControl()
refresher.attributedTitle = NSAttributedString(string: "Pull to refresh")
refresher.addTarget(self, action: "refresh", forControlEvents: UIControlEvents.ValueChanged)
self.tableView.addSubview(refresher)
refresh()
tableView.delegate = self
tableView.dataSource = self
}
and the refresh() function:
func refresh() {
//disable app while it does stuff
UIApplication.sharedApplication().beginIgnoringInteractionEvents()
//get username and match with userId
let getUser = PFUser.query()
getUser?.findObjectsInBackgroundWithBlock({ (objects, error) -> Void in
if let users = objects {
//clean arrays and dictionaries so we dont get indexing error???
self.messages.removeAll(keepCapacity: true)
self.users.removeAll(keepCapacity: true)
self.usernames.removeAll(keepCapacity: true)
for object in users {
if let user = object as? PFUser {
//make userId = username
self.users[user.objectId!] = user.username!
}
}
}
})
let getPost = PFQuery(className: "Posts")
getPost.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if error == nil {
if let objects = objects {
self.messages.removeAll(keepCapacity: true)
self.usernames.removeAll(keepCapacity: true)
for object in objects {
self.messages.append(object["message"] as! String)
self.usernames.append(self.users[object["userId"] as! String]!)
self.tableView.reloadData()
}
}
}
}
self.refresher.endRefreshing()
UIApplication.sharedApplication().endIgnoringInteractionEvents()
}
and:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let myCell = tableView.dequeueReusableCellWithIdentifier("SinglePostCell", forIndexPath: indexPath) as! PostCell
//ERROR GETS REPORTED ON THE LINE BELOW
myCell.usernamePosted.text = usernames[indexPath.row]
myCell.messagePosted.text = messages[indexPath.row]
return myCell
}
You have a race condition given you are doing two background tasks, where the second depends on values returned from the first. getUser?.findObjectsInBackgroundWithBlockwill return immediately, and getPost.findObjectsInBackgroundWithBlock will start executing. The getPost should be inside the block for getUser, to ensure the sequence is correct.
Similarly, the following two lines should be inside the second block:
self.refresher.endRefreshing()
UIApplication.sharedApplication().endIgnoringInteractionEvents()
Given the error line, you probably also have a race condition between the two background tasks and displaying the tableView. I would be inclined to try:
func tableView(tableView:UITableView!, numberOfRowsInSection section:Int) {
return self.refresher.refreshing ? 0 : self.usernames.count
}
This way you won't touch self.usernames until the background refresh is finished (as long as you remember to put endRefreshing inside the second block, which is also put inside the first block).
I Believe that in self.users[user.objectId!] = user.username! the user.ObjectId is some random value assigned by parse which looks like this: "34xcf4". This is why you might be getting 'Array index out of range'.
There are two required methods for configuring a UITableView:
tableView(_:cellForRowAtIndexPath:)
and
tableView(_:numberOfRowsInSection:)
In your code you are presenting only one required method, if you don't implement the second method then it that may cause errors.
Check the documentation at:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableViewDataSource_Protocol/#//apple_ref/occ/intfm/UITableViewDataSource/tableView:cellForRowAtIndexPath:
You are calling self.tableView.reloadData() on every addition to your array and doing so in a background thread.
As a general rule, you should not do UI updates in a background thread. When you clear self.messages and self.usernames, because you are in background thread, nothing prevents the tableview from trying to get a cell at an index that no longer has any data in the array.
If you want to keep your code in the background thread (risky as it may be), you should at least call .beginUpdates before reloading your arrays and wait until they're all done before calling reload and endUpdates.

UITableView: scroll to load more Youtube items

I'm fetching videos from Youtube channel, I'm able to display 10 videos when APP is starting.
I would like to use the following trick (using pagetoken ; Retrieve all videos from youtube playlist using youtube v3 API) to display more videos.
The idea is to scroll down to fetch the next videos, by using willDisplayCell method.
ViewController:
override func viewDidLoad() {
super.viewDidLoad()
self.model.delegate = self
model.getFeedVideo()
self.tableView.dataSource = self
self.tableView.delegate = self
}
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if !loadingData && indexPath.row == 10 - 1 {
model.getFeedVideo()
print("loading more ...")
}
}
How to use pageToken from ViewController whereas I get the token
from VideoModel class?
How to automatically increment the "10" for
indexPath attribute? Is it possible to get the current number of displayed
cell?
VideoModel:
protocol VideoModelDelegate {
func dataReady()
}
class VideoModel: NSObject {
func getFeedVideo() {
var nextToken:String
nextToken = "XXXXXX"
// Fetch the videos dynamically via the Youtube Data API
Alamofire.request(.GET, "https://www.googleapis.com/youtube/v3/playlistItems", parameters: ["part":"snippet", "playlistId":PLAYLIST_ID, "key":API_KEY, "maxResults":10, "pageToken":nextToken], encoding: ParameterEncoding.URL, headers: nil).responseJSON { (response) -> Void in
if let JSON = response.result.value {
nextToken = JSON["nextPageToken"] as! String
print(nextToken)
var arrayOfVideos = [Video]()
for video in JSON["items"] as! NSArray {
// Creating video object...
// Removed to make it clearer
}
self.videoArray = arrayOfVideos
if self.delegate != nil {
self.delegate?.dataReady()
}
}
}
}
Do I need to add a return value or should I create another method to get the pageToken? Please, suggest.
I will give you a general idea on how to accomplish it.
First: Check if user has scrolled to the last element with if !loadingData && indexPath.row == self.videos.count - 1
Second(edit):
//VideoModel.swift
if self.delegate != nil {
//Pass fetched videos and token to the delegate method.
self.delegate?.dataReady(self.videoArray, nextToken)
}
Now, few changes in delegate method:
//ViewController.swift
func dataReady(Array->Videos,String->Token) {
//Define a variable globally and assign received token for later use
tokenGlobal = Token
//add received array of videos to the current array of videos
[self.videos addObjectsFromArray:Videos];
//Here are two options to populate new videos,
//First, Reload whole table view, not a recommended approach, since it will take much more time to load
self.tableView.reloadData()
//Second, add cells to the existing tableView, instead of loading whole table it will added new cells according to the newly added videos into the array.
}
Now when user reaches the end of tableView, pass the token we have saved in global variable like this:
if !loadingData && indexPath.row == self.videos - 1 {
model.getFeedVideo(tokenGlobal)
print("loading more ...")
}
Now, use this tokenGlobal to fetch more videos using 'getFeedVideo' Method.
Look for SO to find out how to add new cells to table view. But for testing you can proceed with reloadData.
P.S: I don't know the Swift syntax. :)

Pull to Refresh: data refresh is delayed

I've got Pull to Refresh working great, except when the table reloads there is a split second delay before the data in the table reloads.
Do I just have some small thing out of place? Any ideas?
viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl?.addTarget(self, action: "handleRefresh:", forControlEvents: UIControlEvents.ValueChanged)
self.getCloudKit()
}
handleRefresh for Pull to Refresh:
func handleRefresh(refreshControl: UIRefreshControl) {
self.objects.removeAll()
self.getCloudKit()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
refreshControl.endRefreshing()
})
}
Need the data in two places, so created a function for it getCloudKit:
func getCloudKit() {
publicData.performQuery(query, inZoneWithID: nil) { results, error in
if error == nil { // There is no error
for play in results! {
let newPlay = Play()
newPlay.color = play["Color"] as! String
self.objects.append(newPlay)
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
} else {
print(error)
}
}
}
tableView:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath)
let object = objects[indexPath.row]
if let label = cell.textLabel{
label.text = object.matchup
}
return cell
}
This is how you should do this:
In your handleRefresh function, add a bool to track the refresh operation in process - say isLoading.
In your getCloudKit function just before reloading the table view call endRefreshing function if isLoading was true.
Reset isLoading to false.
Importantly - Do not remove your model data before refresh operation is even instantiated. What if there is error in fetching the data? Delete it only after you get response back in getCloudKit function.
Also, as a side note, if I would you, I would implement a timestamp based approach where I would pass my last service data timestamp (time at which last update was taken from server) to server and server side would return me complete data only there were changes post that timestamp else I would expect them to tell me no change. In such a case I would simple call endRefreshing function and would not reload data on table. Trust me - this saves a lot and gives a good end user experience as most of time there is no change in data!

IOS/Swift rendering table after JSON request

I am new to IOS and swift. I am trying to implement an api get request that returns json and then display it in a table. Below is my current code. When I run simulator I am getting the following error:
fatal error: Cannot index empty buffer
If I remove the hardcoded return 3 in the tableView function and instead use doDatItems.count nothing renders in the table because I guess the array of doDatItems starts empty before the get request is made. It seems like a timing thing? How do I ensure the get request is made before the table loads?
import UIKit
class ViewController: UIViewController, UITableViewDelegate {
var doDatItems:[String] = []
#IBOutlet weak var doDatItem: UITextField!
#IBOutlet weak var yourDoDats: UILabel!
#IBAction func addDoDat(sender: AnyObject) {
doDatItems.append(doDatItem.text)
println(doDatItems)
}
override func viewDidLoad() {
super.viewDidLoad()
let urlPath = "http://localhost:3000/dodats"
let url: NSURL = NSURL(string: urlPath)!
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(url, completionHandler: {data, response, error -> Void in
println("Task completed")
if((error) != nil) {
// If there is an error in the web request, print it to the console
println(error.localizedDescription)
}
var err: NSError?
var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &err) as NSDictionary
if(err != nil) {
// If there is an error parsing JSON, print it to the console
println("JSON Error \(err!.localizedDescription)")
} else {
let dataArray = jsonResult["dodats"] as [String]
for item in dataArray {
self.doDatItems.append(item)
}
// println(self.doDatItems)
}
})
task.resume()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")
println(self.doDatItems)
cell.textLabel?.text = self.doDatItems[indexPath.row]
return cell
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I found several problems -
You ViewController should conform to UITableViewDatasource (which is missing, not sure how it went that far)
Do not return 3 when self.doDatItems does not have any items. It will cause a crash. As long as the data loads let the table remain empty. return self.doDatItems.count
Once data is loaded and ready to display from self.doDatItems array, just call reloadData() method of your table view.
Before that, you should have a reference (or IBOutlet) of your tableView so that you can call reloadData() from anywhere.
You need to trigger a page refresh once the data has been received and parsed.
Something along the lines of
self.tableView.reloadData()
In this delegate method, which return number of rows,
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
Don't use static number as 3. Get your Array Count & return the count in here.
After the json response comes, reload the tableView. In objective-c it's done by
[tableView reloadData];

Resources