Cancel PFQuery takes too much time to load - ios

Apologies that I couldn't think of a better way to describe my
application’s functionality.
I've found quite a lot of posts to this topic, also in the old archive at parse.com. Nevertheless it just doesn't work for me. After creating an instance of a PFQuery that is triggered by PFQuery.findObjects (but runs on a background thread) I'm not able to cancel it during its request process.
Scenario : Basically I have an app which connects to Parse. I have display the data which is more than 100 records in DataDisplay Screen and it has a back button when user click on back button and if PFQuery.findObjects still run it on background thread then I have to cancel it.
I have tried inserting PFQuery.cancel in the viewWillDisappear, but it can not stop and due to these DataDisplay Screen’s dealloc method is not call.
My code, incase it may help:
- (void)loadANDSortingSongInformationWS {
if(ISINTERNET) {
if(self.isShowLoadingForSkipSong)//Not Showing the activity indicator
self.isShowLoadingForSkipSong = NO;
else if(self.isFirstLoad || self.isAddPullToRefreshLikeSong)//Showing the indicator
[self showHideLoading:YES];
PFQuery *query = [PFQuery queryWithClassName:[UserPlaylistSongs parseClassName]];
[query setLimit:1000];
[query orderByDescending:#"createdAt"];
[query whereKey:#"Playlist" equalTo:self.playlistInfo];
[query includeKey:#"Playlist"];
[query includeKey:#"Song"];
[query includeKey:#"AddedBy"];
[query includeKey:#"Host"];
__weak typeof(self) weakSelf = self;
[self.opearation addOperationWithBlock:^{
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (objects.count == 0) {//No Songs Found
//If there is no records
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf showHideLoading:NO];
if(weakSelf.isFirstLoad || weakSelf.isAddPullToRefreshLikeSong) {//Problem while user pull to refresh when there is no song
[KSToastView ks_showToast:#"No Songs Found." duration:1.0f];
}
});
}
else {//Songs Found
dispatch_async(dispatch_get_main_queue(), ^{
NSMutableArray *arrParseSongList = [[NSMutableArray alloc] init];
__block NSInteger getTotalObjectsCount = 0;
for(NSInteger i=0; i<objects.count; i++) {
SongListInfo *songListData = [[SongListInfo alloc] init];
songListData.userPlaylistInfo = objects[i];
songListData.totLikes = [objects[i][#"Likes"] integerValue];
songListData.totDisLikes = [objects[i][#"Dislikes"] integerValue];
songListData.isPlaying = [objects[i][#"PlayingStatus"] boolValue];
songListData.songInfo = objects[i][#"Song"];
songListData.hostInfo = objects[i][#"Host"];
songListData.addedInfo = objects[i][#"AddedBy"];
songListData.playlistInfo = objects[i][#"Playlist"];
songListData.alreadyPlayedOrder = [objects[i][#"AlreadyPlayedIndex"] integerValue];
songListData.totRating = songListData.totLikes - songListData.totDisLikes;
songListData.createdDate = songListData.userPlaylistInfo.createdAt;
//User Specific for loading the song list.
PFQuery *queryLikeDislike = [PFQuery queryWithClassName:[SongLikeDislike parseClassName]];
[queryLikeDislike whereKey:#"SongID" equalTo:songListData.songInfo.objectId];
[queryLikeDislike whereKey:#"UserID" equalTo:[SINGLETON getUserID]];
[queryLikeDislike whereKey:#"PlaylistID" equalTo:songListData.playlistInfo.objectId];
[queryLikeDislike findObjectsInBackgroundWithBlock:^(NSArray *objectsLike, NSError *error) {
getTotalObjectsCount += 1;
if(error == nil) {
if(objectsLike.count) {
BOOL isDelete = [objectsLike.lastObject[#"DeleteRecord"] boolValue];
BOOL isLike = [objectsLike.lastObject[#"Like"] boolValue];
if(isDelete)
songListData.ratingType = RATING_GRAY;
else if(isLike)
songListData.ratingType = RATING_GREEN;
else
songListData.ratingType = RATING_RED;
}
else
songListData.ratingType = RATING_GRAY;
}
else
NSLog(#"Problem while getting the rating type");
[arrParseSongList addObject:songListData];
NSLog(#"i : %ld, objects : %ld",(long)getTotalObjectsCount, (long)objects.count);
if(getTotalObjectsCount == objects.count)
[weakSelf processAfterGettingLikesAndDislikeInfo:arrParseSongList];
}];
}
});
}
}];
}];
NSLog(#"In method -> All operation : %ld",(long)self.opearation.operations.count);
}
else
[UIAlertView showErrorWithMessage:NO_INTERNET handler:nil];
}
- (void)processAfterGettingLikesAndDislikeInfo:(NSMutableArray *)arrParseSongList {
NSPredicate *filterGrayout = [NSPredicate predicateWithFormat:#"isPlaying == YES"];
NSArray *arrGrayOut = [arrParseSongList filteredArrayUsingPredicate:filterGrayout];
NSSortDescriptor *aSortDescriptorGrayedOut = [[NSSortDescriptor alloc] initWithKey:#"alreadyPlayedOrder.intValue" ascending:YES];
NSArray *arrGrayedOutSong = [NSMutableArray arrayWithArray:[arrGrayOut sortedArrayUsingDescriptors:[NSArray arrayWithObject:aSortDescriptorGrayedOut]]];
NSPredicate *filterNonPlay = [NSPredicate predicateWithFormat:#"isPlaying == NO"];
NSArray *arrNonPlay = [arrParseSongList filteredArrayUsingPredicate:filterNonPlay];
NSSortDescriptor *aSortDescriptorRating = [[NSSortDescriptor alloc] initWithKey:#"totRating.intValue" ascending:NO];
NSSortDescriptor *aSortDescriptorCreatedDate = [[NSSortDescriptor alloc] initWithKey:#"createdDate" ascending:YES];
NSArray *arrSortOnNormalSong = [NSMutableArray arrayWithArray:[arrNonPlay sortedArrayUsingDescriptors:[NSArray arrayWithObjects:aSortDescriptorRating,aSortDescriptorCreatedDate,nil]]];
if(self.arrSongsData.count)
[self.arrSongsData removeAllObjects];
[self.arrSongsData addObjectsFromArray:arrGrayedOutSong];
[self.arrSongsData addObjectsFromArray:arrSortOnNormalSong];
[self showHideLoading:NO];
[self.tblView reloadData];
}
And I am call in viewWillDisappear.
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if(self.queryInstance)
[self.queryInstance cancel];
}
Thanks for any help!

So there are a number of things which are potentially keeping the instance in memory:
You're adding operations to an operation queue, and those operations are retaining contents so any operations still in the queue will keep the controller instance alive
Your completion blocks are all using self rather than creating a __weak reference and using that, so while the blocks are retained so it the controller instance
In the completion of the first query you're starting another query for each of the, potentially 1000, results. Not only could this flood the network with requests, but each one is also retaining the controller instance
You potentially have 1000 queries running that aren't cancelled, depending on exactly when you try to cancel
Mainly, you should be using weak references in the completion blocks so that when you try to deallocate the controller it disappears and the completions of the queries just silently run to nothing in the background. Obviously you do really want to cancel or prevent all 1000 queries from running if the user isn't interested though...

Related

Parse.com returns empty objects

I have an NSArray called "malls" that contains a large number of NSDictionaries (each a specific mall) that I uploaded to Parse.com. I want my users to be able to access this information to create map annotations.
I've tried to do this in 2 different ways:
I tried uploading the entire array as a property of a single object:
this is the upload:
in the dataBank.h file:
#property (strong, nonatomic) NSMutableArray* malls;
in the .m file
PFObject *obj = [PFObject objectWithClassName:#"malls"];
obj[#"mallsData"] = self.malls;
[obj saveInBackground];
I try to get the data from parse:
-(NSMutableArray *)createAnnotationsFromParse
{
__block NSMutableArray* data = [[NSMutableArray alloc]init];
__block NSMutableArray* annots = [[NSMutableArray alloc]init];
PFQuery* query = [PFQuery queryWithClassName:#"malls"];
[query getObjectInBackgroundWithId:#"Eaib9yfTRe" block:^(PFObject *object, NSError *error) {
data = [object objectForKey:#"mallsData"];
annots = [self createAnnotations:data];
}];
return annots;
}
The problem is getObjectInBackground is asynchronous and always returns before getting the data from the server. I tried moving the "return annots" inside the code block but that gives the following error: "incompatible block pointer types".
I uploaded 5 "mall" objects to class "malls2". Each object has 2 properties- name and address:
for(int i = 0; i < 5; i++)
{
PFObject *mallsObj = [PFObject objectWithClassName:#"malls2"];
mallsObj[name] = [[self.malls objectAtIndex:i]objectForKey:name];
mallsObj[address] = [[self.malls objectAtIndex:i]objectForKey:address];
[mallsObj saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if(succeeded)
NSLog(#"yay");
else
NSLog(#"%#", error.description);
}];
}
then I try to get it back:
-(NSMutableArray *)createAnnotationsFromParse
{
__block Annotation* anno = [[Annotation alloc]init];
__block NSMutableArray* annots = [[NSMutableArray alloc]init];
PFQuery* query = [PFQuery queryWithClassName:#"malls2"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if(error)
NSLog(#"%#", error.description);
else
{
for(int i = 0; i < [objects count]; i++)
{
//createAnnotationWithTitle is a func in a different class that creates the annotation
anno = [anno createAnnotationWithTitle:[[objects objectAtIndex:i] objectForKey:name] andAddress:[[objects objectAtIndex:i]objectForKey:address]];
}
[annots addObject:anno];
}
}];
return annots;
}
I get 5 objects but they're all empty.
It's a basic misunderstanding about asynchronous methods with block parameters. The trick is to get out of the habit of thinking that code that appears later in a source file runs later. The assumption works in this function:
- (void)regularFunction {
// these NSLogs run top to bottom
NSLog(#"first");
NSLog(#"second");
NSLog(#"third");
}
This will generate logs: first, second, third. Top to bottom, but not in this one:
- (void)functionThatMakesAsynchCall {
// these NSLogs do not run top to bottom
NSLog(#"first");
[someObject doSomeAsynchThing:^{
NSLog(#"second");
}];
NSLog(#"third");
}
That function will generate logs - first, third, second. The "second" NSLog will run well after the "third" one.
So what should you do? Don't try to update the UI with results of a parse call until after it completes, like this:
// declared void because we can't return anything useful
- (void)doSomeParseThing {
// if you change the UI here, change it to say: "we're busy calling parse"
PFQuery* query = [PFQuery queryWithClassName:#"malls2"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if(!error) {
// change the UI here, say by setting the datasource to a UITableView
// equal to the objects block parameter
}
}];
// don't bother changing the UI here
// don't bother returning anything here
// we just started the request
}
But what if doSomeParseThing is really a model function, whose only job is to fetch from parse, not to know anything about UI? That's a very reasonable idea. To solve it, you need to build your model method the way parse built their's, with block parameter:
// in MyModel.m
// declared void because we can't return anything useful
+ (void)doSomeParseThing:(void (^)(NSArray *, NSError *))block {
PFQuery* query = [PFQuery queryWithClassName:#"malls2"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
block(objects, error);
}];
}
Now your view controller can call, leave the query work to your model and the UI work to the vc:
// change UI to 'busy' here
[MyModel doSomeParseThing:^(NSArray *objects, NSError *error) {
// update UI with objects or error here
}];
Figured it out. It looked like I was getting "empty objects" (can be seen here postimg.org/image/ot7ehn29b ) but once I tried to access data from the objects I saw there was no problem. Basiclly I was tricked by the PFObjects in the array showing "0 objects" and assumed it meant they came back from Parse.com empty. Here's how I checked, just for reference:
PFQuery *query = [PFQuery queryWithClassName:#"malls2"];
NSArray *array = [query findObjects];
NSLog(#"%#", [[array objectAtIndex:0] objectForKey:#"name"]; // I have a string property called "name" in my Parse object.

How to run a loop of parse queries?

I am making an app that registers users and allows them to add friends etc. So I have a LoginViewController where I retrieve the array of user's friends' objectIds when the login is successful. This function is called.
- (void) getFriendList
{
NSString * objectID = [PFUser currentUser].objectId;
NSLog(#"%#", objectID);
PFQuery *query = [PFUser query];
[query getObjectInBackgroundWithId:objectID block:^(PFObject *username, NSError *error) {
sharedClass.sharedInstance->retrievedFriends = username[#"friendsIds"];
friendListLoaded = YES;
[self getFriendsUsernames];
}];
Here i get an array object that contains object ids of all the friends this user has. As you can see when getFriendList is completed, it calls another function called GetFriendsUsernames. This function is supposed to retrieve profile pictures and usernames of those friends so I can populate the Friend List view later.
-(void) getFriendsUsernames
{
NSMutableArray * objectIDs = [[NSMutableArray alloc] initWithArray: sharedClass.sharedInstance->retrievedFriends];
PFQuery *query = [PFUser query];
int friendsCount = [objectIDs count];
for(int i = 0; i<=friendsCount;i++)
{
[query getObjectInBackgroundWithId:objectIDs[i] block:^(PFObject *username, NSError *error) {
[sharedClass.sharedInstance->friendsUsernames addObject:username[#"username"]];
[sharedClass.sharedInstance->friendsProfilePictures addObject:username[#"ProfilePicture"]];
}];
NSLog(#"%#", sharedClass.sharedInstance->friendsUsernames );
}
}
But this seems to be unsuccessful because nothing is logged on the console where it should log username of retrieved friend whenever one query gets finished. Am I doing this the right way? My array count is right so loop runs to the number of friends a user has. It also prints the username of first object in objectIds array if i put the logging command in the loop.
Your NSLog runs immediately after your query objects are queued for execution. Therefore, even if the code is running correctly (and I suspect it might not be), you'll never get the correct results you're after logged to the console.
If you have your Parse classes designed in a certain way, you could collapse this into 1 query by using the include method on PFQuery. This assumes that you've created an array of Pointers on your User object, named "friends". If you actually store the objectId (i.e., the string value) of each friend, this code won't work.
I'm not using Swift yet, so here's how I'd write this query in Objective-C:
- (void)getFriendList {
PFUser *currentUser = [PFUser currentUser];
PFQuery *query = [PFUser query];
[query whereKey:#"objectId" equalTo:currentUser.objectId];
[query includeKey:#"friends.username"];
[query includeKey:#"friends.ProfilePicture"];
[query getFirstObjectInBackgroundWithBlock:^(PFObject *user, NSError *error) {
if (error != nil) {
// Process the error
} else }
// You've got your user and the data you wanted
}
}];
}
Found it. Not the prettiest way out there but well it does what i want. gets me arrays of usernames and profile pictures of every objectID contained in array of friends.
NSMutableArray * objectIDs = [[NSMutableArray alloc] initWithArray: sharedClass.sharedInstance->retrievedFriends];
PFQuery *query = [PFUser query];
PFFile * imageFile;
//UIImage *profilePictureData;
int friendsCount = [objectIDs count];
for(int i = 0; i<friendsCount;i++)
{
PFObject * username = [query getObjectWithId:objectIDs[i]];
[sharedClass.sharedInstance->friendsUsernames addObject:username[#"username"]];
[sharedClass.sharedInstance->friendsEmailAdresses addObject:username[#"email"]];
//NSLog(#"%#",username[#"ProfilePicture"]);
imageFile = [username objectForKey:#"ProfilePicture"];
NSData *imageData = [imageFile getData];
UIImage *imageFromData = [UIImage imageWithData:imageData];
[sharedClass.sharedInstance->friendsProfilePictures addObject:imageFromData];
NSLog(#"%#", sharedClass.sharedInstance->friendsUsernames );
}

Getting Warning with Parse.com

I'm triying to access each item on an NSArray trough enumerateObjectsUsingBlock, since it let me use fast enumration and evaluating the index.
When I use findObjectsInBackgroundWithBlock,
I get Warning: A long-running operation is being executed on the main
thread. Break on warnBlockingOperationOnMainThread() to debug.
As I thought was used in the background to not block the Mainthread. Here is my code and what I'm triying to achieve its that I have two UIImageView container that I'm pulling the images from the result of the relation on that query. Since there are only container I tought it was better just to evaluate the index of NSArray.
Not sure how I can remedy that warning.
Thanks
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (objects != 0) {
[objects enumerateObjectsUsingBlock:^(PFUser *object, NSUInteger idx, BOOL *stop) {
if (idx == 0) {
PFFile *userProfile = [object objectForKey:#"userPic"];
cell.promoter1.file = userProfile;
cell.promoter1.contentMode = UIViewContentModeScaleAspectFit;
[cell.promoter1 loadInBackground];
cell.promoter1Name.textAlignment = NSTextAlignmentCenter;
cell.promoter1Name.lineBreakMode = NSLineBreakByWordWrapping;
}
if (idx == 1) {
PFFile *userProfile = [object objectForKey:#"userPic"];
cell.promoter2.file = userProfile;
cell.promoter2.contentMode = UIViewContentModeScaleAspectFit;
[cell.promoter2 loadInBackground];
NSAttributedString *promoter1Name;
cell.promoter2Name.textAlignment = NSTextAlignmentCenter;
*stop = YES;
}
}];
}
}];
Troubleshooting my code, I realized that findObjectsInBackgroundWithBlock does not cause this warning.
On another part of my code I had this:
PFUser *user = [PFQuery getUserObjectWithId:#"ebwFrl8PcF"];
[relation addObject:user];
[self.event saveInBackground];
Which block the main thread.
I apologize.

Set PFQuery order

I want to make my PFQuery come in a random order, so the next time I'm creating the same PFQuery with limit it won't return the same objects as the first one.
PFQuery *query = [PFUser query];
[query orderBy...]; //Is there a randomOrder method?
//Or a workaround to get random order?
[query setLimit:10];
I need this to be in a random order every time, or else the PFQuery will contain the same 10 objects everytime
You can't change the ordering of data returned in the query, but you can use paging to change the first object that is returned - so you could do something like this (it is based on the ToDo sample code from Parse but it will work for any object) -
PFQuery *query =[PFQuery queryWithClassName:#"Todo"];
NSInteger count=[query countObjects];
NSInteger skip = arc4random_uniform(count-10);
query.skip=skip;
query.limit=10;
NSArray *results=[query findObjects];
NSLog(#"object count=%d",results.count);
for (PFObject *object in results) {
NSLog(#"text=%#",object[#"text"]);
}
You can now retrieve your 10 objects. for any given skip count they will be in the same order, but you could randomise the order after you retrieved the 10 items. Simply put them into an NSMutableArray and use technique in this answer - Re-arrange NSArray/MSMutableArray in random order
Note that this code isn't optimal as it doesn't perform the fetch tasks on the background thread. To use background threads you would use something like the following -
PFQuery *query =[PFQuery queryWithClassName:#"Todo"];
[query countObjectsInBackgroundWithBlock:^(int number, NSError *error) {
query.skip=arc4random_uniform(number-10);;
query.limit=10;
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (error) {
NSLog(#"An error occurred - %#",error.localizedDescription);
}
else {
NSLog(#"object count=%d",objects.count);
for (PFObject *object in objects) {
NSLog(#"text=%#",object[#"text"]);
}
}
}];
}];
PFQuery does not support random order but you can workaround this by creating an increasing index field to each object
Then given that you know the maxIndexin the table you can generate random indices as the following:
- (NSArray *)generateRandomIndices:(int)maxIndex limit:(int)limit {
NSMutableArray *indices = [[NSMutableArray alloc] initWithCapacity:limit];
for (int i=0; i<limit; i++) {
int randomIndex = arc4random() % maxIndex;
[indices addObject:[NSNumber numberWithInt:randomIndex]];
}
return indices;
}
Now you can query your class by using INpredicate
NSArray *randomIndices = [self generateRandomIndices:maxIndex limit:10];
NSPredicate *predicate = [NSPredicate predicateWithFormat:
#"index IN %#", randomIndices];
PFQuery *query = [PFQuery queryWithClassName:#"className" predicate:predicate];
PFQuery don't give random objects. You can get all objects then randomize to get any 10 objects from it and show it.

windows Azure Paging Data

Hi I need to download over 2000 records from azure , the maximum you can download is 1000 at the time , so I need to use a completion handler to download 200 at the time.
They posted this code as an example but I don't know how to use.
If I copy this to Xcode there is an error
(bool)loadResults() - Error " Expect Method Body "
Returning data in pages
Mobile Services limits the amount of records that are returned in a single response. To control the number of records displayed to your users you must implement a paging system. Paging is performed by using the following three properties of the MSQuery object:
BOOL includeTotalCount
NSInteger fetchLimit
NSInteger fetchOffset
In the following example, a simple function requests 20 records from the server and then appends them to the local collection of previously loaded records:
- (bool) loadResults() {
MSQuery *query = [self.table query];
query.includeTotalCount = YES;
query.fetchLimit = 20;
query.fetchOffset = self.loadedItems.count;
[query readWithCompletion:(NSArray *items, NSInteger totalCount, NSError *error) {
if(!error) {
//add the items to our local copy
[self.loadedItems addObjectsFromArray:items];
//set a flag to keep track if there are any additional records we need to load
self.moreResults = (self.loadedItems.count < totalCount);
}
}];
}
thanks for your help.
If you are getting Error " Expect Method Body " then you copied it into your code incorrectly and there is a formatting issue.
If you want to load data with paging in a single call, I would do something like this:
in your .h file declare
typedef void (^CompletionBlock) ();
#property (nonatomic, strong) NSMutableArray *results;
in your .m file
- (void)loadData
{
self.results = [[NSMutableArray alloc] init];
MSClient *client = [MSClient clientWithApplicationURLString:#"YOUR_URL" applicationKey:#"YOUR_KEY"]
MSTable *table = [client tableWithName:#"YOUR_TABLE"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"YOUR_SELECT_FILTER"];
MSQuery *query = [[MSQuery alloc] initWithTable:table predicate:predicate];
//note the predicate is optional. If you want all rows skip the predicate
[self loadDataRecursiveForQuery:query withCompletion:^{
//do whatever you need to do once the data load is complete
}];
}
- (void)loadDataRecursiveForQuery:(MSQuery *)query withCompletion:(CompletionBlock)completion
{
query.includeTotalCount = YES;
query.fetchLimit = 1000; //note: you can adjust this to whatever amount is optimum
query.fetchOffset = self.results.count;
[query readWithCompletion:(NSArray *items, NSInteger totalCount, NSError *error) {
if(!error) {
//add the items to our local copy
[self.results addObjectsFromArray:items];
if (totalCount > [results count]) {
[self loadDataRecursiveForQuery:query withCompletion:completion];
} else {
completion();
}
}
}];
}
Note: I haven't tested this code, but it should work more or less.

Resources