Using PFQuery inside cellForRowAtIndexPath - ios

I was thinking about PFQuery.
I'm developing an App that shows a Feed to the Users and it also displays a Like counter for each Post (like a Facebook App or Instagram App).
So in my PFQueryTableViewController I have my main query, that basically show all the Posts:
override func queryForTable() -> PFQuery {
let query = PFQuery(className: "Noticias")
query.orderByDescending("createdAt")
return query
}
And I use another query to count the number of Likes on another Class in Parse that contais all the Likes.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFTableViewCell? {
var cell = tableView.dequeueReusableCellWithIdentifier("FeedCellIdentifier") as! FeedCell!
if cell == nil {
cell = FeedCell(style: UITableViewCellStyle.Default, reuseIdentifier: "FeedCellIdentifier")
}
let query2 = PFQuery(className:"commentsTable")
query2.whereKey("newsColumn", equalTo: object!)
query2.findObjectsInBackgroundWithBlock {
(objectus: [PFObject]?, error: NSError?) -> Void in
if error == nil {
let quantidade = objectus!.count
let commentQuantidade = String(quantidade)
cell.comentariosLabel.text = commentQuantidade
} else {
// Log details of the failure
print("Error: \(error!) \(error!.userInfo)")
}
}
This way to code works, and I achieve what I want, but! I know that I'm reusing cells, I know that this block of code is called everytime a cell appear.
And I know those facts:
A lot of query requests is sent to Parse Cloud, everytime I scroll the tableview
It's possible to see the values changing, when I'm scrolling the tableview, for example, because I'm reusing the cells a post has a value of my previous cell and then with the new query it's refreshed, this works but not look good for user experience.
So, my main doubt is, is it the right way to code? I think not, and I just want another point of view or an idea.
Thanks.
EDIT 1
As I said I've updated my count method to countObjectsInBackgroundWithBlock instead of findObjectsInBackgroundWithBlock but I'm not able to move the query to the ViewDidLoad, because I use the object to check exactly how many comments each Post have.
EDIT 2
I've embed the query to count the number of comments for each post and printing the results, now I'm think my code is better than the previous version, but I'm not able to pass the result to a label because I'm receiving a error:
Use of unresolved identifier 'commentCount'
I'm reading some documentations about Struct
Follows my updated code bellow:
import UIKit
import Social
class Functions: PFQueryTableViewController, UISearchBarDelegate {
override func shouldAutorotate() -> Bool {
return false
}
var passaValor = Int()
let swiftColor = UIColor(red: 13, green: 153, blue: 252)
struct PostObject{
let post : PFObject
let commentCount : Int
}
var posts : [PostObject] = []
// Initialise the PFQueryTable tableview
override init(style: UITableViewStyle, className: String!) {
super.init(style: style, className: className)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
// The className to query on
self.parseClassName = "Noticias"
// The key of the PFObject to display in the label of the default cell style
self.textKey = "text"
// Uncomment the following line to specify the key of a PFFile on the PFObject to display in the imageView of the default cell style
self.imageKey = "image"
// Whether the built-in pull-to-refresh is enabled
self.pullToRefreshEnabled = true
// Whether the built-in pagination is enabled
self.paginationEnabled = true
// The number of objects to show per page
self.objectsPerPage = 25
}
// Define the query that will provide the data for the table view
override func queryForTable() -> PFQuery {
let query = super.queryForTable()
return query
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(true)
loadObjects()
}
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func viewDidLoad() {
super.viewDidLoad()
// navigationBarItems()
let query = PFQuery(className:"Noticias")
query.findObjectsInBackgroundWithBlock {
(objects: [PFObject]?, error: NSError?) -> Void in
// The find succeeded.
print("Successfully retrieved \(objects!.count) scores.")
// Do something with the found objects
if let objects = objects {
for object in objects {
let queryCount = PFQuery(className:"commentsTable")
queryCount.whereKey("newsColumn", equalTo: object)
queryCount.countObjectsInBackgroundWithBlock {
(contagem: Int32, error: NSError?) -> Void in
let post = PostObject(object, commentCount:commentCount)
posts.append(post)
print("Post \(object.objectId!) has \(contagem) comments")
}
self.tableView.reloadData()
}
}
}
//Self Sizing Cells
tableView.estimatedRowHeight = 350.0
tableView.rowHeight = UITableViewAutomaticDimension
}
// Define the query that will provide the data for the table view
//override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFTableViewCell? {
var cell = tableView.dequeueReusableCellWithIdentifier("FeedCellIdentifier") as! FeedCell!
if cell == nil {
cell = FeedCell(style: UITableViewCellStyle.Default, reuseIdentifier: "FeedCellIdentifier")
}
cell?.parseObject = object
if let assuntoNoticia = object?["assunto"] as? String {
cell?.assuntoNoticia?.text = assuntoNoticia
}
if let pontos = object?["pontos"] as? Int {
let pontosPosts = String(pontos)
cell?.pontosLabel?.text = String(pontosPosts)
}
if let zonaLabel = object?["zona"] as? String {
cell?.zonaLabel?.text = zonaLabel
}
if let criticidade = object?["criticidade"] as? String {
if criticidade == "Problema"{
cell.criticidadeNoticia.backgroundColor = UIColor.redColor()
} else {
cell.criticidadeNoticia.backgroundColor = UIColor.greenColor()
}
}
return cell
}
}
And the result of print:
Successfully retrieved 5 scores.
Post wSCsTv8OnH has 4 comments
Post LbwBfjWPod has 0 comments
Post fN4ISVwqpz has 0 comments
Post 1rXdQr2A1F has 1 comments
Post eXogPeTfNu has 0 comments

