I have a Tableview that gets data with findObjectsInBackgroundWithBlock in viewDidLoad and passes that data to a Detail View Controller no problem.
Im having trouble managing the flow of findObjectsInBackgroundWithBlock. Here is a example: I have a like button on the detail view and when pressed it increments the UILabel and displays it. It also then gets the object in Parse then increments and saves it... Everything good.
#IBAction func likeButtonPressed(sender: AnyObject) {
print("likeButtonPressed()")
// Adding the like to label
mixLike!++
var stringForCount: String = String(mixLike!)
mixLikeLabel.text = stringForCount
// Saving the like back to Parse
var query = PFQuery(className: "musicMixes")
query.whereKey("info", equalTo: mixNameLabel.text)
query.findObjectsInBackgroundWithBlock { (objects:[AnyObject]!, error:NSError!) -> Void in
if error == nil {
for object in objects {
//var votes = object["votes"] as! Int
let mixObject:PFObject = object as! PFObject
mixObject.incrementKey("votes", byAmount: 1)
mixObject.saveInBackgroundWithTarget(nil, selector: nil)
print("mixObjectSaved")
}
} else {
print("Error getLikeCount()")
}
print("sending Notification...")
NSNotificationCenter.defaultCenter().postNotificationName("reload", object: nil)
print("sent Notification...")
}
} // likeButtonPressed End
I also then call a NSNotification back to the Table View so the Table View can update the likes to match the users like click on the detail view (See bellow)
The NSNotification calls this function in the Table View, which removes the like array, grabs the new likes again and then reloads the Table View.
# objc func reloadTableData(notification: NSNotification){
print("Notification Recived, Removing Likes and Reloading. reloadTableData()...")
self.mixLikeArray.removeAll()
//self.stringForCountArray.removeAll()
print("Like array Data removed, getting data again...")
var query = PFQuery(className: "musicMixes")
query.orderByAscending("date")
query.findObjectsInBackgroundWithBlock { (objects:[AnyObject]!,error: NSError!) -> Void in
if error == nil {
for object in objects {
let mixLike = object["votes"] as! Int
self.mixLikeArray.append(mixLike)
print("New mixLikeArray data is \(self.mixLikeArray)")
}
} else {
print("error getting like object")
}
}
dispatch_async(dispatch_get_main_queue(),{
self.allTableView.reloadData()
});
}
I see three issues wrong with how this works at the moment. likeButtonPressed() Is sometimes sending the NSNotification before mixObject.saveInBackgroundWithTarget is finished. Meaning that the incremented like won't be displayed on the table view.
Secondly if I was to click like then click back to tableview swiftly the app will crash. This is because I'm guessing both likeButtonPressed() and the NSNotification function still has not been completed.
Also in # objc func reloadTableData(notification: NSNotification) once again the
dispatch_async(dispatch_get_main_queue(),{
self.allTableView.reloadData()
});
Is being called before the findObjectsInBackgroundWithBlock is being completed? Anyway round this?
How would you suggest I can remodel this to work efficiently? Im pretty new to coding and a bit rusty with designing the best ways to do things... I know the concept behind completion handlers could I use these? I know that Parse likes to work in the background though hhhmmmm.....
to fix your reloadTableData problem, you should trigger the reload once the parse block is done executing, which means moving this line
dispatch_async(dispatch_get_main_queue(),{
self.allTableView.reloadData()
});
inside the block
query.findObjectsInBackgroundWithBlock { (objects:[AnyObject]!,error: NSError!) -> Void in
if error == nil {
for object in objects {
let mixLike = object["votes"] as! Int
self.mixLikeArray.append(mixLike)
print("New mixLikeArray data is \(self.mixLikeArray)")
}
dispatch_async(dispatch_get_main_queue(),{
self.allTableView.reloadData()
});
} else {
print("error getting like object")
}
}
That will ensure that it gets triggered once parse is done updating objects. Currently its triggering before that while the block is executing. It also means that it won't reload if you get an error as you probably need to handle that differently anyway.
As for your problem of the notification happening before the saving is complete, you are calling . saveInBackgroundWithTarget but don't seem to send anything into it. You could use saveInBackgroundWithBlock and then use dispatch_group dispatch_group_enter, dispatch_group_leave, and dispatch_group_notify inside the block to make your program wait till everything is done being saved before sending the notification.
So you would create a dispatch_group
dispatch_group_t group = dispatch_group_create();
And then call it dispatch_group_enter in the for loop through the objects
for object in objects {
dispatch_group_enter(group);
let mixObject:PFObject = object as! PFObject
.....
}
Then call dispatch_group_leave on the mixObject.saveInBackgroundWithBlock
and wrap the notification in dispatch_group_notify
dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 4
NSNotificationCenter.defaultCenter().postNotificationName("reload", object: nil)
});
Something like that
It sounds more daunting than it is, here's a Ray Wenderlich tutorial to bring you up to speed on how to use it, if your not familiar
Related
I am trying to synchronously retrieve a PFUser object information. However, I've tried using fetch, but I keep on getting implementation errors. I would like to find for example: PFUser.currentUser()?.objectForKey("scoreNumber"), but I am not getting the updated value. I was told I would find it using fetch, but I am unable to implement objectForKey in my fetch function.
Some of the attempts
var score: String!
func retrieveScore(){
guard let myUser = PFUser.currentUser() else { return }
do{ let data = try myUser.objectForKey("scoreNumber")?.fetch()
try scoreNumber = data! as! String
}catch{}
}
Another one
let userScore = PFUser.currentUser()?.objectForKey("scoreNumber")
let userScore2 = userScore.fetch()
You shouldn't save the user for NSUserDefaults. Parse SDK does a good job of managing your sessions for you. Just have a specific log out button that calls the [PFUser logout]` function.
So, fetch is an asynchronous operation. This means that it runs on a separate thread, and then the main thread will continue executing the next commands. So, you're calling fetch, which fetches an object in the background, but then you are trying to access a value from the object that hasn't been fetched yet. You need to access the object from within the block handling the results of the fetch call.
When you call fetch the way you did, just with .fetch(), it runs in the background but doesn't alert you when you have the data, nor if the fetch failed. This is not a good way to get data you're going to need immediately.
You need to use fetchInBackgroundWithBlock()
scoreNumber.fetchInBackgroundWithBlock() {
(scoreNumber: PFObject?, error: NSError?) -> Void in
if error == nil && scoreNumber != nil {
print(scoreNumber)
} else {
print(error)
}
}
class func loadData(
onCompletition: #escaping ([LocationInfo])->Void){
let workingQueue = DispatchQueue.global(qos:.utility)
let completitionQueue = DispatchQueue.main
workingQueue.sync {
print("\n Data fetch started \n")
let root = FIRDatabase.database().reference()
let locationSummary = root.child("LocSummary")
locationSummary.observeSingleEvent(of: .value,with: { (snapshot) in
for item in snapshot.children{
let locationInfo = LocationInfo(snapshot: item as! FIRDataSnapshot)
FirebaseDataController.resultsArray.append(locationInfo)
}
completitionQueue.async {
print("\n data fetch completed \n ")
onCompletition(FirebaseDataController.resultsArray)
print("After on completion method")
}
})
}
}
The problem I have no is that, every time I want to access the data inside the results array I have to go through this functions completion handler. Which is not something I can do all the time specially when I want to work with table views and such (I have a seperate class to handle all DB interactions and many other classes to handle table view interactions).
My objective is to run this code at the start of the application may be through the AppDelegate and have a populated array that I can call anytime I want access to data.
To do this I think I need to run this code on the main thread. I tried that by substituting the workingQueue with the main thread but the application keeps crashing.
Is there is anything that I can do about this?
Your application will crash only if you call this synchronously on main thread when launching application because you cause a deadlock. If you run this function in application:didFinishLaunching: method of AppDelegate you should be able to use safely this implementation:
class func loadData(onCompletition: #escaping ([LocationInfo])->Void){
let completitionQueue = DispatchQueue.main
print("\n Data fetch started \n")
let root = FIRDatabase.database().reference()
let locationSummary = root.child("LocSummary")
locationSummary.observeSingleEvent(of: .value,with: { (snapshot) in
for item in snapshot.children{
let locationInfo = LocationInfo(snapshot: item as! FIRDataSnapshot)
FirebaseDataController.resultsArray.append(locationInfo)
}
completitionQueue.async {
print("\n data fetch completed \n ")
onCompletition(FirebaseDataController.resultsArray)
print("After on completion method")
}
})
}
Your guess that you need to perform this operations on the main thread is wrong - there is no reason to do that, in fact, you will end up with stalled user interface. Fetch the data, and then asynchronously update user interface on main thread.
Since you want to call the method on main thread.
First thing you remove all the code related to DispatchQueue.
Then call the loadData using - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
These are the methods and processes which need to happen:
Download images from Parse
Download associated data (which matches images)
Plot this data on a map
Here is my code from view did load:
override func viewDidLoad() {
imageDownload { () -> () in
print("5-----inside closure")
self.queryParseandSave(callback: self.plotImages)
}
}
Image download function:
func imageDownload(completed: #escaping FinishedDownload){
print("1-----Started Image download")
// Query for places
let query = PFQuery(className:"ViewFinderObjects")
query.whereKey("ImageVerified", equalTo: true)
query.whereKey("coordinates", nearGeoPoint:myGeoPoint)
query.limit = 10
query.findObjectsInBackground { (objects, error) -> Void in
if (error == nil) {
for object in objects! {
print("2-----inside object for block")
let imageFromParse = object["image"] as! PFFile
imageFromParse.getDataInBackground(block: {(imageData, error) -> Void in
print("Searching for Image")
if error == nil {
let obsImage:UIImage = UIImage(data: imageData!)!
self.imageToShow = obsImage
self.closestImage.append(self.imageToShow!)
print("There are \(self.closestImage.count) images in the image array")
}
})
print("3-----Completed object loop")
}
}
print("4-----Calling completed statement")
completed()
}
}
Which then calls another function queryParseandSave(callback: self.plotImages)
with the self.plotImages plotting the images on a map.
I have 1 huge issue:
self.plotImahes is always called before the images have finished downloading
I have researched async_dispatch but have no idea if this is the right thing to do.
I'm not familiar with the implementations of the query.findObjectsInBackground and imageFromParse.getDataInBackground methods, but their naming implies that they both happen asynchronously. Also judging from what you're provided above, the former retrieves the object data, while the latter does the actual image data download. If that is indeed the case, then it looks like you're calling your completion handler inside the body of the first asynchronous method instead of waiting for the second method (what appears to be the actual image download).
A couple of ideas for how to resolve this:
You could move your completion handler into the imageFromParse.getDataInBackground block, but this would only make sense if you're comfortable calling the completion block multiple times, after each image finishes downloading.
You could create your own dispatch or operation queue and wait until all tasks complete, then call the completion handler.
You could set up an observer or notification pattern that will call your completion handler at the appropriate time.
There are many different ways to address the issue, but the key thing to remember is that it sounds like you want to call your completion handler after all of the asynchronous operations have completed. Right now you're calling it after you've retrieved the objects, but all of your images are still downloading in the background when your completion handler is called.
I'm using a button to populate a UIPickerView on a hidden UIVisualEffectView. The user clicks the button, the VisualEffectView blurs everything else, and the PickerView displays all the names in their contact list (I'm using SwiftAddressBook to do this.)
This works fine except when the user clicks the button, the UI locks up for about 5-10 seconds. I can't find any evidence of heavy CPU or memory usage. If I just print the sorted array to the console, it happens almost immediately. So something about showing the window is causing this bug.
#IBAction func getBffContacts(sender: AnyObject) {
swiftAddressBook?.requestAccessWithCompletion({ (success, error) -> Void in
if success {
if let people = swiftAddressBook?.allPeople {
self.pickerDataSource = [String]()
for person in people {
if (person.firstName != nil && person.lastName != nil) {
//println("\(person.firstName!) \(person.lastName!)")
self.pickerDataSource.append(person.firstName!)
}
}
//println(self.pickerDataSource)
println("done")
self.sortedNames = self.pickerDataSource.sorted { $0.localizedCaseInsensitiveCompare($1) == NSComparisonResult.OrderedAscending }
self.pickerView.reloadAllComponents()
self.blurView.hidden = false
}
}
else {
//no success, access denied. Optionally evaluate error
}
})
}
You have a threading issue. Read. The. Docs!
requestAccessWithCompletion is merely a wrapper for ABAddressBookRequestAccessWithCompletion. And what do we find there?
The completion handler is called on an arbitrary queue
So your code is running in the background. And you must never, never, never attempt to interact with the user interface on a background thread. All of your code is wrong. You need to step out to the main thread immediately at the start of the completion handler. If you don't, disaster awaits.
I have a UITableViewController and have to load a not so big amount of data on start. In my viewDidLoad, I use a different queue to send the request:
override func viewDidLoad() {
super.viewDidLoad()
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
var data = self.getStoresData()
dispatch_async(dispatch_get_main_queue(), {
self.parseStoresData(data)
self.resultsController = PartnerStore.getAllStores()
});
});
}
And these are the methods:
func getStoresData() -> [NSDictionary]
{
var responseData = [NSDictionary]()
self.refreshControl.beginRefreshing()
AppDelegate.appDelegate().httpRequestOperationManager?.GET(
"partner_stores/",
parameters: nil,
success: { (operation: AFHTTPRequestOperation!, responseObject: AnyObject!) in
self.tableView.reloadData();
println("RESPONSE OBJECT IN GET PARTNER STORES: \(responseObject)") },
failure: { (operation: AFHTTPRequestOperation!, error: NSError!) in
println("FAIL IN GET PARTNER STORES: \(error)") })
self.refreshControl.endRefreshing()
return responseData
}
func parseStoresData(storesData: [NSDictionary])
{
for storeDict in storesData {
// just inserts a new object to CoreData
PartnerStore.addNewStore(storeDict)
}
}
The problem is (I think) that the API call is async, so the two functions in dispatch_async are executed before the data is downloaded from the server. But if I put everything in the success block of the GET call, it takes a lot of time and the whole UI is blocked. What is the best way to to the server call, without blocking the UI thread ?
You are right about the two calls in the dispatch_async in viewDidLoad being called before your GET request completes; that's a problem. You also had the right idea when you said you tried putting everything in the success block; that's where it should go flow wise. There are a few other things that need to be moved around as well though.
One good way to go about handling UI updates is to have separate functions for your UI updates and your data fetch. Doing it that way means we'll need to pass a callback to your getStoresData function and then call it appropriately in your GET function's success and error blocks. That will let us know when the data fetch is complete so that we can finish the UI updates. We'll also want to move the dispatch to the background queue out of viewDidLoad and in to getStoresData.
So, lets pull any UI changes out of getStoresData and move that dispatch:
func getStoresData(callback: (error: NSError?) -> Void) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
// ... do any setup ...
AppDelegate.appDelegate().httpRequestOperationManager?.GET(
// ... other GET parameters ...
success: { (operation: AFHTTPRequestOperation!, responseObject: AnyObject!) in
var responseData = [NSDictionary]()
// do what you need to convert responseObject to responseData
// then...
// NOTE: we'll dispatch the the main thread here because parseStoresData deals with CoreData.
// This dispatch could be done in parseStoresData itself but
// a callback function would need to be added to it as well
// in that case.
dispatch_async(dispatch_get_main_queue(), {
self.parseStoresData(responseData)
// The response has been dealt with, so call the callback
callback(error: nil)
});
},
failure: { (operation: AFHTTPRequestOperation!, error: NSError!) in
// There was an error, so call the callback with the error object
callback(error: error)
}
)
})
}
Now let's make that new function to handle updating the UI so that the data updates are decoupled from the UI updates. In this function we'll first start the refresh control and call getStoresData. Then, when getStoresData finishes, update the table view and stop the refresh control.
func reloadData() {
// start the refresh control on the main thread so the user knows we're updating
dispatch_async(dispatch_get_main_queue(), {
self.refreshControl.beginRefreshing()
})
// do the actual data fetch...
// (remember this will dispatch to a background thread on its own now)
getStoresData({
(error: NSError?) -> Void in
// since this callback could be called from any thread,
// make sure to dispatch back to the main UI thread to finish the UI updates
dispatch_async(dispatch_get_main_queue(), {
if let actualError = error {
// update the UI appropriately for the error
} else {
// update the data in the table view and reload it
self.resultsController = PartnerStore.getAllStores()
self.tableView.reloadData();
}
// we're done; stop the refresh control
self.refreshControl.endRefreshing()
})
})
}
That makes your viewDidLoad function very simple now:
override func viewDidLoad() {
super.viewDidLoad()
reloadData()
}
This also makes it easier to implement things like pull to refresh since you can simply call reloadData when the user triggers the refresh instead of duplicating the UI update code all over the place.