I'm using Parse and have a class of a few jobs with a rating (number 1 out of 5). I want to query for the class and stick each rating into an array, then calculate the average. However, when I try to add the objects into the array, it seems to only add the last item returned by the query, and I can't figure out why.
- (void)viewDidLoad {
[super viewDidLoad];
self.jobName = [self.job objectForKey:#"jobTitle"]; //jobName string
self.title = [NSString stringWithFormat:#"%#", self.jobName];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
PFQuery *getCompletedJobsQuery = [PFQuery queryWithClassName:#"completedJobs"];
[getCompletedJobsQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
self.completedJobs = objects;
for (PFObject *completedJob in objects) {
self.ratingsArray = [[NSMutableArray alloc] init];
NSLog(#"RATING: %#", [completedJob objectForKey:#"customerRating"]);
[self.ratingsArray addObject:[completedJob objectForKey:#"customerRating"]];
}
NSLog(#"ratings array ... %#", self.ratingsArray);
[self.tableView reloadData];
} else {
NSLog(#"Error: %# %#", error, [error userInfo]);
}
}];
}
When it runs, it logs each rating, but when I try to print out the array or check its length, it only shows the last object and doesn't include the other objects:
2015-01-05 13:05:00.274 Ribbit[15538:251863] RATING: 3
2015-01-05 13:05:00.274 Ribbit[15538:251863] RATING: 5
2015-01-05 13:05:00.274 Ribbit[15538:251863] RATING: 4
2015-01-05 13:05:00.274 Ribbit[15538:251863] RATING: 5
2015-01-05 13:05:00.275 Ribbit[15538:251863] ratings array ... (
5
)
So, what's the proper way to retrieve things from Parse and put them into an array?
Edit: As oltman and Jack have indicated, I am re-creating the array each time the loop runs, so moving the array creation outside of the loop solves the problem. That is also my queue to take a break from coding for a few hours and sit in my cube of shame. Thanks!
You're creating a new array (self.ratingsArray = [[NSMutableArray alloc] init];) with each loop iteration. Move this line out of the loop (to before the loop) and you should come out of it with more than just the last object in the array.
You are recreating your array every iteration of the loop:
for (PFObject *completedJob in objects) {
self.ratingsArray = [[NSMutableArray alloc] init]; // HERE
NSLog(#"RATING: %#", [completedJob objectForKey:#"customerRating"]);
[self.ratingsArray addObject:[completedJob objectForKey:#"customerRating"]];
}
Move that outside of the loop:
self.ratingsArray = [[NSMutableArray alloc] init]; // HERE
for (PFObject *completedJob in objects) {
NSLog(#"RATING: %#", [completedJob objectForKey:#"customerRating"]);
[self.ratingsArray addObject:[completedJob objectForKey:#"customerRating"]];
}
Related
I have two array, called array1 and array2. I would like to remove every object from array1 that's value of the "nameId" key can be find in both array. Actually I'm trying it in a for loop, but it doesn't make sense. It doesn not crash, it just simply calls the log in the else statement, that I don't understand why happens. Maybe somebody could show me the right solution.
NSMutableArray *newArray = [self.array1 mutableCopy];
for (PFObject * object in newArray) {
PFObject *placeholderObject = object;
for (PFObject *object2 in self.array2) {
if ([placeholderObject[#"nameId"] isEqualToString:object2[#"nameId"]]) {
[self.array1 removeObject:object];
NSLog (#"EXISTING OBJECT FOUND %#", object);
} else {
NSLog(#"UNIQUE OBJECT FOUND %#", idO[#"hirCime"]);
}
}
}
When creating a mutableCopy of an array you create a new array with a copy of every object in it but they aren't the same ones, so object is a member of newArray but is not a member of self.array1 so you can't remove it from that array.
This should work:
// Creates a new empty mutable array
NSMutableArray *newArray = [#[] mutableCopy];
for (PFObject *object in self.array) {
BOOL found = NO;
for (PFObject *object2 in self.array2) {
if ([object[#"nameId"] isEqualToString:object2[#"nameId"]]) {
found = YES;
NSLog (#"EXISTING OBJECT FOUND %#", object);
break;
} else {
NSLog(#"UNIQUE OBJECT FOUND %#", idO[#"hirCime"]);
}
}
if (!found) {
[newArray addObject:[object copy]];
}
}
// And maybe you want this
self.array = newArray;
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.
I have a mutable array named "eventnameArray". What i want is to add objects. This objects to add come from a Parse query.
This is the mutable array:
eventArray = [[NSMutableArray alloc]init];
This is the Parse query, which works.
PFQuery *query = [PFQuery queryWithClassName:#"Event"];
[query selectKeys:#[#"EventName"]];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
// The find succeeded.
NSLog(#"Successfully retrieved %lu scores.", (unsigned long)objects.count);
// Do something with the found objects
[eventnameArray addObject:[objects valueForKey:#"EventName"]];
NSLog(#"%#", eventnameArray);
} else {
// Log details of the failure
NSLog(#"Error: %# %#", error, [error userInfo]);
}
}];
I suspect the problem is in the addObject call, which is not being performed correctly, or maybe in the whole PFQuery structure.
To show the array in the tableviewcell, i do this:
cell.label.text = [self.eventnameArray objectAtIndex: [indexPath row]];
EDIT :
I replaced addObject for addObjectsFromArray like this:
[eventnameArray addObjectsFromArray:[objects valueForKey:#"EventName"]];
But i still can't get it to work.
EDIT 2:
I replaced EDIT 1 with :
for (NSObject *object in objects){
NSString *name = [object valueForKey:#"EventName"];
[eventnameArray addObject:name];
}
But the label still doesn't show anything.
EDIT 3 :
The solution was just a matter or reloading data like this:
[self.tableView reloadData];
I guess what you're looking for is to replace addObject: with addObjectsFromArray: because parse returns an array to you so calling valueForKey:#"EventName" will return an array and you want to add all of those names to your eventArray.
So:
[eventnameArray addObjectsFromArray:[objects valueForKey:#"EventName"]];
You probably want to add the EventName of each object in the objects array to your eventnameArray, not the "EventName" value of the objects array.
Try this instead:
for (NSObject *object in objects){
NSString *name = [object valueForKey:#"EventName"];
[eventnameArray addObject:name];
}
//EDIT: Addition for a complete fix. Reload the tabeview afterwards.
[self.tableView reloadData];
I have the following code where I am calling self.tableView reloadData inside the loop which is a bad idea. If I put it outside the loop then it would not work as expected since it will be called before updating the noOfGroceryItems field.
How can I improve it?
-(void) populateShoppingLists {
[_groceriesService getAllShoppingLists:^(NSArray *results, NSError *error) {
if(error) {
NSLog(#"%#",error.localizedDescription);
}
for(CKRecord *record in results) {
ShoppingList *shoppingList = [[ShoppingList alloc] initWithRecord:record];
// get the number of grocery items in the shopping list
[_groceriesService getItemsByShoppingList:shoppingList result:^(NSArray *results, NSError *error) {
shoppingList.noOfGroceryItems = results.count;
[_shoppingLists addObject:shoppingList];
// THIS IS BAD IDEA
**dispatch_async(dispatch_get_main_queue(), ^{
_shoppingLists = [_shoppingLists sort:#"title" ascending:YES];
[self.tableView reloadData];
});**
}];
}
}];
}
Implementation for getItemsShoppingList method:
-(void) getItemsByShoppingList:(ShoppingList *)shoppingList result:(GetItemsByShoppingList) getItemsByShoppingList
{
CKQuery *query = [[CKQuery alloc] initWithRecordType:#"GroceryItems" predicate:[NSPredicate predicateWithFormat:#"ShoppingList == %#",shoppingList.record]];
[_privateDB performQuery:query inZoneWithID:nil completionHandler:^(NSArray *results, NSError *error) {
getItemsByShoppingList(results,error);
}];
}
You could add a conditional to reload the table if and only if the last shopping list has been fetched, ex:
[_groceriesService getAllShoppingLists:^(NSArray *results, NSError *error) {
if(error) {
NSLog(#"%#",error.localizedDescription);
}
// Variable to count the number
// of records processed
__block int recordsProcessed = 0;
for(CKRecord *record in results) {
ShoppingList *shoppingList = [[ShoppingList alloc] initWithRecord:record];
// (I've changed this second results variable from results
// to results2 in order to distinguish between the two
// "results" variables.)
// get the number of grocery items in the shopping list
[_groceriesService getItemsByShoppingList:shoppingList
result:^(NSArray *results2, NSError *error) {
shoppingList.noOfGroceryItems = results2.count;
[_shoppingLists addObject:shoppingList];
dispatch_async(dispatch_get_main_queue(), ^{
_shoppingLists = [_shoppingLists sort:#"title" ascending:YES];
// Increment recordsProcessed to indicate another
// record has been processed
recordsProcessed ++;
// If all the records have been processed,
// reload the table (using the outer block's
// results variable, not the inner block's result2
// variable).
if (recordsProcessed == results.count) {
[self.tableView reloadData];
}
});
}];
}
}];
Update: And a second solution. Assuming that _shoppingLists starts out empty, you can simply compare results with the number of elements put into _shoppingLists, ex:
[_groceriesService getAllShoppingLists:^(NSArray *results, NSError *error) {
if(error) {
NSLog(#"%#",error.localizedDescription);
}
for(CKRecord *record in results) {
ShoppingList *shoppingList = [[ShoppingList alloc] initWithRecord:record];
// (I've changed this second results variable from results
// to results2 in order to distinguish between the two
// "results" variables.)
// get the number of grocery items in the shopping list
[_groceriesService getItemsByShoppingList:shoppingList
result:^(NSArray *results2, NSError *error) {
shoppingList.noOfGroceryItems = results2.count;
[_shoppingLists addObject:shoppingList];
dispatch_async(dispatch_get_main_queue(), ^{
_shoppingLists = [_shoppingLists sort:#"title" ascending:YES];
// If the number of shopping lists stored
// equals the number of records processed,
// reload the table (using the outer block's
// results variable, not the inner block's result2
// variable).
if (_shoppingLists.count == results.count) {
[self.tableView reloadData];
}
});
}];
}
}];
If you put a dispatch_async after the for loop, you're guaranteed that the block will be dispatched on the main queue, after the loops completes (and your data is updated).
Why not just dispatch the reloadData call once after all your model are processed and updated?
Instead of fast enumerating the results array, you could enumerate with a block, that gives you the index of the current object. if the index is [results count] - 1, trigger the reload.
Of course you should not name two arrays results if the inner one does hide the outer, especially if you intend to call other peoples code «not a very good solution.»
I have a IBOutlet Collection view hooked up to 10 labels after pulling objects from a Parse query. My problem is that for some reason it logs 10 different object Ids but only displays one of the object Ids through the collection view. Here is the code I have:PFQuery *query =
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
// The find succeeded.
NSLog(#"Successfully retrieved %d scores.", objects.count);
// Do something with the found objects
int i = 0;
for (PFObject *object in objects) {
NSLog(#"%#", object.objectId);
for (UILabel *EventLabel in self.EventTitles){
(EventLabel *)self.EventTitles[i]= object.objectId;
i++;
}
}
Does anyone see a problem with the code for it to only display one rather than the 10?
The error is that you execute this cycle
for (UILabel *EventLabel in self.EventTitles){
EventLabel.text = object.objectId;
}
within this other cycle
for (PFObject *object in objects) {
}
It means that the first one gets hexecuted every time you get a new object from objects. And each time you get a object from objects you overwrite all the labels with the same objectID. The effect is that at end all the labels will show the objectID of the last object analysed. You should do something like the following instead:
int i = 0;
for (PFObject *object in objects) {
if (i >= [self.EventTitles count]) break;//to make sure we only write up to the max number of UILabels available in EventTitles
(UILabel *) self.EventTitles[i].text = object.objectId;//I assume the "objectId" property of object is an NSString!
i++;
}
You should rename "EventTitles" to "eventTitles" - it's a common rule that Class names start with a capital letter but instance variables ones don't. It will run anyway if you don't change it but it's a good think to do across your code.