Better practice would be to query all data on view load saving it into model and then read data from it on table view scroll. When processing query you can show downloading indicator or placeholder data. When query is complete you'll call tableView.reloadData()
You can accomplish this by creating a new variable like this:
var cellModels : [PFObject] = []
In your query2.findObjectsInBackgroundWithBlock:
for object in objectus{
self.cellModels.append(object)
}
self.tableView.reloadData()
And in cellForRowAtIndexPath:
let model = cellModels[indexPath.row]
// configure cell according to model
// something like cell.textLabel.text = model.text
P.S You should take a look at method countObjectsInBackgroundWithBlock if you only need to get count of objects. Because if there're a lot of e.g comments findObjectsInBackgroundWithBlock will return maximum of 1000 objects and still you won't be downloading whole objects, only one number this will speed up query and spare user's cellular plan.
Update: Also if you need to store numbers of comments you can create simple struct like this:
struct PostObject{
let post : PFObject
let commentCount : Int
}
var posts : [PostObject] = []
And when you query for you posts you loop through received objects and populate posts array.
for object in objects{
// create countObjectsInBackgroundWithBlock query to get comments count for object
// and in result block create
let post = PostObject(object, commentCount:commentCount)
posts.append(post)
}
tableView.reloadData()
And in cellForRowAtIndexPath:
let post = posts[indexPath.row]
cell.postCountLabel.text = String(post.commentCount)
// configure cell accordingly

You should do your queries before you present the information in your tableview.

Related

Get results from Firebase dictionary?

I am able to retrieve results from a Firebase query but I am having trouble retrieving them as a dictionary to populate a tableView. Here's how I'm storing the query results:
var invites: Array<FIRDataSnapshot> = []
func getAlerts(){
let invitesRef = self.rootRef.child("invites")
let query = invitesRef.queryOrderedByChild("invitee").queryEqualToValue(currentUser?.uid)
query.observeEventType(.Value, withBlock: { snapshot in
self.invites.append(snapshot)
print(self.invites)
self.tableView.reloadData()
})
}
Printing self.invites returns the following:
[Snap (invites) {
"-KKQWErkyuehmbxom8NO" = {
invitedBy = T2k7Bm9G9RNLcHLvLlKApRbnas23;
invitee = dRJ1FqctSfTlLF8iO2ddlc9BANJ3;
role = guardian;
};
}]
I'm having trouble populating the tableView. The cell labels don't show anything:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
let inviteDict = invites[indexPath.row].value as! [String : AnyObject]
let role = inviteDict["role"] as? String
cell.textLabel!.text = role
return cell
}
Any ideas?
Thanks!
EDIT: Printed the dictionary to console after my function is run, and it's printing []. Why is it losing it's values?
override func viewWillAppear(animated: Bool) {
getAlerts() // inside of function prints values
print(self.invites) //prints []
}
EDIT 2: Paul's solution worked!! I added the following function to have Firebase "listen" for results! I may have to edit this to only show alerts for the logged in user, but this has at least pointed me in the right direction:
override func viewWillAppear(animated: Bool) {
getAlerts()
configureDatabase()
}
func configureDatabase() {
// Listen for new messages in the Firebase database
let ref = self.rootRef.child("invites").observeEventType(.ChildAdded, withBlock: { (snapshot) -> Void in
self.invites.append(snapshot)
self.tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: self.invites.count-1, inSection: 0)], withRowAnimation: .Automatic)
})
}
Check out the friendlychat Firebase codelab for an example of populating a table view from an asynchronous call. Specifically, see viewDidLoad and configureDatabase in FCViewController.swift.
Your array will always return [ ] because Firebase is async. If you add var invites: Array<FIRDataSnapshot> = [] inside of the Firebase closure you will print the data you are looking for, but im assumming you would like to use the variable outside of the closure. in order to complete that you must create a completion within getAlerts() function. If you are not sure how to complete that do a google search and that will solve your question.

