How can I make sure an instance is not deallocated? - ios

In my app, I am loading images for UITableViewCells asynchronously like this:
-(void) loadImage:(NSString *)urlString cell:(UITableViewCell *)cell {
UIImageView *iv = cell.imageView;
UIImage *image = [imageCache objectForKey:urlString];
if(!image) {
NSURL *url = [NSURL URLWithString:urlString];
image = [UIImage imageWithData: [NSData dataWithContentsOfURL:url]];
if(image) {
[imageCache setObject:image forKey:urlString];
}
}
dispatch_async(dispatch_get_main_queue(), ^{
iv.image = image;
[cell addSubview:iv];
});
}
When the tableview is refreshed before all of the images have finished loading, an exception is thrown on iv.image = image; because iv has been deallocated. What is the best way to make sure I never try to set an image for a deallocated cell? Is there some way to kill any lingering asynchronous loads when the tableview is reloaded?

I would implement this other way around. Try to get an image from cache when you are constructing the cell. If the image is not found trigger on asynchronous download and notify cell or controller (from main thread) that image was downloaded. You can use KVO to connect cells with images cache.

Related

i want to load images from NSData but scrolling not smoothly

i am storing images as base64 NSData in database and getting from Database and display in UICollectionView cellForItemAtIndexPathas cell.bgImageView.imageimages are displaying but scrolling is not smooth and when i am trying to get back from another ViewController its taking too much time to load the view.
CODE
NSData *imageData=[[NSData alloc]initWithBase64EncodedString:itemObj.strImage options:NSDataBase64DecodingIgnoreUnknownCharacters];
cell.bgImageView.image = [UIImage imageWithData:imageData];
Code
-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
ItemViewController *vc = [[ItemViewController alloc]initWithNibName:#"ItemViewController" bundle:nil];
vc.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
Item *itemObject = [[Item alloc]init];
itemObject = [arrayOfItems objectAtIndex:indexPath.item];
vc.strItemName = itemObject.strItemName;
vc.strItemDesc = itemObject.strShortDescription;
vc.strImage = itemObject.strImage;
[self presentViewController:vc animated:YES completion:nil];}
above is the code for next viewController and passing values and iam come back to CollectionView like this
- (void)dismiss{
[self dismissViewControllerAnimated:YES completion:nil];
}
If you are setting image as below,
[UIImage imageWithData:data]
this is synchronous operation, so it will block your main thread until it finishes loading.
THis is the reason why your scroll view is jerky because you blocked the main thread.
Try loading the images in backGround Thread, it will definitely solve ur problem.
Code for loading in backGround thread,
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:#"www.example.com"]];
dispatch_async(dispatch_get_main_queue(), ^{
cell.myImageView.image = image;
});
});
Put your iimage loading code in
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
your code here....
});

Activity Indicator in a list

