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.
Related
I am trying to set up three NSMutableArray to use in UITableView.
Here is my code:
for (PFObject *object in objects) {
PFUser *user = (PFUser *) object[#"user"];
[ [user objectForKey:#"image"] getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (!error)
{
//Add Comment text
[_commentsArray insertObject:[object objectForKey:#"comment"] atIndex:i];
//Add comment Id
[_commentIDArray insertObject:object.objectId atIndex:i];
//Add user image
[_picsArray insertObject:[UIImage imageWithData:data] atIndex:i ];
if (i == [objects count]-1)
{
[self.tableView reloadData];
}
}
else
{
NSLog(#"Errrror == %ld",(unsigned long)[_picsArray count]);
}
i++;
}];
}
In the PFQuery I am ordering it:
[query orderByDescending:#"createdAt"];
But as far as I can understand image in first row is large. So it takes time to download it. So it goes to second loop. Try to download image. Size is small. Download finished. Add to array. Now download for first image is finished. Add to array but to second place. How can manage it so it add items one by one in the order?
Check this:
// initially, add place holder
for (int i=0; i<objects.count; i++) {
[_commentsArray addObject:#""];
[_commentIDArray addObject:#""];
[_picsArray addObject:#""];
}
for (PFObject *object in objects) {
PFUser *user = (PFUser *) object[#"user"];
[ [user objectForKey:#"image"] getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (!error)
{
NSInteger orderIndex = [objects indexOfObject:object];
//Replace Comment text
[_commentsArray replaceObjectAtIndex:[object objectForKey:#"comment"] atIndex:orderIndex];
//Replace comment Id
[_commentIDArray replaceObjectAtIndex:object.objectId atIndex:orderIndex];
//Replace user image
[_picsArray replaceObjectAtIndex:[UIImage imageWithData:data] atIndex:orderIndex ];
if (i == [objects count]-1)
{
[self.tableView reloadData];
}
}
else
{
NSLog(#"Errrror == %ld",(unsigned long)[_picsArray count]);
}
i++;
}];
}
Rather than downloading image and create array to populate tableview, you have to just create array of PFObjects and use it with SDWebImage for Asynchronous image downloading without any issue or blocking UI.
I'm guessing that the question is really about not expending effort to download images beyond the scroll position while the visible rows are still being fetched.
The solution to that problem is to load images lazily, when they're needed to configure a cell in cellForRowAtIndexPath:. There's plenty of generic content available about this idea. For a parse-specific solution, see PFImageView here.
The gist is that image view will take care of loading and caching an image from the file. It will do this asynchronously, so there will be a low perceived lag. Just give the file to the image view and let it do the rest...
Your cellForRowAtIndexPath will look something like this:
// just guessing that your "objects" array is the table's datasource
PFObject *object = self.objects[indexPath.row];
PFUser *user = (PFUser *) object[#"user"];
// put a PFImageView in the cell (in this case with a tag==32)
PFImageView *imageView = (PFImageView *)[cell viewWithTag:32];
imageView.image = [UIImage imageNamed:#”placeholder.png”];
imageView.file = [user objectForKey:#"image"]; // assuming this is a file attribute
[imageView loadInBackground];
You have a problem that you try to do order based adding, where your blocks fire asynchronously so it can be in random order.
You should change to a dictionary or any other keyed data structure and use keys for your comments and pics (e.g. use comment id as the key).
Also double check if the callback of the block is executed on the main queue or any serial queue, because if it's not you need to add locks.
I had the same problem, my images were downloaded but not appearing in the order it should, my table view images and the titles were not matching.
To solve that, I created a column at my class in Parse.com that hold exclusively nameForImages, then each downloaded image is saved using this name.
The nameForImages had to be the same used for the column title, for example:
Title = Pepperoni and Four Cheese | nameForImage =
PepperoniAndFourCheese
Title - Muzzarella and Spinach | nameForImage = MuzzarellaAndSpinach
Etc...
This trick fit to solve my problem because the name of the image and the title appearing in the cell were short and had no special caracters.
I hope it helps or light a solution, good luck.
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'm currently working on a PFQueryTableView and trying to get it to populate with data from an array that's pulled from ViewDidLoad. UPDATE: I've moved the function to an NSObject and implemented a singleton to be used across multiple classes in an effort to silo the operation away from the view controller. Below is the updated code:
+ (NSArray *)savedTankArray
{
PFUser *userName = [PFUser currentUser];
NSString *userNameString = [userName objectForKey:#"username"];
PFQuery *query = [[PFQuery alloc] initWithClassName:#"SavedTanks"];
[query whereKey:#"userName" equalTo:userNameString];
[query setValue:#"SavedTanks" forKeyPath:#"parseClassName"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error)
{
if (!error)
{
// The find succeeded.
NSLog(#"Successfully retrieved %lu Tanks.", objects.count);
// Do something with the found objects
for (PFObject *object in objects)
{
NSString *tankNameString = [[NSString alloc] init];
NSString *tankCapacityString = [[NSString alloc] init];
tankNameString = [object valueForKey:#"tankName"];
tankCapacityString = [object valueForKey:#"tankCapacity"];
NSLog(#"%#", tankNameString);
NSLog(#"%#", tankCapacityString);
_savedTankArray = [objects objectAtIndex:0];
}
}
else
{
// Log details of the failure
NSLog(#"Error: %# %#", error, [error userInfo]);
}
}];
NSLog(#"TANK NAME ARRAY: %#", _savedTankArray);
return [_savedTankArray savedTankObjects];
}
While the NSLogs inside of the function work just fine, my problem is a bit expanded now, and I feel as though I'm missing something really simple here.
By the time I get to #"TANK NAME ARRAY: %#"... obviously it's returning null because its outside of the portion that handles the query. This doesn't help me much if I'm trying to bring the data in through another class.
I've tried so much over the past few days and I can't imagine I'm missing something terribly complex. I'm sorry for re-opening this but I can't wrap my head around it at this time.
Any ideas on how I could handle this? I appreciate the help as always.
There may be other trouble, but for sure this line:
tableData = [NSArray arrayWithObjects:objects, nil];
is a mistake. This will create a single-element array whose first element is the array of results. I think you can fix and simplify as:
tableData = objects;
For your question on how to proceed, I think you can carry on in this class the way one would in any table view controller. Answer the table datasource methods by referring to tableData (i.e. it's count for numberOfRowsInSection:, and tableData[indexPath.row] to configure a cellForRowAtIndexPath:, and so on).
New answer for the edited new question:
It appears that the mixup is with calling the asynch service. I'll give two kinds of advice here. First, the simplest possible table-containing view controller that gets its data from an asynch service, and second, a little class that wraps the parse asynch service. First the VC:
// in a vc with a table view .m
#interface MyViewController ()
#property(weak,nonatomic) IBOutlet UITableView *tableView;
#property(strong,nonatomic) NSArray *array; // this class keeps the array
#end
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[ClassThatHandlesMyQuery doQuery:^(NSArray *results) {
self.array = results;
[self.tableView reloadData];
}];
}
See how the query class method in the other class takes a block parameter? This is required because the query happens asynchronously.
// do the normal table view stuff
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.array.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
PFObject *pfObject = self.array[indexPath.row];
cell.textLabel.text = [pfObject valueForKey:#"someStringProperty"];
return cell;
}
That should be pretty much all you need in the vc. Now let's look at your query method. It makes three mistakes: (a) No block parameter to let the caller get the asynch result, (b) it mishandles the array in the query completion block, (c) at the end of the method, it wrongly supposes that a variable _savedTankArray is initialized, in the block. That code appears below the block, but it actually runs before the block runs.\
Let's fix all three problems. First declare a public method:
// ClassThatHandlesMyQuery.h
+ (void) doQuery:(void (^)(NSArray *))completion;
See how it takes a block as param? Now implement:
// ClassThatHandlesMyQuery.m
+ (void) doQuery:(void (^)(NSArray *))completion {
// your query code. let's assume this is fine
PFUser *userName = [PFUser currentUser];
NSString *userNameString = [userName objectForKey:#"username"];
PFQuery *query = [[PFQuery alloc] initWithClassName:#"SavedTanks"];
[query whereKey:#"userName" equalTo:userNameString];
[query setValue:#"SavedTanks" forKeyPath:#"parseClassName"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
// the job is MUCH simpler here than your code supposed.
// log the result for fun
NSLog(#"did we get objects? %#", objects);
// hand it back to the caller
// notice there's no array kept in this class. it's not needed
// and it would be awkward to do it at the class (not instance) level
completion(objects);
} else {
NSLog(#"bad news from parse: %#", error);
completion(nil);
}
}
// this is important
NSLog(#"hi mom!");
// watch your log output. 'hi mom' will appear before either message
// from the block. why is that? because that block runs later
// after the network request completes. but the hi mom NSLog runs
// just before the network request starts. this is why it's wrong to expect
// any variable set in the block to be initialized here
}
Believe it or not, that's it. You should be able to write exactly the mini view controller class and the mini query classes as described here, and see data from parse in a UITableView. I suggest you build something just like this (exactly like this) first just to get going
I am using Parse.com in IOS Application. In that i am using one Class Background which contains another Class values as row like an array.
I want to update an array of those values without using for loop. I want to update with only one Single PFQuery Call.
Class
Image -- id - image (PFFile) - count (integer)
Background -- id - imagesArr (Array)
The Background class contains image ids in imagesArr. I want to update one row in background. Then i need to update all images whose are imagesArr increment their count column in Image Class.
We can do it by using for loop.
like
[bgQuery findObjectinBackground:^(NSArray * imageIds, NSError *error)
{
if(!error)
{
for(int i= 0; i<imageIds.count; i++)
{
NSString *imageId = [imagesIds objectAtIndex:i];
PFQuery *getImageQuery = [PFquery queryWithClassName:#"Image"];
[getImageQuery getObjectWithIdInBackground:imageId withBlock]
// Code for refresh
}
}
}
We can do it like by using for . But i need to execute n number of PFQueries. I felt it leads to slow the application Performance.
Instead of this can we update all images in Background row imagesArr id with one single PFQuery.
Please help me in this issue.
Thanks in advnace.
This is a bit confusing because you keep saying that you are updating with a query. Queries are for retrieving objects.
With that being said, nested objects save automatically. For example
PFObject * object1 = [PFObject objectWithClassName:#"Object1"];
PFObject * object2 = [PFObject objectWithClassName:#"Object2"];
object1[#"object2ref"] = object2;
[object1 save]; // this should save object 2 as well.
If you do this several times and have an array of Object1's
[PFObject saveAllInBackground:arrayOfObject1objects]; // will save all object 1's, and object 2's
In retrieving objects.
PFQuery * query = [PFQuery queryWithClassName:#"Object1];
[query includeKey:#"object2Ref"]
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
// all objects, and object2ref data should be available
}
else {
NSLog(#"Error, %# %#",error,[error userInfo]);
}
}];
Hope this helps, I'm not entirely sure what you're trying to do.
Im very new to iOS and PFQuery and I need your help please
IM trying to store the array of objects obtained form PFQuery into a local NSArray, Im trying to do it inside if (!error) but it does not leave the block, once the block terminates so does the values for it on my array.
//InstallersDirectory.m
#interface InstallersDirectoryTVC ()
#property (nonatomic, strong) NSArray *supervisors;
#end
//more code goes here
- (void)viewDidLoad
{
[super viewDidLoad];
PFQuery *query = [PFQuery queryWithClassName:#"InstallersInfo"];
[query whereKey:#"supervisor" equalTo:#"yes"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
for (PFObject *object in objects) {
self.supervisors = [objects valueForKey:#"supervisor"];
}
}else {
NSLog(#"Error, %# %#",error,[error userInfo]);
}
}];
Everything works inside the block, like self.supervisors.count or NSLog, but it doesn't leave the block at all. Could you please tell me how I can get those values store definitely to self.supervisors?
Thanks !!
edit | comment
If you still need help on this issue, here is a suggestion or two:
first, self.supervisors is a NSArray. As a NSArray it has to be fully initialized and populated at creation time. So, even though you are iterating through your results, you are creating a new array each time which means only the last iteration would likely be stored in self.supervisors.
try this instead,
#property (nonatomic,strong) NSMutableArray *supervisors;
in your block:
[self.supervisors addObject:object];
//note: this will put the entire returned object in your mutable array
then later outside your block:
to list all the supervisors:
for (PFObject *supervisor in self.supervisors) {
NSLog(#"supervisor info:%#",supervisor);
}
To get the 4th supervisor in the list:
PFObject *superVisorNumber4 = [self.supervisors objectAtIndex:4];
To get the phone number from that supervisor (making this up :)
NSString *phone = [superVisorNumber4 objectForKey:#"phone"];
hope that helps