Very straight forward question here; I'm using a UICollectionView and looking to display a certain number of images per section determined by a count of the images that match the section.
For example:
Category: x
# Images in Category: y
# Images In Section: y
I've got sorting everything into the categories down, and I can get the count for each category, but I'm not sure how to return the correct value for each image in the "Category" section. This is what I've got so far:
- (NSInteger) collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
// Category Delineater Here
for (int i = 0; i <= _categoryCount.count; i++)
{
PFQuery *tiredQuery = [[PFQuery alloc] initWithClassName:#"savedImages"];
[tiredQuery whereKey:#"imageCategory" equalTo:[_categoryCount objectAtIndex:0]];
[tiredQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error)
{
if (!error)
{
NSLog(#"What does this return? %#", objects);
}
else
{
NSLog(#"This returns an error: %#", error);
}
}];
}
return _extractedImagesArray.count;
}
Currently, _extractedImagesArray.count is the total number of images that are downloaded. How to I change the amount returned in this section based on the number of images a particular category has?
EDIT: For Clarification of data structure:
Each image has a category affixed to it. The images are all placed in the same Class on Parse.
My ultimate goal is to get it to look similar to this ([x] representing an image in this case):
Category 1
X X X X X
Category 2
X X X
Category 3
X X X X
etc etc
This line looks like a problem:
[tiredQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error)
The method you're using runs asynchronously, so you're not going to get the results back right away. -collectionView:numberOfItemsInSection: isn't going wait around for you to find the objects asynchronously and then count them -- you need to return the number of objects immediately.
It sounds like you'll probably need to do some processing ahead of time to determine the number of objects in each section, and then remember the counts somewhere so that you can provide them when asked by the collection view.
Looks like you are using Parse.
You should not make a call to the backend while reloading data. Try something like this:
- (void)getImages {
for (int i = 0; i <= _categoryCount.count; i++) {
PFQuery *tiredQuery = [[PFQuery alloc] initWithClassName:#"savedImages"];
[tiredQuery whereKey:#"imageCategory" equalTo:[_categoryCount objectAtIndex:0]];
[tiredQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
_extractedImagesArray = objects;
[collectionView reloadData];
}
else {
NSLog(#"This returns an error: %# %#", error, [error userInfo]);
}
}];
}
}
- (NSInteger) collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return _extractedImagesArray.count;
}
Here is an example of returning different amounts of items dependent no which section you are referencing. Remember, section 0 is always the first one.
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
switch (section) {
case 0:
return xNumber;
break;
case 1:
return yNumber;
break;
case 2:
return zNumber;
}
}
Related
Sorry for the poorly worded title. I have a logic issue that I'm trying to get my head around. The view that I'm working in has a UICollectionView that displays a list of "tanks" associated with a user. This collection view displays a three items:
Tank Name
Tank Capacity
Last Image Stored
The last image stored part is where I'm having trouble. I'm making progress but its a matter of the logic behind it that I'm not sure on. Here is what the data looks like:
I have two classes that I'm interacting with; SavedTanks and SavedTankImages. The unique objectId from a saved tank is also stored as a value in SavedTankImages to allow a sort of pointer reference to the image. This logic works when the user loads a "tank" and can see all of the images they've stored associated with it.
However, for the purposes of this view, I only need to grab the first image from each tank and display that. This is where I need help. Here's what I have so far:
#pragma mark COLLECTION VIEW
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
photoHandler *cell = (photoHandler *)[collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
_tankNameArray = [_array objectAtIndex:indexPath.section * 1 + indexPath.row];
cell.tankNameLabel.text = [_tankNameArray valueForKey:#"tankName"];
cell.tankCapLabel.text = [_tankNameArray valueForKey:#"tankCapacity"];
NSArray *objectId = [_array valueForKey:#"objectId"];
for (int i = 0; i < objectId.count; i++)
{
NSString *objectString = [[NSString alloc] init];
objectString = [objectId objectAtIndex:i];
PFQuery *imageQuery = [PFQuery queryWithClassName:#"SavedTankImages"];
[imageQuery whereKey:#"tankObjectId" equalTo:objectString];
[imageQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error)
{
for (PFObject *object in objects)
{
NSLog(#"OBJECT TEST: %#", object);
}
}
}];
}
return cell;
}
On OBJECT TEST: %#, this is the logged output:
2014-05-28 11:59:44.750 ReefTrack[305:60b] OBJECT TEST: <SavedTankImages:U6fRTuRo2c: (null)> {
tankImages = "<PFFile: 0x18a25890>";
tankObjectId = tsz4yvrIAN;
}
SavedTankImages: <x> is the objectId of the individual image, and tankObjectId is the tank the image is associated with. I'm getting close, but I need to know how I can effectively iterate and only grab the first item where tankObjectId matches the original objectId. Please forgive me if this sounds a little convoluted.
Thanks for the help in advance as usual.
UPDATE
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
photoHandler *cell = (photoHandler *)[collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
_tankNameArray = [_array objectAtIndex:indexPath.section * 1 + indexPath.row];
cell.tankNameLabel.text = [_tankNameArray valueForKey:#"tankName"];
cell.tankCapLabel.text = [_tankNameArray valueForKey:#"tankCapacity"];
NSArray *objectId = [_array valueForKey:#"objectId"];
for (int i = 0; i < objectId.count; i++)
{
// NSString *objectString = [[NSString alloc] init];
// objectString = [objectId objectAtIndex:i];
PFQuery *imageQuery = [PFQuery queryWithClassName:#"SavedTankImages"];
[imageQuery whereKey:#"tankObjectId" equalTo:[objectId objectAtIndex:i]];
[imageQuery getFirstObjectInBackgroundWithBlock:^(PFObject *objects, NSError *error)
{
if (!error)
{
PFFile *imageFile = [objects valueForKey:#"tankImages"];
[imageFile getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (!error)
{
cell.parseImage.image = [UIImage imageWithData:data];
}
}];
NSLog(#"Heres your image: %#", objects);
}
}];
}
return cell;
}
The above code selects the first available image and makes it the background for every cell in collectionView. I want to get it so that it only returns the first image for the objectId. In other words
Tank 1 = tank 1 image 1
Tank 2 = tank 2 image 1
Tank 3 = tank 3 image 1
Right now this is what it's doing:
Tank 1 = tank 1 image 1
Tank 2 = tank 1 image 1
Tank 3 = tank 1 image 1
Simple solution, sort and take just the first result. Be aware this isn't as efficient as the next solution:
// sort by createdAt or use updatedAt
[imageQuery orderByDescending:#"createdAt"];
// change from find to getFirst
//[imageQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
[imageQuery getFirstObjectInBackgroundWithBlock:^(PFObject *object, NSError *error) {
if (!error)
{
NSLog(#"OBJECT TEST: %#", object);
}
}];
A more advanced solution if you'll often be querying on this is to store a pointer to the most recent object on the tank. You can either do this in your code or create a Cloud Code method to update it automatically after each SavedTankImages object is saved (it would simply load up the related SavedTanks and set mostRecentImage to point to the saved image, then save the SavedTanks).
If you have done this, then you can just use the include: method to load the mostRecentImage with the SavedTanks.
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 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.
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.
I'm trying to initialize a couple properties fetched from parse.com when the view is loaded so I can do calculation with them. For instance, I declare the following in my header file:
TaskViewController.h
#property (nonatomic, assign) int taskTotalCount;
#property (nonatomic, assign) int taskCompletedCount;
#property (nonatomic, assign) int progressCount;
- (void)CountAndSetTotalTask;
- (void)CountAndSetCompletedCount;
- (void)CalculateProgress;
Then in the implementation, assuming all the other initialization are setup properly and they are called in viewdidload, below are the method implementations:
TaskViewController.m
- (void)CountAndSetCompletedCount {
// Query the tasks objects that are marked completed and count them
PFQuery *query = [PFQuery queryWithClassName:self.parseClassName];
[query whereKey:#"Goal" equalTo:self.tasks];
[query whereKey:#"completed" equalTo:[NSNumber numberWithBool:YES]];
[query countObjectsInBackgroundWithBlock:^(int count, NSError *error) {
if (!error) {
// The count request succeeded. Assign it to taskCompletedCount
self.taskCompletedCount = count;
NSLog(#"total completed tasks for this goal = %d", self.taskCompletedCount);
} else {
NSLog(#"Fail to retrieve task count");
}
}];
}
- (void)CountAndSetTotalTask {
// Count the number of total tasks for this goal
PFQuery *query = [PFQuery queryWithClassName:self.parseClassName];
[query whereKey:#"Goal" equalTo:self.tasks];
[query countObjectsInBackgroundWithBlock:^(int count, NSError *error) {
if (!error) {
// The count request succeeded. Assign it to taskTotalCount
self.taskTotalCount = count;
NSLog(#"total tasks for this goal = %d", self.taskTotalCount);
} else {
NSLog(#"Fail to retrieve task count");
}
}];
}
- (void)CalculateProgress {
int x = self.taskCompletedCount;
int y = self.taskTotalCount;
NSLog(#"the x value is %d", self.taskCompletedCount);
NSLog(#"the y value is %d", self.taskTotalCount);
if (!y==0) {
self.progressCount = ceil(x/y);
} else {
NSLog(#"one number is 0");
}
NSLog(#"The progress count is = %d", self.progressCount);
}
The issue I am encountering is that the taskTotalCount and taskCompletedCount are set correctly and returns different numbers in the first two methods while the NSLog returns 0 for both x and y. Therefore I'm not sure if the third method somehow got loaded before the two properties are set or it's some other issues. Thank you in advance for any pointers.
Assuming you call these three methods like this:
- (void)viewDidLoad {
[super viewDidLoad];
[self CountAndSetCompletedCount];
[self CountAndSetTotalTask];
[self CalculateProgress];
}
then the problem is that the first two methods return immediately while the calls to Parse occur in the background. This means that CalculateProgress is called long before you get back the results from the calls to Parse.
One solution is to just call CountAndSetCompletedCount from viewDidLoad. In its completion handler you then call CountAndSetTotalTask. In its completion handler you finally call CalculateProgress.