I'm storing 5 PFFiles in an array and using getDataInBackgroundWithBlock to download those files from Parse.
The problem is the order at which they appear in the table view cells is different every time, presumably because the files are download at different speeds due to the different file sizes.
for (PFFile *imageFile in self.imageFiles) {
[imageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
if (!error) {
UIImage *avatar = [UIImage imageWithData:imageData];
[self.avatars addObject:avatar];
cell.userImageView.image = self.avatars[indexPath.row];
}
}];
}
The self.imageFiles array is in the correct order.
How do I ensure that the images downloaded are added to the self.avatars array in the same order as the self.imageFiles?
The question has two parts: (1) explicitly, how to maintain the order of results of asynchronous operations, (2) implied by the use of cell, how to properly handle asynch requests in support of a tableview.
The answer to the first question is simpler: keep the result of the request associated with the parameter for the request.
// change avatars to hold dictionaries associating PFFiles with images
#property(nonatomic,strong) NSMutableArray *avatars;
// initialize it like this
for (PFFile *imageFile in self.imageFiles) {
[avatars addObject:[#{#"pfFile":imageFile} mutableCopy]];
}
// now lets factor an avatar fetch into its own method
- (void)avatarForIndexPath:(NSIndexPath *)indexPath completion:^(UIImage *, NSError *)completion {
// if we fetched already, just return it via the completion block
UIImage *existingImage = self.avatars[indexPath.row][#"image"];
if (existingImage) return completion(existingImage, nil);
PFFile *pfFile = self.avatars[indexPath.row][#"pfFile"];
[pfFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
if (!error) {
UIImage *avatar = [UIImage imageWithData:imageData];
self.avatars[indexPath.row][#"image"] = avatar;
completion(avatar, nil);
} else {
completion(nil, error);
}
}];
}
Okay for part (1). For part 2, your cellForRowAtIndexPath code must recognize that cells are reused. By the time the asynch image fetch happens, the cell you're working on might have scrolled away. Fix this by not referring to the cell in the completion block (only the indexPath).
// somewhere in cellForRowAtIndexPath
// we're ready to setup the cell's image view
UIImage *existingImage = self.avatars[indexPath.row][#"image"];
if (existingImage) {
cell.userImageView.image = existingImage;
} else {
cell.userImageView.image = // you can put a placeholder image here while we do the fetch
[self avatarForIndexPath:indexPath completion:^(UIImage *image, NSError *error) {
// here's the trick that is often missed, don't refer to the cell, instead:
if (!error) {
[tableView reloadRowsAtIndexPaths:#[indexPath]];
}
}];
}
Reloading the row in the completion block will cause cellForRowAtIndexPath to be called again, except on that subsequent call, we'll have an existing image and the cell will get configured immediately.
Whilst danh's answer has answered my question, I did manage to solve it shortly after posting the question. I'm capturing the index of each imageFile and making sure they are added to the self.avatars array in that order.
for (PFFile *imageFile in self.imageFiles) {
NSInteger index = [self.imageFiles indexOfObject:imageFile];
[imageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
if (!error) {
UIImage *avatar = [UIImage imageWithData:imageData];
self.avatars[index] = avatar;
[self.tableView reloadData];
}
}];
}
Then cell.userImageView.image = self.avatars[indexPath.row]; in cellForRowAtIndexPath:
Related
I have added a table view, and I am display image in the cells. I have also added this code:
So that the cells resize depending on the image.
When I launch my app though, I get this : [![enter image description here][1]][1]
And the images do not load untill I start scrolling...If I scroll down half the page then go back to the top, I get this: Which is correct
[![enter image description here][2]][2]
Any ideas? I have researched on google and tried the odd solution for the older versions of Xcode, But nothing seems to work!
Here is the rest of my code from the TableViewController:
Image isn't loaded correctly in cellForRowAtIndexPath delegate method, you're (probably) downloading the image in the background, so cellForRowAtIndexPath is returned before image is ready.
Downloaded image is probably cached somewhere so next time it's loaded properly.
post.downloadImage() better have a callback closure to be called when image was downloaded, to assign the downloaded image into the proper cell.
Keep in mind that user may scroll this cell out of the screen before image is loaded, so you better use a unique id to abort downloaded image assignment if cell has already changed.
Here's an example for a method that downloads an image in the background, then assigns it to the cell -
+ (void)loadImage:(NSString *)imageUrl onComplete:(void(^)(UIImage *image, BOOL loaded, NSString *callIdentifier))callback callIdentifier:(NSString *)callIdentifier {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul), ^{
[self downloadPicture:url onComplete:^(UIImage *image, BOOL loaded) {
dispatch_sync(dispatch_get_main_queue(), ^{
callback(image, loaded, callIdentifier);
});
}];
});
callback([UIImage imageNamed:#"placeholder"], NO, callIdentifier);
}
+ (void)downloadPicture:(NSString *)url saveTo:(NSString *)filePath onComplete:(void (^)(UIImage *image, BOOL loaded))onComplete {
NSError *error = nil;
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:url] options:NSDataReadingMappedAlways error:&error];
if (!error) {
UIImage *image = [UIImage imageWithData:data scale:GTUserPictureScale];
if (onComplete)
onComplete(image, YES);
} else {
NSLog(#"Error loading user picture: %#", [error description]);
if (onComplete)
onComplete([UIImage imageNamed:#"missing"], NO);
}
}
- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// ...
__weak MyClass *wself = self;
self.imageUrl = #"http://...";
[self loadImage:self.imageUrl onComplete:^(UIImage *image, BOOL loaded, NSString *callIdentifier) {
#synchronized(wself) {
if ([callIdentifier isEqualToString:wself.imageUrl]) {
if (loaded) {
wself.image = image;
}
} else
NSLog(#"Expired load image request for id %#", callIdentifier);
}
} callIdentifier:self.imageUrl];
// ...
}
I'm currently using SDWebImage to load pictures for my table cells, using the following code:
[cell.coverImage sd_setImageWithURL:[self.dataInJSONModel.Content[indexPath.row] CoverImage] placeholderImage:[UIImage imageNamed:#"imageplaceholder_general"]];
The problem is when I scroll up and down, the images were inserted into the wrong cells. After reading some post on StackOverflow regarding this issue, I suspect it to be due to that cells are reused when we scroll and hence the asynchonous download of the image may be placed on a cell indexPath that has changed.
Hence I implemented several changes e.g.:
SDWebImageManager *manager = [SDWebImageManager sharedManager];
UIImageView * cellCoverImage = cell.coverImage;
[manager downloadImageWithURL:[self.dataInJSONModel.Content[indexPath.row] CoverImage] options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL * oriURL) {
NSArray *visibleIndexPaths = [tableView indexPathsForVisibleRows];
if ([visibleIndexPaths containsObject:indexPath]) {
cellCoverImage.image = image;
}
}];
Or even to compare URLs:
SDWebImageManager *manager = [SDWebImageManager sharedManager];
UIImageView * cellCoverImage = cell.coverImage;
[manager downloadImageWithURL:[self.dataInJSONModel.Content[indexPath.row] CoverImage] options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL * oriURL) {
if([oriURL isEqual:[self.dataInJSONModel.Content[indexPath.row] CoverImage]])
{
cell.coverImage.image = image;
}
}];
Still the problem persist. Or I might have wrongly programmed it? Found several suggestions online but no concrete solution yet seen.
Need help!
EDIT
I've already made some changes to it but still doesn't work:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NewsFeedCell * cell = [tableView dequeueReusableCellWithIdentifier:#"NewsFeedCell" forIndexPath:indexPath];
if (self.dataInJSONModel)
{
cell.coverImage.image = nil;
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager downloadImageWithURL:[self.dataInJSONModel.Content[indexPath.row] CoverImage] options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL * oriURL) {
if ([cell isEqual:[self.tableView cellForRowAtIndexPath:indexPath]])
{
cell.coverImage.image = image;
}
}];
}
I met the same problem, and tried assign .image = nil, but not work.
Finally, my sloution is to override prepareForReuse in UITableViewCell with cancel operation:
- (void)prepareForReuse
{
[super prepareForReuse];
[_imageView sd_cancelCurrentImageLoad];
}
Posted the question on the SDWebImage Github page and gotten a suggestion from someone who solves my problem! I just override the prepareForReuse method in my cell's implementation file and nullify the image of the affected imageView.
Sample code for future reader:
In my NewsFeedCell.m
- (void) prepareForReuse
{
[super prepareForReuse];
self.coverImage.image = NULL;
}
And this solves the problem! My opened issue at GitHub is https://github.com/rs/SDWebImage/issues/1024, should any of you want to see.
I tried most of these solutions, spent some time fixing this. I got 2 solutions working for me.
When setting image to ImageView in cells stop download.
In cellForRow add this:
cell.imageView.sd_cancelCurrentImageLoad()
and then in cell:
func prepareForReuse() {
imageView.image = UIImage.placeholderImage() // or nill
}
This is not real solutions because your actually stop image download and waste already downloaded data.
Another more elegant solutions is adding extension for UIImageView:
Choose animation which suits you, and try this:
func setImageAnimated(imageUrl:URL, placeholderImage:UIImage) {
self.sd_setImage(with: imageUrl, placeholderImage: placeholderImage , options:SDWebImageOptions.avoidAutoSetImage, completed: { (image, error, cacheType, url) in
if cacheType == SDImageCacheType.none {
UIView.transition(with: self.superview!, duration: 0.2, options: [.transitionCrossDissolve ,.allowUserInteraction, .curveEaseIn], animations: {
self.image = image
}, completion: { (completed) in
})
} else {
self.image = image
}
})
}
You are right in your analysis of the problem, just not executed it quite correctly.
some pseudocode for cellForRowAtIndexPath...
- set the cell.imageView.image to nil to wipe out
previous use of cell image view contents
- get the URL from data source
- initiate asynchronous download with completion ^{
//check the cell is still the correct cell
if ([cell isEqual: [collectionView cellForRowAtIndexPath:indexPath]]) {
cell.imageView.image = image
}
}
A couple of things you are doing wrong
- don't grab a reference to the cell's image view until you know you need it (in the completion block)
- don't check visibleIndexPaths, the indexPath might still be visible but allocated to a different cell (if you have scrolled off then on again for example). The 'cell isEqual' technique I use here should suffice for all cases.
You can also nil out old cell contents for a recycled cell by overriding the cells -prepareForReuse method.
The Obvious error here is that you're not accounting for using Blocks. Essentially, the completion happens on a background thread, and all UI updates must happen on the Main Thread.
The simple solution is;
dispatch_async(dispatch_get_main_queue(), ^{
cell.coverImage.image = image;
});
Further, if you intend to reference the tableView in your completion block, you should use a weak reference to it.
I am using Parse. I have a PFFILE that I am retrieving using a Query. I need to save it, and i found that you normally use saveEventualy. But it doesn't support PFFile. So how can I turn the PFFile into a PFObject? Or else how save the image for offline? That's my code up to now:
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self GetImage];
}
- (void)enteredForeground:(NSNotification*) not
{
[self GetImage];
}
-(void)GetImage
{
PFQuery *query = [PFQuery queryWithClassName:#"Image"];
[query getObjectInBackgroundWithId:#"4tmub1uxVd" block:^(PFObject *imageObject, NSError >*error)
{
if (imageObject) {
PFFile *imageFile = imageObject[#"image"];
[imageFile getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (data) {
UIImage *image = [UIImage imageWithData:data];
if (image) {
self.imageview.image = image;
}
} else {
NSLog(#"Error fetching image file: %#", error);
}
}];
} else {
NSLog(#"Error fetching object: %#", error);
}
}];
}
Parse has recently introduced a new method called local Data Store. It let's you store objects and files, update and retrieve them. Check out the documentation.
Blog Post
Documentation
That doesn't exactly answer your question, but it will achieve what you want it to!
You can't convert a PFFile to a PFObject, but you don't need to. The Image PFObject class you're fetching in the code above has a property, with key image, that represents a PFFile. If you modify this, you'd save the parent object, which would save the updated file alongside it.
I have the following problem. I'm currently building a recipe-based app and use a standard tableview to display the recipes. On the app launch, a file is downloaded from a server which includes recipe data. The data is then entered into CoreData making sure, that it's unique. At the same time, I use SDWebImage in cellForRowAtIndexPath in order to load async. After downloading the images I save the images and save their paths in an Recipe object (it's an NSManagedObject). I also use UIRefreshControl in order to refresh the tableview in case there had been changes on the server (it uses the same mechanism as on app launch).
The issue with the code below is the saveImage function. The line recipe.thumbImage = [NSString...] causes issues with CoreData in that when I refresh the app using the pull-down gesture (thus activating UIRefreshControl), the recipes appear twice in the tableView. If I delete the line, the problems go away.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
...
NSString *imageURL = [#"http://www.example.com" stringByAppendingString:recipe.externalThumbImageURL];
[cell.thumbImageView setImageWithURL:[NSURL URLWithString:imageURL]
placeholderImage:[UIImage imageNamed:#"blankTableviewImage"]
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType) {
if(error) NSLog(#"Error when downloading thumbImage, error: %#", error);
else {
[self saveImage:image forRecipeID:recipe.objectID];
}
}];
}
- (void)saveImage:(UIImage *)image forRecipeID:(NSManagedObjectID *)recipeID
{
if(image) {
ImageHandler *imageHandler = [[ImageHandler alloc] init];
NSError *error;
Recipe *recipe = (Recipe *)[self.recipeDatabase.managedObjectContext existingObjectWithID:recipeID error:&error];
NSString *fileName = [imageHandler getImageName:recipe.externalThumbImageURL];
NSString *localImageDirectory = [imageHandler imageDirectoryPathFromRecipe:recipe];
if (![[NSFileManager defaultManager] fileExistsAtPath:localImageDirectory]) {
[[NSFileManager defaultManager] createDirectoryAtPath:localImageDirectory withIntermediateDirectories:NO attributes:nil error:&error];
}
[imageHandler saveImage:image withFileName:fileName ofType:#"png" inDirectory:localImageDirectory];
recipe.thumbImage = [localImageDirectory stringByAppendingString:[NSString stringWithFormat:#"%#.png", fileName]];
if(error) NSLog(#"Error in saveImage – error: %#", error);
}
}
The function [cell.thumbImageView setImageWithURL:...] is part of SDWebImage and works async as far as I know. And I think that's part of the problem. And I have tried wrapping recipe.thumbImage in a [recipe.managedObjectContext performBlock:^{}] block, but it doesn't help either.
Does anyone have a hint as to what is the cause of the problem? I know that threading with CoreData is tricky and I've tried several things in order to make it work, but nothing has worked so far. Any hints are very much appreciated!
After some additional trial and error and reading I found out that it makes sense to create a child context when you do that kind of async updating. So in the saveImage method I now simply create a child context and wrap the recipe.thumbImage in a [context performBlock:^{}]. As I use UIManagedDocument with CoreData, you have to make sure to write the changes to the file.
Here's the code
- (void)saveImage:(UIImage *)image forRecipeID:(NSManagedObjectID *)recipeID
{
if(image) {
NSError *error;
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
context.parentContext = self.document.managedObjectContext;
Recipe *recipe = (Recipe *)[context existingObjectWithID:recipeID error:&error];
...
[context performBlock:^{
recipe.thumbImage = [localImageDirectory stringByAppendingString:[NSString stringWithFormat:#"%#.png", fileName]];
[context save:nil];
[self.document saveToURL:document.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:nil];
}];
}
}
Here's also a good thread with more comments on multi-threading and core data and here an in-depth post
I might be asking something really easy, but I can't manage to find a tutorial or example that would help me.
I have learned how to retrieve string data from Parse and now I am trying to retrieve an image thinking it would be easier.. but I can't figure it out.
I am trying to load 1 image (that I'll be changing every day) in the UIImageView, retrieving the image from my data browser in Parse.com.
Could somebody help please?
Here is what I've done:
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelector:#selector(retrieveFromParse)];
}
- (void) retrieveFromParse {
PFQuery *retrieveImage = [PFQuery queryWithClassName:#"outfitDay"];
[retrieveImage findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
loadimageArray = [[NSArray alloc] initWithArray:objects];
}
}];
}
I am missing the part where you indicate the UIImageView to load that information.
Thanks in advance!!
You can set image in UIImageView with the help of this code.If you want to set image in imageview from with the help of url you can try this code.For this you have to download SDWebImages library
PFObject *objFollow1 = ["your array of PFObject" objectAtIndex:your index];
PFFile *image = [objFollow1 objectForKey:#"your key"];
NSLog(#"%#",teaserImage.url);
[your imageview setImageWithURL:[NSURL URLWithString:[teaserImage url]]];
And if you don't want to download image form url then you have to convert this PFFile in to NSData and then convert in to UIImage
You can set image using parse by below code...If you are storing image as PFFile....
PFFile *eventImage = [[loadimagesarray objectAtIndex:indexPath.row] objectForKey:#"ProfileImageFile"]; //ProfileImageFile is the name of key you stored the image file
if(eventImage != NULL)
{
[eventImage getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
UIImage *thumbnailImage = [UIImage imageWithData:imageData];
UIImageView *thumbnailImageView = [[UIImageView alloc] initWithImage:thumbnailImage];
cell.yourimageview.image = thumbnailImageView.image;
}];
}
If you want how to retrieve the details and save it in loadimagesarray use below code..
- (void)retrieveDetails
{
PFQuery *query = [PFQuery queryWithClassName:#"outfitDay"];
__block int totalNumberOfEntries = 0;
[query orderByDescending:#"createdAt"];
[query countObjectsInBackgroundWithBlock:^(int number1, NSError *error) {
if (!error) {
// The count request succeeded. Log the count
totalNumberOfEntries = number1;
if (totalNumberOfEntries > [loadimagesarray count])
{
NSLog(#"Retrieving data");
//query.skip=[NSNumber numberWithInt:2300];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
// The find succeeded.
NSLog(#"Successfully retrieved %d chats.", objects.count);
int j=[loadimagesarray count];
if([objects count]>j)
{
[loadimagesarray addObjectsFromArray:objects];
}
}
else
{
// Log details of the failure
NSLog(#"Error: %# %#", error, [error userInfo]);
}
}];
}
} else
{
// The request failed, we'll keep the chatData count?
number1 = [loadimagesarray count];
}
}];
}
Hope it works for you..
Loading an image from parse is easy with the PFImageView class. Below is an example of loading a PFFile from Parse, and showing it in a PFImageView, (or using a local placeholder image if remote file is not found):
PFImageView *profileImageView;
// User thumbnail
PFFile *imageFile = [self.author objectForKey:FIELDNAME_PROFILE_PROFILEPICTUREFILE];
if (imageFile) {
profileImageView.file = imageFile;
[profileImageView loadInBackground];
} else {
profileImageView.image = [UIImage imageNamed:#"AvatarPlaceholder.png"];
}
In your case, you would probably get the image from the array you've just retrieved...
For anyone who needs help! I found the answer to my question.
Found it in this example:
https://parse.com/questions/retrieve-images-from-parse-to-uiimageview
Hope it helps for someone else!!
And thanks to all who took the time to answer my question!