I am creating a real estate app. I have a screen which displays a listing of all entries with a thumbnail and a little text on the side. These I have loaded from the server when the app launched. Each entry can have up to 5 photos, which I do not pre-load for obvious reasons. My issue is this… when the user selects an entry, the app downloads the larger photos from the server. Depending on circumstances this can take a few seconds. Right now the app just hangs for those few seconds. I don't know of any practical way to use an activity indicator in a list. A header space just seems like wasted space to use only to display"Loading…". Anyone have any ideas on what I can do to let the user know that loading is in progress?
Clarification: Once an entry is selected from the list, I load up another Table View Controller which has the photos in its list of selections. I currently load the photos in the ViewDidLoad using
NSData *myPhoto = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:myURL]];
You can:
Use UIActivityIndicatorView to show a spinning activity indicator in the precise spot where the image will eventually be loaded.
In a separate queue download the image. While the below code uses GCD, it's actually much better to use NSOperationQueue because on a slow network, using GCD can consume all of the available worker threads, detrimentally affecting performance on the app. A NSOperationQueue with a reasonable maxConcurrentOperationCount (such as 4 or 5) is much better.
When the download is complete, dispatch the updating of the UI back to the main queue (e.g. turn off the activity indicator and set the image).
This is sample code from a gallery app that shows how you might do it. This is probably more complicated than you need and might be hard to repurpose via cut-and-paste, but the loadImage method shows the basic elements of the solution.
#interface MyImage : NSObject
#property (nonatomic, strong) NSString *urlString;
#property (nonatomic, strong) UIImageView *imageView;
#property (nonatomic, strong) UIActivityIndicatorView *activityIndicator;
#property (nonatomic, strong) UIView *view;
#property BOOL loading;
#property BOOL loaded;
#end
#implementation MyImage
// I find that I generally can get away with loading images in main queue using Documents
// cache, too, but if your images are not optimized (e.g. are large), or if you're supporting
// older, slower devices, you might not want to use the Documents cache in the main queue if
// you want a smooth UI. If this is the case, change kUseDocumentsCacheInMainQueue to NO and
// then use the Documents cache only in the background thread.
#define kUseDocumentsCacheInMainQueue NO
- (id)init
{
self = [super init];
if (self)
{
_view = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, IMAGE_WIDTH, IMAGE_HEIGHT)];
_imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0.0, 0.0, IMAGE_WIDTH, IMAGE_HEIGHT)];
_imageView.contentMode = UIViewContentModeScaleAspectFill;
_imageView.clipsToBounds = YES;
[_view addSubview:_imageView];
_loading = NO;
_loaded = NO;
}
return self;
}
- (void)loadImage:(dispatch_queue_t)queue
{
if (self.loading)
return;
self.loading = YES;
ThumbnailCache *cache = [ThumbnailCache sharedManager];
if (self.imageView.image == nil)
{
// I've implemented a caching system that stores images in my Documents folder
// as well as, for optimal performance, a NSCache subclass. Whether you go through
// this extra work is up to you
UIImage *imageFromCache = [cache objectForKey:self.urlString useDocumentsCache:kUseDocumentsCacheInMainQueue];
if (imageFromCache)
{
if (self.activityIndicator)
{
[self.activityIndicator stopAnimating];
self.activityIndicator = nil;
}
self.imageView.image = imageFromCache;
self.loading = NO;
self.loaded = YES;
return;
}
// assuming we haven't found it in my cache, then let's see if we need to fire
// up the spinning UIActivityIndicatorView
if (self.activityIndicator == nil)
{
self.activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
self.activityIndicator.center = CGPointMake(self.view.frame.size.width / 2.0, self.view.frame.size.height / 2.0);
[self.view addSubview:self.activityIndicator];
}
[self.activityIndicator startAnimating];
// now, in the background queue, let's retrieve the image
dispatch_async(queue, ^{
if (self.loading)
{
UIImage *image = nil;
// only requery cache for Documents cache if we didn't do so in the main
// queue for small images, doing it in the main queue is fine, but apps
// with larger images, you might do this in this background queue.
if (!kUseDocumentsCacheInMainQueue)
image = [cache objectForKey:self.urlString useDocumentsCache:YES];
// if we haven't gotten the image yet, retrieve it from the remote server
if (!image)
{
NSData *data = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:self.urlString]];
if (data)
{
image = [UIImage imageWithData:data];
// personally, I cache my image to optimize future access ... you might just store in the Documents folder, or whatever
[cache setObject:image forKey:self.urlString data:data];
}
}
// now update the UI in the main queue
dispatch_async(dispatch_get_main_queue(), ^{
if (self.loading)
{
[self.activityIndicator stopAnimating];
self.activityIndicator = nil;
self.imageView.image = image;
self.loading = NO;
self.loaded = YES;
}
});
}
});
}
}
// In my gallery view controller, I make sure to unload images that have scrolled off
// the screen. And because I've cached the images, I can re-retrieve them fairly quickly.
// This sort of logic is critical if you're dealing with *lots* of images and you want
// to be responsible with your memory.
- (void)unloadImage
{
// remove from imageview, but not cache
self.imageView.image = nil;
self.loaded = NO;
self.loading = NO;
}
#end
By the way, if the image you're downloading is in a UIImageView in a UITableViewCell the final update back to the table might want to do something about checking to see if the cell is still on screen (to make sure it wasn't dequeued because the UITableViewCell scrolled off the screen). In that case, the final UI update after successful download of the image might do something like:
dispatch_async(dispatch_get_main_queue(), ^{
// if the cell is visible, then set the image
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
if (cell)
{
cell.imageView.image = image;
}
});
Note, this is using the UITableView method cellForRowAtIndexPath, which should not be confused with the UITableViewController method tableView:cellForRowAtIndexPath.
For one of my projects i used this custom class for UIImageView:
https://github.com/nicklockwood/AsyncImageView
Small tutorial is located here: http://www.markj.net/iphone-asynchronous-table-image/
With just few lines of code i managed to implement asynchronous loading of images, caching etc. Just give it a look.

iPad App Crashes After Loading 30+ Images