Selecting data to be loaded into table view Swift

I have a table view controller and above the cell is a segmented control. The segmented control has 3 options. Past Posts, Current Posts, and Future Posts. I am trying to figure out how to load the specific data into the table view depending on what index is selected on the segmented control.
For example if Past Posts is selected I want to load the Past Post data from Parse Server into the table view. Or of Future Posts is selected load the Future Posts date from Parse Server into the table view.
I am not at all sure how to load the "selected" data, then remove and load different data if the index changes. Any help is much appreciated!
Also, I know how to fetch data from Parse Server. I only mention that to explain where my data is coming from.
I would do something creating a controller that performs the fetch, the parsing, and returns a closure with the associated identifier if it ever changes, you can still use this approach. Something along these lines.
UPDATE
With help from Rob's answer I wanted to put a little context into my answer for completeness.
typealias PostsCompletionClosure = (requestIdentifier : String, posts : [Post])->Void
class PostController {
func fetchPastPosts(requestIdentifier : String,
completion : PostsCompletionClosure,
queue : dispatch_queue_t?) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
let queryParams = ["status" : "past"]
self.performQuery(queryParams, completion: { (requestID, posts) in
dispatch_async(queue != nil ? queue : dispatch_get_main_queue()) {
completion(requestIdentifier : requestIdentifier, posts : posts)
}
})
}
}
func fetchCurrentPosts(requestIdentifier : String,
completion : PostsCompletionClosure,
queue : dispatch_queue_t?) {
// Same as Above
}
func fetchFuturePosts(requestIdentifier : String,
completion : PostsCompletionClosure,
queue : dispatch_queue_t?) { {
// Same as Above
}
private func performQuery(queryParams: [String : String],
completion : PostsCompletionClosure) {
let query = PFQuery(className: "Posts")
for {key, value) in queryParams {
query.whereKey(key, equalTo: value)
}
query.findObjectsInBackgroundWithBlock { objects, error in
guard let error == nil else {
// Handle Error
return
}
if let results = objects as? [Post] {
dispatch_get_main_queue()) {
completion(requestIdentifier : requestIdentifier, posts : posts)
}
})
}
}
You can even create a post request queue for the segment requests, and cancel all prior operations if you are about to start a new one, thus never even giving it an opportunity to reload your data in the first place.
Here is a possible approach on how to implement the viewController :)
class ViewController: UIViewController, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
#IBOutlet var segnmentControl: UISegmentedControl!
var posts: [Post]?
var activeRequestId: String = ""
// This should prolly be injected or a singleton
let postsController = PostController()
override func viewDidLoad() {
super.viewDidLoad()
didSelectSelegment(segnmentControl)
}
#IBAction func didSelectSelegment(sender: UISegmentedControl) {
posts = nil
tableView.reloadData()
activeRequestId = "\(sender.selectedSegmentIndex)"
switch sender.selectedSegmentIndex {
case 0:
self.postsController.fetchPastPosts(activeRequestId, completion: { (requestIdentifier, posts) in
self.reloadDataWith(requestIdentifier, posts : [Post])
})
case 1:
self.postsController.fetchCurrentPosts(activeRequestId, completion: { (requestIdentifier, posts) in
self.reloadDataWith(requestIdentifier, posts : [Post])
})
case 2:
self.postsController.fetchFuturePosts(activeRequestId, completion: { (requestIdentifier, posts) in
self.reloadDataWith(requestIdentifier, posts : [Post])
})
default:
fatalError("unexpected segment index")
}
}
func reloadDataWith(requestIdentifier : String,
posts : [Post]) {
if self.requestIdentifier == requestIdentifier {
self.posts = posts
self.tableView.reloadData()
}
}
}
The basic idea would be that as the segmented control changes, you would initiate a PFQuery that would populate your model, and then trigger the reloading of the table. For example, something like:
class ViewController: UIViewController, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
var posts: [Post]?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func didChangeValueForSegmentedControl(sender: UISegmentedControl) {
// first empty the table
posts = nil
tableView.reloadData()
// prepare new query
let query = PFQuery(className: "Posts")
switch sender.selectedSegmentIndex {
case 0:
query.whereKey("status", equalTo: "past")
case 1:
query.whereKey("status", equalTo: "current")
case 2:
query.whereKey("status", equalTo: "future")
default:
fatalError("unexpected segment index")
}
// now perform query
query.findObjectsInBackgroundWithBlock { objects, error in
guard error == nil else {
// report error
return
}
guard let searchResults = objects as? [Post] else {
// handle situation where results were not an array of `Post` objects
return
}
self.posts = searchResults
self.tableView.reloadData()
}
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return posts?.count ?? 0
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! PostCell
let post = posts![indexPath.row]
// configure `cell` using `post`
return cell
}
}
Now, those whereKey clauses are certainly not right, and that would change depending how your object model was set up, but this illustrates the basic idea. Initiate PFQuery on the basis of which segmented control was selected and then update the results accordingly.
Now, this all makes a lot of assumptions (that you've defined your table view and specified the view controller as its data source; that you've hooked up the outlet for the table view; that you've hooked up the IBAction for valueChanged on the segmented control; that you've defined a cell prototype with a custom cell type; etc.), but it illustrates the key parts of the solution.

