im using PFQueryTableViewController from Parse and i noticed when scrolling that images are being repeated. how can i prevent this?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath object:(PFObject *)object {
static NSString *CellIdentifier = #"Item";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
PFObject *photoObject = [object objectForKey:#"toObject"];
PFQuery *query = [PFQuery queryWithClassName:#"Photo"];
[query getObjectInBackgroundWithId:photoObject.objectId block:^(PFObject *gameScore, NSError *error) {
// Do something with the returned PFObject in the gameScore variable.
PFImageView *photo = (PFImageView *)[cell viewWithTag:1];
photo.file = [gameScore objectForKey:#"image"];
[photo loadInBackground:^(UIImage * _Nullable image, NSError * _Nullable error) {
}];
}];
return cell;
}
Your query returns the image asynchronously and places it in the cell object captured in your completion block. There is no guaranty that, when the query returns, you cell object hasn't already been reused by the tableview for another row.
Also, updating UI components in a background thread (which I suspect the completion block will be) is also a source of UI problems.
One way around this could be to capture the index path (instead of the cell) and use tableView.cellForRowAtIndexPath() in the completion block to make sure you're updating the right cell with the image. You should also dispatch this to the main thread in order to avoid conflicting with other updates (such as having the cell you're trying to update be victim to yet another reuse - less probable but not impossible)
Related
I have an array that contains image references such as: <"UIImage: some number>". However, when I try to set the image in my table my app crashes with the following code:
UIImageView *pic = (UIImageView *)[cell viewWithTag:2];
pic.image = [pictures objectAtIndex:indexPath.row];
The method where the code above resides is:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
But when I set the variable "pic" equal to "image" which comes from the following code, everything works. I would use this, but I need to save the images from the Parse database in an array to populate my table:
[file getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (!error) {
image= [UIImage imageWithData: pictureData];
//image is already declared in the implementation.
Any ideas on what I could do?
I am building an app which will query a set of images (thumbnails) from the parse server and then show them in a collection view, similar to how is done on instagram in the users profile page. I created a method which queries the data from the backend successfully :
-(void)queryForTable {
PFQuery *query = [PFQuery queryWithClassName:#"VideoApp"];
NSString * author = [[PFUser currentUser] objectForKey:#"FBName"];
[query whereKey:#"author" equalTo:author];
[query orderByAscending:#"createdAt"];
[query setCachePolicy:kPFCachePolicyNetworkOnly];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
NSLog(#"Successfully retrieved %d objects", objects.count);
[self.collectionView reloadData];
userVideosArray = [[NSMutableArray alloc] initWithCapacity:objects.count];
for (PFObject *object in objects) {
PFFile *thumbnail = [object objectForKey:#"video_thumbnail"];
[thumbnail getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (!error) {
NSLog(#"Fetching image");
[userVideosArray addObject:[UIImage imageWithData:data]];
} else {
NSLog(#"Error: %# %#", error, [error userInfo]);
}
}];
}
}
}];
}
This method successfully retrieves four objects from the back end, and is called in the ViewDidLoad method.
Then in the collection view cellForRowAtIndexPath method I try to set the queried objects images to the UIImageview on the collectionviewcell as follows:
-(UICollectionViewCell*)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
//CollectionViewcellCollectionViewCell * cell = (CollectionViewcellCollectionViewCell*)[collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 70, 70)];
imageView.backgroundColor = [UIColor grayColor];
[cell addSubview:imageView];
PFQuery *query = [PFQuery queryWithClassName:#"VideoApp"];
NSString * info = [[PFUser currentUser] objectForKey:#"FBName"];
[query whereKey:#"author" equalTo:info];
imageView.image = [UIImage imageWithData:[userVideosArray objectAtIndex:indexPath.row]];
return cell;
}
I keep getting an NSException on imageView.image = [UIImage imageWithData:[userVideosArray objectAtIndex:indexPath.row]]; . Not 100 percent sure why. Any ideas?
You are doing this:
1) reloadData: wrong because you did not update any array before doing this. I assume that it is side effect of findObjectsInBackgroundWithBlock which is definitely wrong, because only object which performs reloadData should be responsible for updating data for datasource.
2) initializing userVideosArray with no items (userVideosArray.count == 0). Looking at your error and knowing that cellForItemAtIndexPath is invoked I'm assuming that -collectionView:numberOfItemsInSection: uses other different array to tell the number of items which is wrong, because you are trying to get item from userVideosArray which may have different number of items
3) filling userVideosArray with items in background. Keeping in mind 1 and 2 gives us an answer to your crash. in cellForItemAtIndexPath you are trying to obtain item which is still not loaded
Btw: [cell addSubview:imageView]; will keep adding image views to your cell each time collection view will reuse it
You should update collectionView after you have handled received data, not before. Your call to [self.collectionView reloadData] trickers calls to cellForItemAtIndexPath and my guess is that userVideosArray does not yet contain as many items as you're expecting.
Anyways, the crash. Try this to prevent crashing:
if (indexPath.item < userVideosArray.count)
imageView.image = [UIImage imageWithData:userVideosArray[indexPath.item]];
Btw when using collectionView, I'd recommend using item instead of row, since one collectionView row might contain several items. You know now what you're doing, and it's ok to use row, but later row vs. item terms might get confusing.
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'm trying to access a method block but I have no idea how to:
__block NSString *username;
PFUser *user = [[self.messageData objectAtIndex:indexPath.row] objectForKey:#"author"];
[user fetchIfNeededInBackgroundWithBlock:^(PFObject *object, NSError *error) {
username = [object objectForKey:#"username"];
NSLog(#"%#", username); //returns "bob";
}];
NSLog(#"%#", username); //returns null
How do I access the variable 'username' from this code outside of the block?
Actually you are accessing the variable username outside the block. You are getting null because the block runs in another thread and you set the value after the block finish it's execution. So, your last line has been already executed in main thread while the block was running , so it's value was not set when last line was executed.That's why you are getting null.
fetchIfNeededInBackgroundWithBlock is an asynchronous method. That's why your last NSLog returns null because this it is performed before username was retrieved. So what you want is probably to call some method inside the block to be sure that it executes after you fetched your user data. Something like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyUserCell *userCell = (MyUserCell *)[tableView dequeueReusableCellWithIdentifier:MyUserCellIdentifier];
PFUser *user = [[self.messageData objectAtIndex:indexPath.row] objectForKey:#"author"];
userCell.user = user;
[user fetchIfNeededInBackgroundWithBlock:^(PFObject *object, NSError *error) {
if (object == userCell.user && !error) {
username = [object objectForKey:#"username"];
cell.textLabel.text = userName;
}
}];
}
UPDATE: The answer is updated to for the case when the block is called inside tableView:cellForRowAtIndexPath: method as requested.
NOTE: Here you will probably need a custom cell to store a reference to the current user, because if you are reusing your cells the block callback might be called after the same cell was reused for a different indexPath (so it will have a different user).
I would suggest using NSOperationQueue as presented in WWDC. See this article for reference, it think it would be helpful:
https://stavash.wordpress.com/2012/12/14/advanced-issues-asynchronous-uitableviewcell-content-loading-done-right/
Below is the example of what I do: try it:
Write this below import statement
typedef double (^add_block)(double,double);
Block - Write this in view did load
__block int bx=5;
[self exampleMethodWithBlockType:^(double a,double b){
int ax=2;
//bx=3;
bx=1000;
NSLog(#"AX = %d && BX = %d",ax,bx);
return a+b;
}];
NSLog(#"BX = %d",bx);
Method:
-(void)exampleMethodWithBlockType:(add_block)addFunction {
NSLog(#"Value using block type = %0.2f",addFunction(12.4,7.8));
}