I need to put data in the array using a query, but it only works in the block. I searched here and found out that it's because the findObjectsInBackgroundWithBlock is asynchronous, but how can I make it synchronous then?
var cities = [String]()
func loadCityArray() {
let citiesVisited = PFQuery(className: "Trips")
citiesVisited.whereKey("userId", equalTo: (PFUser.currentUser()?.objectId)!)
citiesVisited.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if let objects = objects {
for object in objects {
let city = object["cityId"] as! String
let query = PFQuery(className: "Cities")
query.whereKey("objectId", equalTo: city)
query.findObjectsInBackgroundWithBlock({ (objects, error) -> Void in
if let objects = objects {
for object in objects {
self.cities.append(object["cityName"] as! String)
}
}
})
}
}
You do not want to make that synchonous on the main thread. That is strongly discouraged.
You rather want to store the things you need from the request in instance variables and tell the corresponding View Controller new values are present in the block.
edit:
Suppose you have an object waiting to use the data:
var chart : DataConsumer?
In the block where you get the data,
chart.useData(data)
edit 2:
the useData function should keep track of changes in the data set and make use of the information that new data arrived. For example, by displaying it.
Related
I have a dynamic array (named "items") of users I follow:
["user5#t.co", " user6#t.co"]
and I'm essentially retrieving these user's posts using:
for i in 0..< items.count{
db.collection("users").document("\(items[i])").collection("posts").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.posts = documents.map { QueryDocumentSnapshot -> Post in
let longitudeVar = data["longitude"] as? String ?? ""
let latitudeVar = data["latitude"] as? String ?? ""
return Post(id: .init(), longitudeVAR: longitudeVAR, latitudeVAR: latitudeVAR)
}
}
}
I'm trying to draw information from both users at the same time but the issue I'm having is that this only draws post information (longitudeVar & latitudeVar) for ONE user OR the other- and it seems to randomly pick between user5#t.co and user6#t.co. Any suggestions? Also I apologize if this is a basic question or if my code isn't well written- I'm just trying to get this to work. Thanks!
If you want to loop database fetches and coordinate their returns into a single task then you should use a DispatchGroup. It's a very simple API that will simply record how many calls you make to Firestore and give you a completion handler to execute when that many returns eventually come in (at the end of the last one).
Don't use a snapshot listener here because those are for streams of data—all you want is a one-time document grab. If there are lots of documents to parse and they are relatively big then I would consider using a background queue so the UI doesn't stutter when this is going on.
var posts = [Post]()
let dispatch = DispatchGroup() // instantiate dispatch group outside loop
for item in items {
dispatch.enter() // enter on each iteration
// make a get-documents request, don't add a continuously-updating snapshot listener
db.collection("users").document(item).collection("posts").getDocuments { (querySnapshot, error) in
if let documents = querySnapshot?.documents {
// fill a local array, don't overwrite the target array with each user
let userPosts = documents.map { QueryDocumentSnapshot -> Post in
let longitudeVar = data["longitude"] as? String ?? ""
let latitudeVar = data["latitude"] as? String ?? ""
return Post(id: .init(), longitudeVAR: longitudeVAR, latitudeVAR: latitudeVAR)
}
self.posts.append(userPosts) // append local array to target array
} else if let error = error {
print(error)
}
dispatch.leave() // always leave, no matter what happened inside the iteration
}
}
/* this is the completion handler of the dispatch group
that is called when all of the enters have equaled all
of the leaves */
dispatch.notify(queue: .main) {
self.tableView.reloadData() // or whatever
}
I have some user comments stored in a database (parse-server) that I would like to would like to display on my viewController's viewDidLoad(). I can easily pull the comment objects as follows:
super.viewDidLoad()
func query(){
let commentsQuery = PFQuery(className: "Comments")
commentsQuery.whereKey("objectId", equalTo: detailDisclosureKey)
commentsQuery.findObjectsInBackground { (objectss, error) in
if let objects = objectss{
if objects.count == 1{
for object in objects{
self.unOrderedComments.append(object)
}
}
}
}
}
This query dumps all of the of the comments in the unOrederedComments array. Each comment is added to the database with a createdAt property automatically being added relating the exact time of its creation. This property is a string with (as an example) the form: "2017-08-13T19:31:47.776Z" (the Z at the end is at the end of every string... not exactly sure why its there but its constant). Now, each new comment is added in order to the top of database and thus any queried result should be in order regardless. However, I would like to make sure of this by reordering it if necessary. My general thought process is to use .sorted, but I cannot figure out how to apply this to my situation
func orderComments(unOrderComments: [PFObject]) -> [PFObject]{
let orderedEventComments = unOrderedEventComments.sorted(by: { (<#PFObject#>, <#PFObject#>) -> Bool in
//code
})
}
This is the generic set up but I cannot, despite looking up several examples online figure out what to put in the <#PFObject#>'s and in the //code. I want to order them based on the "createdAt" property but this is not achieved via dot notation and instead requires PFObject["createdAt"] and using this notation keeps leading to error. I feel as so though I may need to set up a custom predicate but I do not know how to do this.
I was in the same situation, what I did was to first create an array of structs with the data I downloaded where I turned the string createdAt into a Date, then used this function:
dataArrayOrdered = unOrderedArray.sorted(by: { $0.date.compare($1.date) == .orderedAscending})
(.date being the stored Date inside my array of strcuts)
Try this code, notice that I assumed you have a variable name called ["Comments"] inside your Parse database, so replace if necessary. Also, I realised that createdAt it's in Date format, so there was no need to change it from String to Date, chek if it works the same for you, if it doesn't refer to this: Swift convert string to date.
struct Comment {
var date = Date()
var comment = String()
}
var unOrderedComments: [Comment] = []
var orderedComments = [Comment]()
override func viewDidLoad() {
super.viewDidLoad()
query()
}
func query(){
let commentsQuery = PFQuery(className: "Comments")
commentsQuery.findObjectsInBackground { (objectss, error) in
if let objects = objectss{
if objects.count >= 1{
for object in objects{
let newElement = Comment(date: object.createdAt!, comment: object["Comments"] as! String)
self.unOrderedComments.append(newElement)
print(self.unOrderedComments)
}
}
self.orderedComments = self.unOrderedComments.sorted(by: { $0.date.compare($1.date) == .orderedAscending})
print(self.orderedComments)
}
}
}
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 6 years ago.
Improve this question
I'm looking to update an array called 'events' from an api.
Any ideas? Thanks in advance!
var events = [AnyObject]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let url : String = "http://api/tickets"
let request : NSMutableURLRequest = NSMutableURLRequest()
request.URL = NSURL(string: url)
request.HTTPMethod = "GET"
NSURLSession.sharedSession().dataTaskWithRequest(request) {
(data : NSData?, response : NSURLResponse?, error : NSError?) in
dispatch_async(dispatch_get_main_queue()) {
// Main thread
self.tableView.reloadData()
}
}
}
So you've got your data back, now you need to convert the data into an object that you can work with. Your event array is an array of AnyObject, which can be cumbersome to work with. Instead, I would recommend that you create a model for these objects.
Since I don't know what your data looks like I'll just make up a model, you'll need to edit this to suit your needs:
struct Ticket {
let id : Int
let description : String
// pass your data object directly into your initializer.
init?(data: [String:AnyObject]) {
guard let itemId = data["id"] as? Int else { return }
guard let itemDesc = data["description"] as? String else { return }
id = itemId
description = itemDesc
}
}
// note: this initializer will fail (by design) if you pass in an
// object that doesn't have the proper requirements. You can exchange the
// guards for if-lets to avoid this behavior
Now, rather than using an array of AnyObject, you work specifically with your modeled item:
// use your model for easy data access
var events = [Ticket]()
Next you update your NSURLSession's closure to create and append your new model object to your array:
NSURLSession.sharedSession().dataTaskWithRequest(request) {
(data : NSData?, response : NSURLResponse?, error : NSError?) in
// make sure you have data
guard let returnedData = data else {
print("no data was returned")
return
}
do {
// convert your object to JSON data... the following code
// may differ depending on how your JSON is formed. However,
// the concept is still the same
// get array of dictionaries
let jsonObject = try NSJSONSerialization.JSONObjectWithData(returnedData, options: .MutableLeaves) as! [[String: AnyObject]]
// loop over your array and create Ticket objects
jsonObject.forEach { item in
var ticket = Ticket(item)
// append your tickets to the array
events.append(ticket)
}
} catch let error {
print(error)
}
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
Let's say, that what is returned to you is Array of tickets and only array of tickets. If you want to get this array, as [AnyObject] you could write:
if let arrayOfAnyObjects = data as? [AnyObject] {
events = arrayOfAnyObjects
}
in this way, you will try to cast data from type NSData to type [AnyObject] if possible. And if it succeed, you will assign it.
You have the right idea. What part don't you understand?
How you parse the response from the server depends on the format of the data. Is it JSON? XML? Some other format?
EDIT:
Ok, you said the data from the server is in JSON format. So inside the completion block of your data request, use NSJSONSerialization to convert your data to a collection object. It looks like Dan gave a pretty complete explanation on how to do that while I was away from my computer.
I am trying to do two queries to Parse in my PFQueryTableViewController. I am trying to do one query to obtain all of the objects relating to the currentUser and then place those objects into an array. After the array is filled with its objects, I would like to do another query from
override func queryForTable() -> PFQuery { }.
This second query will use the objects from the array that was created from the first query, in order to retrieve objects of its own.
I don't know how to make the first query execute before the second. As of right now, the second query will execute before the first one before it has a chance to fill the array and as a result messes up the query for the second one. The following is what i have for my code at this point.
class PFNewsFeedTableViewController: PFQueryTableViewController {
var leadersArray : NSMutableArray = NSMutableArray()
override func viewDidAppear(animated: Bool) {
leadersArray.removeAllObjects()
var findLeaders = PFQuery(className: "Follow")
findLeaders.whereKey("follower", equalTo: PFUser.currentUser()!)
findLeaders.orderByDescending("createdAt")
findLeaders.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]?, error: NSError?) -> Void in
//If no error
if error == nil {
println("Successfully retrieved \(objects!.count) leaders.")
// Do something with the found objects
if let objects = objects as? [PFObject] {
for object in objects {
self.leadersArray.addObject(object.objectForKey("leader")!)
}
}
}
self.tableView.reloadData()
self.queryForTable()
}
}
override func queryForTable() -> PFQuery {
var findContent = PFQuery(className: "Content")
findContent.whereKey("user", containedIn: self.leadersArray as [AnyObject])
findContent.orderByDescending("createdAt")
return findContent
}
}
Is there a way to determine the order in which these queries get executed? Any help to this problem would be appreciated.
Yes, you can.
Let me first start by saying that it does sound like you should try to maybe rethink your data model, as executing two queries in succession like this is often a sign of focusing too much on traditional data modeling rather than the more pragmatic nosql-style of modeling where you (especially for mobile apps) should model for queries rather than normalisation.
If you were to solve your case like you said, you can employ the Bolts framework. No need to install this, as this is already installed as part of Parse (which uses Bolts behind the scenes).
It could then look something like this (just a quick write-up. Will probably not work out of the box, but you should be able to make it fit):
var findLeaders = PFQuery(className: "Follow")
findLeaders.whereKey("follower", equalTo: PFUser.currentUser()!)
findLeaders.orderByDescending("createdAt")
findLeaders.findObjectsInBackground().continueWithBlock {
(task: BFTask!) -> AnyObject! in
//If no error
if task.error != nil {
print("Something went wrong...")
} else {
// Do something with the found objects
if let objects = task.result as? [PFObject] {
print("Successfully retrieved \(objects.count) leaders.")
for object in objects {
self.leadersArray.addObject(object.objectForKey("leader")!)
}
}
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
self.queryForTable()
return nil
}
I figured out the problem and it looks like this
override func queryForTable() -> PFQuery {
var findLeaders = PFQuery(className: "Follow")
findLeaders.whereKey("follower", equalTo: PFUser.currentUser()!)
findLeaders.orderByDescending("createdAt")
var findContent = PFQuery(className: "Content")
findContent.whereKey("user", matchesKey: "leader", inQuery: findLeaders)
findContent.orderByDescending("createdAt")
return findContent
}
Currently trying to save objects to parse database, when i println the objects they contain the new array that i wanted inside the array field but for some reason it won't save to table been on this a while any ideas?
The array needs to add a new integer value for each one corresponding to each the queries,the "test" array archives this but saving each object to the USER class seems to be a problem
func taskAllocation(){
// var query = PFQuery(className: "Tasks")
var query = PFUser.query()
// let user = PFUser()
var posts:[PFUser] = []
query.whereKey("Year", equalTo: yearTextField.text.toInt())
query.whereKey("Class", equalTo: classTextField.text.toInt())
query.whereKey("Keystage", equalTo: keystageTextField.text.toInt())
// query.limit = 10
query.findObjectsInBackgroundWithBlock { (objects: [AnyObject]!, error: NSError!) -> Void in
if error != nil {
println(error)
} else {
if objects.isEmpty {
println("empty query")
}
else {
for object in objects as [PFUser] {
var test:[Int] = object["taskIDs"] as [Int]
test.append(self.getIndex)
println(test)
object.setValue(test, forKey: "taskIDs")
object["tasksCorrect"] = "hello"
posts.append(object)
}
println(posts)
PFUser.saveAllInBackground(posts)
}}}
I assume you want to save these components to your "Tasks" class and not your User class. PFUser Query is just for the "_User" class which does not have the tasks in it.
var tasks:PFObject = PFObject(className: "Tasks")
// Make sure in your parse data table you have the columns created as type String or Int
item["Year"] = yearTextField.text.toInt()
item["Keystage"] = keystageTextField.text.toInt()
item["Class"] = classTextField.text.toInt())
// Save the task to a user, make sure you create a Username column in your data table
item["Username"] = PFUser.currentUser().username
// save it
item.saveInBackgroundWithTarget(nil , selector: nil)
Your code is a little clunky so I made assumptions on what your trying to do. It might be beneficial taking a screen shot of your task class.