I have a loadItem functions which is suppose to load items from a parse server. This include a loop where i'm at the end is saving the data to an array:
itemArray?.addObject(arrayDic)
When this is saved i would like to reloadData of the collectionView. therefor i've inserted it into a dispatch_async block, but it still seems like it is being run before the data is saved into the itemArray array since the itemArray.count is 0 and i've checked inside the loop that the data is saved into the array. What am i doing wrong?
func loadItems() {
var query = PFQuery(className:"Items")
query.orderByAscending("createdAt")
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]!, error: NSError!) -> Void in
if error == nil {
for object in objects {
var imageRelation: PFRelation = object.relationForKey("pictures")
var query = imageRelation.query()
query.orderByDescending("createdAt")
query.findObjectsInBackgroundWithBlock {
(imageObjects: [AnyObject]!, error: NSError!) -> Void in
if error == nil {
var theArray = imageObjects as NSArray
var arrayDic = NSMutableDictionary()
let imageFile = theArray.objectAtIndex(0).objectForKey("image") as PFFile
imageFile.getDataInBackgroundWithBlock {
(imageData: NSData!, error: NSError!) -> Void in
if !(error != nil) {
let image = UIImage(data:imageData)
arrayDic.setValue(object.objectForKey("price"), forKey: "price")
arrayDic.setValue(image, forKey: "image")
itemArray?.addObject(arrayDic)
}
}
}
}
}
dispatch_async(dispatch_get_main_queue()) {
println(itemArray?.count)
self.collectionView.reloadData()
}
} else {
// Log details of the failure
NSLog("Error: %# %#", error, error.userInfo!)
}
}
}
I would not advise trying to make the requests synchronous (as that can significantly slow down the process, unless you scheduled them to run concurrently on some concurrent background queue). I would suggest, instead, a pattern that allows you to keep the asynchronous requests, but notifies you of their completion. One such pattern is the dispatch group, in which you:
create a dispatch group:
let group = dispatch_group_create()
for every iteration of your loop, call dispatch_group_enter:
dispatch_group_enter(group)
win the completion block of the asynchronous method, call dispatch_group_leave:
dispatch_group_leave(group)
specify what you want to do when the dispatch group is complete:
dispatch_group_notify(group, dispatch_get_main_queue()) {
println(self.itemArray?.count)
self.collectionView.reloadData()
}
When all of the calls to dispatch_group_enter are offset by the final dispatch_group_leave call, the notification block will be called for you.
So, looking at the code as you loop through the objects, it might look something like:
let group = dispatch_group_create()
for object in objects {
dispatch_group_enter(group)
// do some stuff
query.findObjectsInBackgroundWithBlock {
(imageObjects: [PFObject]!, error: NSError!) -> Void in
if error == nil {
// do some more stuff
imageFile.getDataInBackgroundWithBlock {
(imageData: NSData!, error: NSError!) -> Void in
if !(error != nil) {
// do even some more stuff
}
dispatch_group_leave(group)
}
} else {
dispatch_group_leave(group)
}
}
}
dispatch_group_notify(group, dispatch_get_main_queue()) {
println(self.itemArray?.count)
self.collectionView.reloadData()
}
Their are other functionally equivalent patterns (e.g. custom concurrent dispatch queue with the final reloadData dispatched with a barrier; make these individual requests NSOperation objects that you add to NSOperationQueue and create separate completion operation that is dependent upon those other operations; etc.), but the dispatch group seems to entail the least refactoring of this code. Regardless, hopefully this illustrates the basic idea: Make the final reload only be triggered when the other asynchronous requests are done.
Related
Hey I'm working with Swift 2 and I'm trying to make a method that returns an array of strings of IDs downloaded from a database through a query. My problem is that within the function I cannot update my Array, meaning that I can access the downloaded information from the server but I cannot append it to my array for some reason. Or better, I can, but it doesn't really do anything. My array seems to stay empty.
func ATMsAroundMe(myLocation : PFGeoPoint) -> [String]{
var results = [String]()
let query = PFQuery(className: "ATMs")
query.whereKey("location", nearGeoPoint: myLocation, withinMiles: 5)
query.limit = 10
query.findObjectsInBackgroundWithBlock { (atms: [PFObject]?, error: NSError?) -> Void in
if (error == nil) {
for atm in atms! {
print(atm.objectId) //Works!
results.append(atm.objectId!) //Doesn't work
}
} else {
// Log details of the failure
}
}
print(results) //Prints "[]"
return results
}
So yeah if you have any suggestion or any idea on what am I doing wrong it'd be really helpful and appreciated if you could let me know.
Thanks.
The reason why results is not updated is because it is updated in another block scope. So updated values persists only in that block scope. To get the updated result you would need to use closures or __block in variable declaration in Objective-c.
which is quite nicely explained here here in BLOCKS VS CLOSURES
The issue here is that the call-
query.findObjectsInBackgroundWithBlock
This is an asynchronous call, and thus, your method simply returns as it does not wait for the results that will be returned by this asynchronous call. Thus you will need to think of an asynchronous API ATMsAroundMe in the form-
func ATMsAroundMe(myLocation : PFGeoPoint, completionHandler:(Bool,[String]?) ->Void){
let query = PFQuery(className: "ATMs")
query.whereKey("location", nearGeoPoint: myLocation, withinMiles: 5)
query.limit = 10
query.findObjectsInBackgroundWithBlock { (atms: [PFObject]?, error: NSError?) -> Void in
if (error == nil) {
for atm in atms! {
print(atm.objectId) //Works!
var results = [String]()
results.append(atm.objectId!)
completionHandler(true, results)
}
} else {
// Report the failure
completionHandler(false, nil)
}
}
}
You can now call this API like-
ATMsAroundMe(myLocation){(success :Bool, results:[String]?) in
if(success){
if let results = results {
//Process results
}
}
}
Synchronous solution:
func ATMsAroundMe(myLocation : PFGeoPoint) -> [String]{
var results = [String]()
let query = PFQuery(className: "ATMs")
query.whereKey("location", nearGeoPoint: myLocation, withinMiles: 5)
query.limit = 10
//Declare a semaphore to help us wait until the background task is completed.
let sem = dispatch_semaphore_create(0);
query.findObjectsInBackgroundWithBlock { (atms: [PFObject]?, error: NSError?) -> Void in
if (error == nil) {
for atm in atms! {
print(atm.objectId) //Works!
results.append(atm.objectId!)
dispatch_semaphore_signal(sem);
}
} else {
// Log details of the failure
dispatch_semaphore_signal(sem);
}
}
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
print(results) //Should print your results
return results
}
Note Be care full in calling this synchronous API from your main thread, it can potentially stall the main thread until the call returns like any other synchronous calls.
I'm writing a function to query the class Photos with a given Object Id in order to download a photo and set it to the UIImageView "background." I've narrowed the issue down to "for object in objects!" which I've commented in the code below. This seems like standard practice for casting, but the code won't run past this point. It compiles and no errors are thrown, but it fails to print anything past the "for" line, much less set the background.
// set new background image
func imageSet(objId: String) {
var query : PFQuery = PFQuery(className: "Photos")
query.whereKey("objId", equalTo:objId)
query.findObjectsInBackgroundWithBlock {
(objects:[AnyObject]?, error:NSError?) -> Void in
if error == nil {
println("First query")
// last working line
for object in objects! {
println("Won't print here")
// won't pass this point
let userImageFile = object["image"] as! PFFile
userImageFile.getDataInBackgroundWithBlock {
(imageData: NSData?, error:NSError?) -> Void in
if error == nil {
println("Or here")
self.background.image = UIImage(data:imageData!)
}
}
}
}
else {
println("\(error)")
}
}
}
Any tips are greatly appreciated! Thanks!
Under the class initialization I set:
var cardsCover = [PFFile]()
I have two functions. One of them take from Parse.com data and append it to arrays:
let cards = PFQuery(className: "cards")
cards.whereKey("category", equalTo: "Fruits")
cards.findObjectsInBackgroundWithBlock { (cards: [PFObject]?, error: NSError?) -> Void in
if error == nil {
for card in cards! {
self.cardsCover.append(card["cover"] as! PFFile)
}
} else {
print("error")
}
}
and the second function takes from this cardsCover array and show the items:
self.cardsCover[self.cardIndex].getDataInBackgroundWithBlock { (imageData: NSData?, error: NSError?) -> Void in
if imageData != nil {
let image = UIImage(data: imageData!)
let imageView = UIImageView(image: image!)
contentView.addSubview(imageView)
} else {
print(error)
}
}
but when I launch my app it at first try to show images from an empty array, instead of appending items to array and just later to read from it. So, it crashes.
I set breakpoint to the first line of my second func and when I type:
po cardsCover
it returns 0 elements.
How can I solve this problem and make at first, append items to my array and just later to read them from it?
First of all, while fetching elements from an Array, always put a safe check on array size to avoid the crash.
Second, you need to ensure that function to read data from cardsCover gets always called after data setter function is called. You have a completion block in data fetcher where in you can trigger the data fetcher. You can also implement delegate callbacks or post a notification once data download is done so data usage could start thereafter.
I think getDataInBackgroundWithBlock is asynchronous API (sounds like its running off the main queue) so you need to dispatch to the main queue. Here is how to do it
if self.cardsCover.count > self.cardIndex {
self.cardsCover[self.cardIndex].getDataInBackgroundWithBlock { (imageData: NSData?, error: NSError?) -> Void in
dispatch_async(dispatch_get_main_queue()) {
if imageData != nil {
if let image = UIImage(data: imageData!){
let imageView = UIImageView(image: image!)
contentView.addSubview(imageView)
} else { print ("imageData can not be converted to image") }
} else {
print("There was no imageData")
}
}
}
} else { print("cardIndex bigger than array count") }
Noob question here and I know my code below is very wrong but it works in that it retrieves the 3 images I need. However, I'd like to know a better way to retrieve multiple images from Parse.
Any help would be greatly appreciated!
func retrieveImage() {
var query = PFQuery(className: "Items")
query.orderByDescending("createdAt")
query.findObjectsInBackgroundWithBlock { (objects: [AnyObject]?, error: NSError?) -> Void in
if error == nil {
let imageObjects = objects as! [PFObject]
for (index, object) in enumerate(imageObjects) {
let thumbnail1 = object["image1"] as! PFFile
let thumbnail2 = object["image2"] as! PFFile
let thumbnail3 = object["image3"] as! PFFile
thumbnail1.getDataInBackgroundWithBlock{(imageData: NSData?, error: NSError?) -> Void in
if error == nil {
if let image = UIImage(data: imageData!) {
self.itemImages[index] = image
}
}
thumbnail2.getDataInBackgroundWithBlock{(imageData: NSData?, error: NSError?) -> Void in
if error == nil {
if let image = UIImage(data: imageData!) {
self.itemImages2[index] = image
}
}
}
thumbnail3.getDataInBackgroundWithBlock{(imageData: NSData?, error: NSError?) -> Void in
if error == nil {
if let image = UIImage(data: imageData!) {
self.itemImages3[index] = image
}
}
}
}
}
}
}
}
First the idea... we want to do an arbitrarily long list of asynch tasks, collect their results, and be notified on completion or error. We do this by parameterizing the task (in this case, the PFFiles whose contents are to be fetched are the parameters), and we use those parameters as a "to-do list".
A recursive function does the work, picking off the first item in the list, doing the asynch task, and then calling itself with the remainder of the list. An empty to-do list means we're done.
I've tried to translate the answer I referred to here into swift (literally learning the language line by line)....
func load(pfFiles: Array<PFFile>, var filling: Dictionary<PFFile, UIImage>, completion: (success: Bool) -> Void) {
completion(success: true)
var count = pfFiles.count
if (count == 0) {
return completion(success: true)
}
var file = pfFiles[0]
var remainder = Array(pfFiles[1..<count])
file.getDataInBackgroundWithBlock{(imageData: NSData?, error: NSError?) -> Void in
if error == nil {
if let image = UIImage(data: imageData!) {
filling[file.name] = image
self.load(remainder, filling: filling, completion: completion)
}
} else {
completion(success: false)
}
}
}
Given this is my first attempt, I'll be a little shocked and delighted if it works, but the algorithm is sound, and the swift compiles and appears to match the idea I outlined. Here's how to call it...
var pfFiles: Array<PFFile>
for (index, object) in enumerate(imageObjects) {
pfFiles.append(object["image1"])
pfFiles.append(object["image2"])
pfFiles.append(object["image3"])
}
var filling: Dictionary<String, UIImage>
// call the function here
// in the completion assign filling to property
// anytime after, when you have a PFFile like someObject["image2"]
// you use its name to look it up the UIImage in the results dictionary
Let me know if that last bit is clear enough. As you can see, I ran out of steam on my swift translation and resorted to pseudo code.
I believe you can just do self.itemImages[index] = thumbnail1.getData()!
If it crashs, do : query.includeKey("image1")
NOTE:
If you afraid to block the main queue, open a new thread to do such thing
I'm trying to reload the tableView when this loop is finished, but whenever i try to put the self.tableView.reloadData() outside the loop, the loop returns 0. I guess this is due to i'm getting the data in the background. What can i do in order to reload after the loop is completed.
func loadData() {
var query = PFQuery(className: "Items")
query.includeKey("user")
query.getObjectInBackgroundWithId(itemId) {
(itemObject: PFObject!, error: NSError!) -> Void in
if (error == nil) {
var userObject = itemObject.objectForKey("user") as PFObject
let userImageFile = userObject.objectForKey("file") as PFFile
userImageFile.getDataInBackgroundWithBlock {
(imageData: NSData!, error: NSError!) -> Void in
if error == nil {
let image = UIImage(data:imageData)
self.detailDic?.setObject(itemObject.objectForKey("title"), forKey: "title")
self.detailDic?.setObject(itemObject.objectForKey("description"), forKey: "desc")
self.detailDic?.setObject(itemObject.objectForKey("location"), forKey: "point")
self.detailDic?.setObject(userObject.objectForKey("name"), forKey: "name")
self.detailDic?.setObject(userObject.objectForKey("gender"), forKey: "gender")
self.detailDic?.setObject(image, forKey: "image")
var relation = itemObject.relationForKey("file") as PFRelation
var imageQuery = relation.query() as PFQuery
imageQuery.findObjectsInBackgroundWithBlock { (imageObj: [AnyObject]!, error1: NSError!) -> Void in
if error1 == nil {
var imageDic:NSMutableArray = NSMutableArray()
let group = dispatch_group_create()
for obj in imageObj {
var imageObject = obj as PFObject
var thumbnail = imageObject.objectForKey("file") as PFFile
// Important: enter the group *before* starting the background task.
dispatch_group_enter(group)
thumbnail.getDataInBackgroundWithBlock {
(imageData: NSData!, error: NSError!) -> Void in
if error == nil {
let theImage = UIImage(data:imageData)
theImage.CGImage
self.imageArray!.addObject(theImage)
dispatch_group_leave(group)
}
}
}
dispatch_group_notify(group, dispatch_get_main_queue()) {
self.tableView?.reloadData()
}
}
}
}
}
}
}
}
Use a dispatch group. A dispatch group is basically a counter. You can increment and decrement it atomically, and you can ask to be notified when it is zero.
Each time you add a background task, “enter” the group (which increments its counter). When a background task finishes, “exit” the group (which decrements its counter). Use dispatch_group_notify to run a block when the counter is zero (meaning all background tasks have finished).
let group = dispatch_group_create()
for obj in imageObj {
var imageObject = obj as PFObject
var thumbnail = imageObject.objectForKey("file") as PFFile
// Important: enter the group *before* starting the background task.
dispatch_group_enter(group)
thumbnail.getDataInBackgroundWithBlock {
(imageData: NSData!, error: NSError!) -> Void in
if error == nil {
let theImage = UIImage(data:imageData)
self.imageArray!.addObject(theImage)
dispatch_group_leave(group)
}
}
}
dispatch_group_notify(group, dispatch_get_main_queue()) {
self.tableView?.reloadData()
}
Note that if there are no elements in imageObj, or if all of the background tasks finish before dispatch_group_notify runs, dispatch_group_notify will run the block immediately (which is what you want).
Also, UIImage doesn't necessarily decode the image data when you create it. It can do so lazily. You may want to force it to decode the image data on the background thread. You can do that by asking for its CGImage:
if error == nil {
let theImage = UIImage(data:imageData)
theImage.CGImage // force decoding immediately
self.imageArray!.addObject(theImage)
dispatch_group_leave(group)
}
You could also do:
for (index,obj) in enumerate(imageObj) {
.......
// reload after last loop
if(index == imageObj.count) {
self.tableView?.reloadData()
}
}