Is there a reason the ipad crashes and restarts after loading 30+ images quickly in an image view using this code:
else if([webData isEqualToString:#"ppt33"]){
if(movieisplaying == #"yes"){
UIImage *image = [UIImage imageNamed: #"33.png"];
[imageView setImage:image];
[moviePlayerController stop];
[moviePlayerController.view removeFromSuperview];
movieisplaying = #"no";
}
else{
UIImage *image = [UIImage imageNamed: #"33.png"];
[imageView setImage:image];
}
}
EDIT:
Crash Error - 2012-07-26 13:48:55.172 Stream[5554:707] Received memory warning.
Are all the images and videos in use/visible constantly? From the warning it sounds like you may be exceeding your allotted memory for you app.
This question seems to be related, Received memory warning though it does not detail how to destroy images not in use. I would say try to be smart about when you are loading what image, and then release an image if you are no longer using it. Same goes for the videos. Hope that helps!
EDIT: You could do something like this:
NSMutableArray * imageArray;
imageArray = [[NSMutableArray alloc] init];
UIImage *oneImage = [UIImage imageNamed: #"33.png"];
[imagesArray addObject:oneImage];
[oneImage release];
//Add the remaining images, could probably turn this into a for loop
Alternatively, before you give that a shot, you could try to do [image release] after everything in your else statement that you provided.

showing progress on imageview on customcell on uitableview

I followed the codes here https://stackoverflow.com/a/7212943/711837 to get me started to show an indicator my app trying to download images from a particular website. The scenario is:
i have a tableview with many custom cells, the custom cells has 2 labels and a imageview.
i have a NSURL to download the contents that will fill up the labels and then a separate class that will download the images to be filled into the UIImageView. The code for the spinner and the downloading of images are:
resultArray = [[NSArray alloc] initWithArray:response];
[self downloadImageFromInternet:#"http://static.colourlovers.com/images/shapes/0/7/7090/50x50.png"];
//spinner for the download
spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
spinner.frame = CGRectMake(0, 0, 24, 24);
custcell.accessoryView = spinner;
[spinner startAnimating];
///[spinner release];
[self.thetableView reloadData];
and then i call the [spinner stopAnimating] at the finish downloading method of the class but somehow, the spinners just don't animate, or appear for the matter! am i missing something? or is there somewhere i can refer to? my aim is to show the UIIndicatorView at the place of the UIImageView then after loading, the imageview takes over the same position and this is on every cell.
UPDATED added the methods
-(void) downloadImageFromInternet:(NSString*)urlToImage{
// Create a instance of InternetImage
asynchImage = [[DownloadThumb alloc] initWithUrl:urlToImage];
// Start downloading the image with self as delegate receiver
[asynchImage downloadImage:self];
}
-(void) internetImageReady:(DownloadThumb*)downloadedImage{
// The image has been downloaded. Put the image into the UIImageView
[arrayImg addObject:downloadedImage.Image];
[spinner stopAnimating];
[self.thetableView reloadData];
}
-(void)downloadImage:(id)delegate{
m_Delegate = delegate;
NSURLRequest *imageRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:ImageUrl]
cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:60.0];
imageConnection = [[NSURLConnection alloc] initWithRequest:imageRequest delegate:self];
if(imageConnection)
{
workInProgress = YES;
m_ImageRequestData = [[NSMutableData data] retain];
}
}
You should really initiate the spinner when you create the cell.
Then you should check in cellForRowAtIndexPath if it should be visible or not.
Finally, when you need to display it, you can use [tableView reloadData] to display or remove it.

A follow-up Q for avoid UI getting lagged (IOS)

Now I'm using this code, with a little modification:
if (self._photoPath && !self._photo) {
dispatch_queue_t bg_thread = dispatch_queue_create("com.yourcompany.bg_thread", NULL);
dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_async(bg_thread,^{
NSData *data = [NSData dataWithContentsOfFile:self._photoPath];
if(data != nil) {
dispatch_async(main_queue,^{
self._photo = [UIImage imageWithData:data];
[self.photoButton setImage:[UIImage imageNamed:#"photoButton.png"] forState:UIControlStateNormal];
});
}
});
}
As you can see, in fact after I get that photo I want set it immediately to my "photoButton",
but now, UI got smoothed, but my photoButton's appearance is always black...
What should I do next?
Thanks for your patience.
_______________Updated________
I have 2 viewControllers, A and B.
A is the root viewController, and B is A's child viewController.
In B, there is a button for calling the camera to take a photo.
After user toke a photo, the photo's apperance becomes that photo.
When I push a new B (with no photo) from A,
things goes smoothly.
But when there is an old B with a photo in it.
the animation gets a little stucked, since the following code I guess:
- (void)viewWillAppear:(BOOL) animated {
if (self._photoPath && !self._photo) {
NSData *data = [NSData dataWithContentsOfFile:self._photoPath];
if(data != nil)
self._photo = [UIImage imageWithData:data];
}
[super viewWillApear];
}
But I do need to get that photo before the view is displayed since I need to set that photo to my photoButton's background.
So, is there a way to avoid stucking the view's animation? Cause it really result in bad user experience.
Thanks a lot!
I found the answer.
Just add a line of code to refresh the button.
For me, it is
[self.photoButton setImage:[UIImage imageNamed:#"photoButton.png"] forState:UIControlStateNormal];
NSUInteger p[2] = {1,4};
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:
[NSIndexPath indexPathWithIndexes:p length:2]]
withRowAnimation:UITableViewRowAnimationNone];

Resources