I am currently developing a inventory app. My goal is to retrieve objects from Parse and then saving onto the local datastore. Querying objects from Parse and saving them works (because of the console message) but querying later on from the local datastore, does not retrieve anything! Here's my code:
let query = PFQuery(className: "Publication")
query.limit = 150
query.selectKeys(["publication_id","publication_Type","publication_Name"])
dispatch_async(dispatch_get_main_queue()) { () -> Void in
query.findObjectsInBackgroundWithBlock({ (pubObject, error) -> Void in
if error == nil {
print("Succesfully retrieved \(pubObject!.count)")
PFObject.saveAllInBackground(pubObject, block: { (success, error) -> Void in
print("Saved \(pubObject!.count) in local DataStore")
})
}
})
}
This message comes out from the XCode console:
"Succesfully retrieved 103
Saved 103 in local DataStore"
So far so good right?
This is my code when I am about to query from the local datastore:
dispatch_async(dispatch_get_main_queue()) { () -> Void in
let bookQuery = PFQuery(className: "Publication")
.fromLocalDatastore()
bookQuery.whereKey("publication_Type", equalTo: "Book")
bookQuery.findObjectsInBackgroundWithBlock { (bookObject, error) -> Void in
if error == nil{
print("Books found: \(bookObject!.count)")
self.displayData(bookObject!)
}
}
}
And I get from the console: Books found: 0.
What gives? What am I doing wrong? I read and read and read. NOTHING. I thought the ".ignoreACL()" would work but it didn't. Can anyone help me please?
I don't see where you are pinning the PFObjects into the local datastore. Perhaps that is your problem.
Calling any of PFObjects save methods saves them back to your parse server, not a local datastore. Look up how to use pin to accomplish what you want.
Also, dispatching these asynchronous calls to the main queue makes no sense. They are already executing on a background queue. In most cases, you only need to dispatch back to the main queue if you want to do something to the UI and that should be done in the completion handler.
Related
I have a Parse backend setup where I have three classes:
User
Place - with restaurant information
SavedPlace - an object modelling the User and Place relationship with pointers to both User and SavedPlace.
What I'm trying to do is to be unable to sync my network and local datastore but only where there are changes i.e. only for SavedPlace objects that are different in the network and local (using updateAt). However, I'm running into issues with pinning and unpinning and I've looked everywhere including the following post below but I cannot seem to resolve it.
Parse Local Datastore + Network Sync
See my code for this function where I want to fetch only the updated SavedPlace objects, unpin the old ones in Local Datastore, and re-pin into the Datastore.
The issue seems to be when I re-pin the updated SavedPlace objects retrieved from the network - it seems to delete the Place objects in the Local Datastore. As you can see in the image below, there are both SavedPlace and Place objects pinned and re-pinning the SavedPlace objects delete all the Place objects except for the SavedPlace object I repinned.
SQLite of Local Datastore
Any way round this? Am I using pinAllInBackground correctly?
Appreciate your help on this.
func fetchUpdatedSavedPlacesRemotelyAndPinLocally() {
if let user = PFUser.currentUser(),
let lastUpdateLocalDatastore = self.userDefaults.objectForKey("lastUpdateLocalDatastore") {
// Fetch the places from Parse where lastUpdateDate in Parse is newer than the lastUpdateLocalDatastore
print("Current lastupdateLocalDatastore: \(lastUpdateLocalDatastore)")
let savedPlaceQueryParse = PFQuery(className: "SavedPlace")
savedPlaceQueryParse.whereKey("user", equalTo: user)
savedPlaceQueryParse.includeKey("place")
savedPlaceQueryParse.includeKey("objectId")
savedPlaceQueryParse.whereKey("updatedAt", greaterThanOrEqualTo: lastUpdateLocalDatastore)
savedPlaceQueryParse.findObjectsInBackgroundWithBlock {
(updatedSavedPlacesNetwork: [PFObject]?, error: NSError?) -> Void in
if let updatedSavedPlacesNetwork = updatedSavedPlacesNetwork {
if updatedSavedPlacesNetwork != [] {
print("Success - retrieved \(updatedSavedPlacesNetwork.count) updated places from Parse")
// Create an array of objectIds of the updated saved places to match against in the Local datastore
var updatedSavedPlaceObjectId = [String]()
for updatedSavedPlaceNetwork in updatedSavedPlacesNetwork {
updatedSavedPlaceObjectId.append(updatedSavedPlaceNetwork.objectId!)
}
// Fetch these updated saved places from the Local Datastore
let savedPlaceQuery = PFQuery(className: "SavedPlace")
savedPlaceQuery.fromLocalDatastore()
savedPlaceQuery.whereKey("user", equalTo: user)
savedPlaceQuery.includeKey("place")
savedPlaceQuery.includeKey("objectId")
savedPlaceQuery.whereKey("objectId", containedIn: updatedSavedPlaceObjectId)
savedPlaceQuery.findObjectsInBackgroundWithBlock {
(updatedSavedPlacesLocal: [PFObject]?, error: NSError?) -> Void in
if error == nil {
if let updatedSavedPlacesLocal = updatedSavedPlacesLocal {
// Unpin the updated saved places from the Local Datastore
PFObject.unpinAllInBackground(updatedSavedPlacesLocal) { (success: Bool, error: NSError?) -> Void in
if (success) {
print("Success - \(updatedSavedPlacesLocal.count) updated saved places unpinned from Local Datastore")
// Pin the updated saved places from Parse to the Local Datastore and update the lastUpdateLocalDatastore
PFObject.pinAllInBackground(updatedSavedPlacesNetwork) { (success: Bool, error: NSError?) -> Void in
if (success) {
print("Success - \(updatedSavedPlacesNetwork.count) updated saved places pinned to Local Datastore")
self.userDefaults.setObject(NSDate(), forKey: "lastUpdateLocalDatastore")
print("New lastUpdateLocalDatastore: \(self.userDefaults.objectForKey("lastUpdateLocalDatastore"))")
}
else {
print("Fail - updated saved places not pinned and returned with error: \(error!.description)")
}
}
}
else {
print("Fail - updated saved places not unpinned and returned with error: \(error!.description)")
}
}
}
}
else {
print("Fail - updated saved places not fetched in Local Database and returned with error: \(error!.description)")
}
}
}
else {
print("No updates")
}
}
else {
print("Fail - load from Parse failed with error: \(error!.description)")
}
}
}
}
Could anyone tell me why my startingPoints array is still at 0 elements? I know that I am getting objects returned during the query, because that print statement prints out each query result, however it seems like those objects are not getting appended to my local array. I've included the code snippet below...
func buildStartSpots() -> Void {
let queryStartingPoints = PFQuery(className: "CarpoolSpots")
queryStartingPoints.whereKey("spotCityIndex", equalTo: self.startingCity)
queryStartingPoints.findObjectsInBackgroundWithBlock{(objects: [PFObject]?, error: NSError?) -> Void in
if error == nil {
for object in objects! {
print("starting point: \(object)")
self.startingPoints.append(object)
}
} else {
// Log details of the failure
print("Error: \(error!) \(error!.userInfo)")
}
}
print("starting points")
dump(self.startingPoints)
}
While I have no experience in Parse, the block is asynchronously executed and likely non-blocking as dictated by the method name of the API call. Therefore, it is not guaranteed that the data would be available at the time you call dump, since the background thread might still be doing its work.
The only place that the data is guaranteed to be available at is the completion block you supplied to the API call. So you might need some ways to notify changes to others, e.g. post an NSNotification or use event stream constructs from third party libraries (e.g. ReactiveCocoa, RxSwift).
When you try to access the array, you need to use it within the closure:
func buildStartSpots() -> Void {
let queryStartingPoints = PFQuery(className: "CarpoolSpots")
queryStartingPoints.whereKey("spotCityIndex", equalTo: self.startingCity)
queryStartingPoints.findObjectsInBackgroundWithBlock{(objects: [PFObject]?, error: NSError?) -> Void in
if error == nil {
for object in objects! {
print("starting point: \(object)")
**self.startingPoints.append(object)**
}
//use it here
startingPoints xxx
} else {
// Log details of the failure
print("Error: \(error!) \(error!.userInfo)")
}
}
print("starting points")
dump(self.startingPoints)
}
I am able to get the application functioning as intended and will close this answer out.
It seems as though that the startingPoints array is not empty, and the values I need can be accessed from a different function within that same class.
The code snippet I am using to access my locally stored query results array is here:
for object in self.startingPoints {
let startingLat = object["spotLatitude"] as! Double
let startingLong = object["spotLongitude"] as! Double
let carpoolSpotAnnotation = CarpoolSpot(name: object.valueForKey("spotTitle") as! String, subTitle: object.valueForKey("spotSubtitle") as! String, coordinate: CLLocationCoordinate2D(latitude: startingLat, longitude: startingLong))
self.mapView.addAnnotation(carpoolSpotAnnotation)
The code snippet above is located within my didUpdateLocations implementation of the locationManager function, and with this code, I am able to access the query results I need.
I'm developing an iOS project using Parse.com as backend server.
Basically, I'm currently implementing a very basic feature which just simply retrieve some objects with simple condition.
However, the objects can only be correctly retrieved the first time. No matter how I changed any values in Parse "Core" via Web, I still cannot get updated values by refreshing in the app.
For example, I have a class called "Event", the fields are changed from Parse server, but the result I retrieve are never updated.
let eventServerQuery = Event.query()
// I tried to clear all cached results
PFQuery.clearAllCachedResults()
eventServerQuery?.whereKey(EventFields.Campus.rawValue, equalTo: campus!)
eventServerQuery?.findObjectsInBackgroundWithBlock({ (allEvents, error) -> Void in
self.refreshControl?.endRefreshing()
self.toggleRefreshButtonWithSpinner(false)
if error != nil {
print(error?.localizedDescription)
}else{
if allEvents?.count > 0 {
// Display on the map
for eventObject in allEvents! {
let event = Event.initializeFieldsFromPFObject(eventObject)
self.delegate?.addEventToMap(event)
self.events.append(event)
print("\(event.updatedAt)")
print("\(event.title) has \(event.numberOfTasks) tasks")
}
// Event TVC data source
self.tableView.reloadData()
}
}
})
If I delete the app in my device and run the project again, it will of course reload everything from scratch, so that the data will become correct again...
Any help will be appreciated!
Finally, I worked out by myself. I found that whenever the PFObject was pinned, its fields will not be updated. The solution is that the object need to be unpinned before retrieve from server.
Event.unpinAllInBackground(events, block: { (success, error) -> Void in
if error != nil {
print(error?.localizedDescription)
}else{
self.events.removeAll()
let eventServerQuery = Event.query()
eventServerQuery?.whereKey(EventFields.Campus.rawValue, equalTo: self.campus!)
eventServerQuery?.findObjectsInBackgroundWithBlock({ (allEvents, error) -> Void in
print("Debug: retrieving events from server")
self.refreshControl?.endRefreshing()
self.toggleRefreshButtonWithSpinner(false)
if error != nil {
print(error?.localizedDescription)
}else{
if allEvents?.count > 0 {
// Display on the map
for eventOnline in allEvents! {
let event: Event = eventOnline as! Event
event.pinInBackground()
self.delegate?.addEventToMap(event)
self.events.append(event)
}
// Event TVC data source
self.tableView.reloadData()
}
}
})
}
})
Welcome to add comments here regarding the internal logic of Parse library, as sometimes it is not quite clear I think.
Please help me to solve this problem - after a lot of (not so efficent...) search I can't do this alone.
I have the following methods:
showLoadingAnimation()
to show the loading animation while background tasks are running
hideLoadingAnimation()
to hide the loading animation as soon as all the background tasks are finished
getUserFacebookData()
to get Facebook-user data
uploadUserFacebookDataToServer()
to upload the Facebook-user data to the server (and perform tasks with them).
What I want to perform:
Show up the loading animation: showLoadingAnimation()
Get the user data from Facebook: getFacebookData()
Wait until these data are being downloaded
As soon as the Facebook-user data are being download, upload these data to the server: uploadUserFacebookDataToServer()
Wait untile these data are being uploaded
Hide the loading animation: hideLoadingAnimation()
Now my problem is, that I don't know how to solve this problem. I know, that I should use sync and/or async tasks, GCD... But I don't know how, and I can't find a proper guide to it.
Could someone explain it to me through these functions?
Thanks!
UPDATE:
Thank you, Zhi-Wei Cai, that was the kind of answer what I was hoping for.
Now it seems to work, the calling order is OK,
but now the problem is the same as the beginning:
uploadUserFacebookDataToServer()
doesn't wait until
getUserFacebookData
downloads the user data from Facebook, that's why it won't be able to work with the necessary data given back from
getUserFacebookData
Any idea? Is there anything to do with dispatch?
UPDATE 2:
As you requested, here are the fuctions. I hope, with these information you can help me to solve this problem and to understand this whole process.
func getFacebookUserData(completionHandler: () -> Void)
{
println("getFacebookUserData")
let graphRequest : FBSDKGraphRequest = FBSDKGraphRequest(graphPath: "me", parameters: nil)
graphRequest.startWithCompletionHandler({ (connection, result, error) -> Void in
if ((error) != nil)
{
// Process error
println("Error: \(error)")
}
else
{
let userID : NSString = result.valueForKey("id") as NSString!
var defaults: NSUserDefaults = NSUserDefaults.standardUserDefaults()
defaults.setObject(userID, forKey: "settings_facebookID")
self.facebookID_fromSettings = userID
}
})
and
func getObjectIDfromFacebookID(completionHandler: () -> Void)
{
println("getObjectIDfromFacebookID")
var query = PFQuery(className:"users")
query.whereKey("facebookID", equalTo:facebookID_fromSettings)
println("getObjectIDfromFacebookID: facebookID: " + facebookID_fromSettings)
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]?, error: NSError?) -> Void in
if error == nil {
// The find succeeded.
println("Successfully retrieved \(objects!.count) scores.")
// Do something with the found objects
if (objects!.count == 0) {
// New user, registering
println("New user, registering")
}
else
{
//User is already regsitered, reading out objectID
println("User is already regsitered, reading out objectID")
}
if let objects = objects as? [PFObject] {
for object in objects {
println("objectID: " + object.objectId)
var objectID: String = object.objectId
println(objectID)
var defaults: NSUserDefaults = NSUserDefaults.standardUserDefaults()
defaults.setObject(objectID, forKey: "settings_objectID")
}
}
}
}
completionHandler()
}
So the first function gets the facebookID from the FB-server, but this process takes time, it won't give a result immediately. The second function should work with this data, that's why it should "wait" until the first gives back the requested data.
I can solve this problem by building these 2 fuctions together in one, but that's "not elegant", and I also would like to use this (sync/async dispatch) method in other parts of the project,
Thanks for trying to help me!
You can use completion handlers:
func showLoadingAnimation() {
self.getUserFacebookData({ () -> Void in
self.uploadUserFacebookDataToServer({ () -> Void in
self.hideLoadingAnimation()
})
})
}
func getUserFacebookData(completionHandler: () -> Void) {
println("getUserFacebookData")
completionHandler()
}
func uploadUserFacebookDataToServer(completionHandler: () -> Void) {
println("uploadUserFacebookDataToServer")
completionHandler()
}
func hideLoadingAnimation() {
println("hideLoadingAnimation")
}
Once showLoadingAnimation() is called, the rest will be done asynchronously.
Reference: https://developer.apple.com/library/ios/featuredarticles/Short_Practical_Guide_Blocks/
[EDIT] Use dispatch_group_t
check following code
Step 1: showLoadingAnimation()
Step 2: dispatch_group_t faceBookService = dispatch_group_create();
Step 3:dispatch_group_async_enter(faceBookService,queue,^{
getUserFacebookData()
// The below given line should be inside completion handler or after the above task has finished
dispatch_group_leave(faceBookService);
});
Step 4:dispatch_group_async_enter(faceBookService,queue,^{
uploadUserFacebookDataToServer()
// The below given line should be inside completion handler or after the above task has finished
dispatch_group_leave(faceBookService);
});
Step 5:dispatch_group_notify(faceBookService,dispatch_get_main_queue(),^{
//Called after finishing both tasks.
hideLoadingAnimation()
});
I've used Parse successfully in other apps before but never used the delete function. I'm trying to delete a value ( an alphabetical letter) in a column (column title is 'letter') associated with a user in Parse. I'm using Swift. The code is finding the correct value as evident via a println in the deletion code, but nothing is happening after the remove and save functions are executed. The value is still there in the column. And I'm not getting any Parse errors. The code is below. Any help, as always, will be greatly appreciated.
var query = PFQuery(className: "game")
query.whereKey("player", equalTo:PFUser.currentUser())
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]!, error: NSError!) -> Void in
if !(error != nil) {
for object in objects {
var myLetter = object["letter"]! as String
println("The object for key letter is \(myLetter)") //This prints the correct letter in the current user's Letter column
PFUser.currentUser().removeObjectForKey("letter")
PFUser.currentUser().saveInBackgroundWithBlock{
(success: Bool, error: NSError!) -> Void in
if (success) {
// The object has been saved.
println("success")
} else {
// There was a problem, check error.description
println(error)
}
}
}
}
}
I think the issue is that you are creating a new Parse query and deleting it locally as opposed to retrieving the item and then deleting it. So, retrieve the item you want to delete and then call the deleteInBackground method.