swift PFQueryTableViewController - remove duplicate objects before presenting cells

I have two Classes in Parse, one contains details of images that users have liked ("Liked"), the second class lists users that are following other users ("Follows").
In my PFQueryViewController I am able to create a query in queryForTable() that does the following:
//get username of users being followed
let query:PFQuery = PFQuery(className:"Follows")
query.whereKey("fromUser", equalTo: (PFUser.currentUser()?.username)!)
//get images of all followed users
let imageQuery:PFQuery = PFQuery(className: "Liked")
imageQuery.whereKey("username", matchesKey: "toUser", inQuery: query)
//get images current user liked
let userImagesQuery:PFQuery = PFQuery(className: "Liked")
userImagesQuery.whereKey("username", equalTo: (PFUser.currentUser()?.username)!)
//combine liked images of following and current user
let combinedQuery:PFQuery = PFQuery.orQueryWithSubqueries([imageQuery,userImagesQuery])
if(objects?.count == 0)
{
combinedQuery.cachePolicy = PFCachePolicy.CacheThenNetwork
}
combinedQuery.orderByDescending("createdAt")
return combinedQuery
This works where I am able to display the liked images of the user, and the users being followed, in cellForRowAtIndexPath by querying the imageID of the object for each cell. However, multiple users can like the same image, so if a user and someone they are following like the same image, the image will appear twice in the table.
Is there a way to force the query to ignore objects when it finds a duplicate based on the ImageID key?
I am aware this can be done by executing the query and iterating through the objects, however this wont work in this scenario as I need the duplicates to be removed before the query is returned in queryForTable().
I managed to solve this by using objectsDidLoad(). This captures the objects before they are sent to the table, therefore I was able to iterate through each object and delete any duplicates.
I store all the loaded objects into a new array and remove an object if a duplicate is found. I then make the object in cellForRowAtIndexPath the corresponding object in the array.
override func objectsDidLoad(error: NSError?) {
super.objectsDidLoad(error)
displayedImages.removeAll()
imageObjects = NSMutableArray(array: objects!)
for item in imageObjects {
if displayedImages.count == 0 {
let image = item["imageid"] as! NSNumber
displayedImages.insert(image, atIndex: 0)
}else{
var count = 0
let image = item["imageid"] as! NSNumber
for imageToCheck in displayedImages {
if imageToCheck != image {
count++
}
}
if count != displayedImages.count {
imageObjects.removeObject(item)
}else{
displayedImages.insert(image, atIndex: 0)
}
}
}
self.tableView.reloadData()
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return imageObjects.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFTableViewCell? {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as! FeedTableViewCell
if let pfObject:PFObject = self.imageObjects.objectAtIndex(indexPath.row) as? PFObject {
//display images as before

Comments not Displayed in tableView

In the app I'm working on, it allows users to post on the main timeline and other users can comment on that user's post, so I've been trying to display the comments in the tableView, but it's not showing. I have already confirmed that the data is being posted to parse, so on that end it's working as expected, but when it comes to display the comments, I cannot seem to get it to work. I'm using this function to display the comments:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("commentCell", forIndexPath: indexPath) as! CommentTableViewCell
cell.commentLabel?.text = comments![indexPath.row]
return cell
}
is anything wrong with my code? or is there another way to display the comments?
where is the code to retrieve the comments? make sure you are calling "self.tableView.reloadData()" after the for loop.
the way I generally retrieve information from parse is like so:
func query() {
var query = PFQuery(className: "comments")
query.orderByDescending("createdAt")
query.findObjectsInBackgroundWithBlock { (caption2: [AnyObject]?, erreur: NSError?) -> Void in
if erreur == nil {
// good
for caption in caption2! {
self.comments.append(caption["<YOUR COLUMN NAME WHERE COMMENT IS STORED IN PARSE HERE>"] as! String)
}
self.tableView.reloadData()
}
else {
// not good
}
}
}
Add this function to your class. Then change this:
func reply() {
post?.addObject(commentView!.text, forKey: "comments")
post?.saveInBackground()
if let tmpText = commentView?.text {
comments?.append(tmpText)
}
commentView?.text = ""
println(comments?.count)
self.commentView?.resignFirstResponder()
self.commentTableView.reloadData()
}
to this:
func reply() {
post?.addObject(commentView!.text, forKey: "comments")
post?.saveInBackground()
if let tmpText = commentView?.text {
comments?.append(tmpText)
}
commentView?.text = ""
println(comments?.count)
self.commentView?.resignFirstResponder()
self.query
}
It turned out that I was missing:
UITableViewDataSource
in my class, so this fixed it:
class DetailViewContoller: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextViewDelegate {
...
...
...
override func viewDidLoad() {
super.viewDidLoad()
commentTableView.delegate = self
commentTableView.dataSource = self

Parse, iOS, includeKey query does not retrieve attribute of pointer object

I'm quite new to working with Parse and I'm building a todo list as part of a CRM. Each task in the table view shows the description, due date, and client name. The description and due date are in my Task class, as well as a pointer to the Deal class. Client is a string in the Deal class. I'm able to query the description and due date properly, but I am not able to retrieve the client attribute from within the Deal object by using includeKey. I followed the Parse documentation for includeKey.
The description and due date show up properly in the resulting table view, but not the client. The log shows client label: nil and the printed task details include <Deal: 0x7ff033d1ed40, objectId: HffKOiJrTq>, but nothing about the client attribute. How can I retrieve and assign the pointer object's attribute (client) to my label within the table view? My relevant code is below. Thank you in advance.
Edit: I've updated my code with func fetchClients() based on this SO answer, but I'm still not sure whether my function is complete or where to call it.
class TasksVC: UITableViewController {
var taskObjects:NSMutableArray! = NSMutableArray()
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
println("\(PFUser.currentUser())")
self.fetchAllObjects()
self.fetchClients()
}
func fetchAllObjects() {
var query:PFQuery = PFQuery(className: "Task")
query.whereKey("username", equalTo: PFUser.currentUser()!)
query.orderByAscending("dueDate")
query.addAscendingOrder("desc")
query.includeKey("deal")
query.findObjectsInBackgroundWithBlock { (tasks: [AnyObject]!, error:NSError!) -> Void in
if (error == nil) {
var temp:NSArray = tasks! as NSArray
self.taskObjects = temp.mutableCopy() as NSMutableArray
println(tasks)
self.tableView.reloadData()
} else {
println(error?.userInfo)
}
}
}
func fetchClients() {
var task:PFObject = PFObject(className: "Task")
var deal:PFObject = task["deal"] as PFObject
deal.fetchIfNeededInBackgroundWithBlock {
(deal: PFObject!, error: NSError!) -> Void in
let client = deal["client"] as NSString
}
}
//MARK: - Tasks table view
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.taskObjects.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier("TaskCell", forIndexPath: indexPath) as TaskCell
var dateFormatter:NSDateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "M/dd/yy"
var task:PFObject = self.taskObjects.objectAtIndex(indexPath.row) as PFObject
cell.desc_Lbl?.text = task["desc"] as? String
cell.date_Lbl.text = dateFormatter.stringFromDate(task["dueDate"] as NSDate)
cell.client_Lbl?.text = task["client"] as? String
var clientLabel = cell.client_Lbl?.text
println("client label: \(clientLabel)")
return cell
}
}
If the deal column is a pointer then includeKey("deal") will get that object and populate it's properties for you. There is no need to perform a fetch of any type on top of that.
You really should be using Optionals properly though:
if let deal = task["deal"] as? PFObject {
// deal column has data
if let client = deal["client"] as? String {
// client has data
cell.client_Lbl?.text = client
}
}
Alternatively you can replace the last if let with a line like this, which handles empty values and uses a default:
cell.client_Lbl?.text = (deal["client"] as? String) ?? ""
In your posted cellForRowAtIndexPath code you are trying to read client from the task instead of from the deal: task["client"] as? String.

Resources