I've been working on this for a couple days now (off & on) and I'm not exactly sure why this isn't working, so I'm askin you pros at SOF for some insight.
NewsItem.m
On my first view controller, I'm reading from a JSON feed which has 10+ items. Each item is represented by a NewsItem view which allows for a title, body copy, and a small image. The UIImageView has an IBOutlet called imageView. I'm loading the image for my imageView asynchronously. When the image is loaded, I'm dispatching a notification called IMAGE_LOADED. This notification is only picked up on the the NewsItemArticle
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//this will start the image loading in bg
dispatch_async(concurrentQueue, ^{
NSData *image = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:self.imageURL]];
//this will set the image when loading is finished
dispatch_async(dispatch_get_main_queue(), ^{
[self.imageView setAlpha:0.0];
self.image = [UIImage imageWithData:image];
[self.imageView setImage:self.image];
[UIView animateWithDuration:0.5 animations:^{
[self.imageView setAlpha:1.0];
}];
if(self)
[[NSNotificationCenter defaultCenter] postNotificationName:IMAGE_LOADED object:self];
});
});
NewsItemArticle.m
When a user taps on a NewsItemView then I load a new controller which is a scroll view of several NewsItemArticle views inside a scrollview. A NewsItemArticle will listen for IMAGE_LOADED and if it is decided the current notification has an image for this particular article, it will use the same image for it's own reference like so:
- (void)handleImageLoaded:(NSNotification *)note
{
if([note.object isEqual:self.cell]) {
// this next line is hanging the app. not sure why.
[self.imageView setImage:self.cell.image];
[self.activityViewIndicator removeFromSuperview];
}
}
So essentially:
I'm using an asynchronous load on my first image reference
I'm using notifications to let other parts of the app know and image was loaded
The app hangs when the existing image is reference to a second UIImageView
If I comment out the suspect line, the app never hangs. As it its, my app hangs until all the images are loaded. My thoughts are:
This is a network threading conflict (not likely)
This is a GPU threading conflict (perhaps during a resize to the container view's size?)
Has anyone seen anything like this before?
For lazy loading of table view images there are few good options available. Can make use of them in your design to save time and avoid efforts to reinvent the wheel.
1. Apple lazy loading code --link
2. SDWebImage --link
SDWebImage will provide you a completion handler/block where you can use the notification mechanism to notify other modules of your application.
Cheers!
Amar.
Related
My app is presently made of a TableViewController, and a ViewController. When a cell in the TableView is selected, the ViewController is pushed, which is the main app. This View Controller previously loads all its UIViews in the main thread, which caused the screen to freeze as the code ran, often leading users to believe it has crashed. To prevent this issue, and improve the user experience, I have changed my code to the following overall format:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self initialiseApp];
}
- (void) initialiseApp {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//Initialising views
imageView = [[UIImageView alloc] initWithImage:[self getImageFromUrl:currentWallpaperURL]];
[imageView setFrame:CGRectMake(0.0, 100, screenWidth, (screenWidth/7)*4)];
[imageView setContentMode:UIViewContentModeScaleAspectFit];
// etc etc for other views
dispatch_async(dispatch_get_main_queue(), ^{
//Add subviews to UI
[[self view] addSubview:imageView];
});
});
}
When app running in the simulator, this causes the ViewController to load as a blank screen, and later load the UI after some time. During loading, I would have some form of spinner, or text on the screen. I would therefore like clarification on this topic:
Is loading an app's UI when the ViewController is opened (or alternatively, when the app is launched) conventional? And if not, what is a better alternative to prevent having the app freeze for 10 seconds on launch?
Thanks.
I tend to experience issues when creating UI elements on background threads, so I would avoid this if possible (Apple says the same). In your situation, rather than loading the UI elements in the background, load the image in the background and then create the UI elements when the image has been loaded. As an example,
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage *image = [self getImageFromUrl:currentWallpaperURL];
dispatch_async(dispatch_get_main_queue(), ^{
//Add subviews to UI
imageView = [[UIImageView alloc] initWithImage:image];
[imageView setFrame:CGRectMake(0.0, 100, screenWidth, (screenWidth/7)*4)];
[imageView setContentMode:UIViewContentModeScaleAspectFit];
[[self view] addSubview:imageView];
});
});
So I have read several posts on here about the queueing system but I cannot seem to figure out how to do what I am looking for. Currently I am going to a page and loading images using a loop, and each image uses async dispatch seen here.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
//Load Image Code Goes Here
dispatch_async(dispatch_get_main_queue(), ^{
//Display Image Code Goes Here After Loading.
});
});
And this works perfectly, however I need to be able to destroy this queue or wait until it is finished before doing anything else. Basically certain pages have dozens and dozens of images, so they all start loading, then I go to a totally separate area in the app and do a completely different image loading (1-2 images) and it will take almost a minute because it is still waiting for the other images to load. Is there a way to destroy my old queue or suspend? I have seen a people say "you can but it will corrupt the incoming data" which is fine because the image would just re download upon a new page load. Any ideas?
A slightly different approach is to only dispatch a few images to start and then dispatch another when any of the previous requests finishes. Here's what the code looks like
- (IBAction)startButtonPressed
{
self.nextImageNumber = 1;
for ( int i = 0; i < 2; i++ )
[self performSelectorOnMainThread:#selector(getImage) withObject:nil waitUntilDone:NO];
}
- (void)getImage
{
if ( self.view.window && self.nextImageNumber <= 6 )
{
int n = self.nextImageNumber++;
NSLog( #"Requesting image %d", n );
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"http://images.apple.com/v/iphone-5s/gallery/a/images/download/photo_%d.jpg", n]];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage *image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:url]];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog( #"Received image %d", n );
[self updateImage:image forView:n];
[self performSelectorOnMainThread:#selector(getImage) withObject:nil waitUntilDone:NO];
});
});
}
}
The images being downloaded are named "photo_1.jpg" through "photo_6.jpg". The process is started by requesting the first two photos. When one of those finishes, the next request is dispatched, and so on until all 6 photos are retrieved. The key line of code is
if ( self.view.window && self.nextImageNumber <= 6 )
The first part of the if checks whether the view controller is still on the screen. When the user navigates away from the view controller, self.view.window will be set to nil. So navigating away from the view controller breaks the chain of downloads.
The second part of the if checks whether all of the downloads are finished. This is easy to do since the filenames contain a number. For random filenames, you could fill an NSArray with the URLs and then index through the array until you reach the end.
I started the chain with 2 downloads because there are only 6 images to download at that URL. Depending on the image sizes and number of images, you might want to start by dispatching more. The tradeoff is to maximize bandwidth usage (by starting with more) versus minimizing cancellation time (by starting with less).
if you are using an NSURLConnection, you should maybe keep references to them outside of the blocks, then if you move away from the screen call [downloadConnection cancel] on each of them
I have an UIView with an UIImageView subview. I need to load an image in the UIImageView without blocking the UI. The blocking call seems to be: UIImage imageNamed:. Here is what I thought solved this problem:
-(void)updateImageViewContent {
dispatch_async(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
UIImage * img = [UIImage imageNamed:#"background.jpg"];
dispatch_sync(dispatch_get_main_queue(), ^{
[[self imageView] setImage:img];
});
});
}
The image is small (150x100).
However the UI is still blocked when loading the image. What am I missing ?
Here is a small code sample that exhibits this behaviour:
Create a new class based on UIImageView, set its user interaction to YES, add two instances in a UIView, and implement its touchesBegan method like this:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if (self.tag == 1) {
self.backgroundColor= [UIColor redColor];
}
else {
dispatch_async(dispatch_get_main_queue(), ^{
[self setImage:[UIImage imageNamed:#"woodenTile.jpg"]];
});
[UIView animateWithDuration:0.25 animations:
^(){[self setFrame:CGRectInset(self.frame, 50, 50)];}];
}
}
Assign the tag 1 to one of these imageViews.
What happens exactly when you tap the two views almost simultaneously, starting with the view that loads an image? Does the UI get blocked because it's waiting for [self setImage:[UIImage imageNamed:#"woodenTile.jpg"]]; to return ? If so, how may I do this asynchronously ?
Here is a project on github with ipmcc code
Use a long press then drag to draw a rectangle around the black squares. As I understand his answer, in theory the white selection rectangle should not be blocked the first time the image is loaded, but it actually is.
Two images are included in the project (one small: woodenTile.jpg and one larger: bois.jpg). The result is the same with both.
Image format
I don't really understand how this is related to the problem I still have with the UI being blocked while the image is loaded for the first time, but PNG images decode without blocking the UI, while JPG images do block the UI.
Chronology of the events
The blocking of the UI begins here..
.. and ends here.
AFNetworking solution
NSURL * url = [ [NSBundle mainBundle]URLForResource:#"bois" withExtension:#"jpg"];
NSURLRequest * request = [NSURLRequest requestWithURL:url];
[self.imageView setImageWithURLRequest:request
placeholderImage:[UIImage imageNamed:#"placeholder.png"]
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
NSLog(#"success: %#", NSStringFromCGSize([image size]));
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
NSLog(#"failure: %#", response);
}];
// this code works. Used to test that url is valid. But it's blocking the UI as expected.
if (false)
if (url) {
[self.imageView setImage: [UIImage imageWithData:[NSData dataWithContentsOfURL:url]]]; }
Most of the time, it logs: success: {512, 512}
It also occasionnaly logs: success: {0, 0}
And sometimes: failure: <NSURLResponse: 0x146c0000> { URL: file:///var/mobile/Appl...
But the image is never changed.
The problem is that UIImage doesn't actually read and decode the image until the first time it's actually used/drawn. To force this work to happen on a background thread, you have to use/draw the image on the background thread before doing the main thread -setImage:. This worked for me:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
UIImage * img = [UIImage imageNamed:#"background.jpg"];
// Make a trivial (1x1) graphics context, and draw the image into it
UIGraphicsBeginImageContext(CGSizeMake(1,1));
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextDrawImage(context, CGRectMake(0, 0, 1, 1), [img CGImage]);
UIGraphicsEndImageContext();
// Now the image will have been loaded and decoded and is ready to rock for the main thread
dispatch_sync(dispatch_get_main_queue(), ^{
[[self imageView] setImage: img];
});
});
EDIT: The UI isn't blocking. You've specifically set it up to use UILongPressGestureRecognizer which waits, by default, a half a second before doing anything. The main thread is still processing events, but nothing is going to happen until that GR times out. If you do this:
longpress.minimumPressDuration = 0.01;
...you'll notice that it gets a lot snappier. The image loading is not the problem here.
EDIT 2: I've looked at the code, as posted to github, running on an iPad 2, and I simply do not get the hiccup you're describing. In fact, it's quite smooth. Here's a screenshot from running the code in the CoreAnimation instrument:
As you can see on the top graph, the FPS goes right up to ~60FPS and stays there throughout the gesture. On the bottom graph, you can see the blip at about 16s which is where the image is first loaded, but you can see that there's not a drop in the frame rate. Just from visual inspection, I see the selection layer intersect, and there's a small, but observable delay between the first intersection and the appearance of the image. As far as I can tell, the background loading code is doing its job as expected.
I wish I could help you more, but I'm just not seeing the problem.
You can use AFNetworking library , in which by importing the category
"UIImageView+AFNetworking.m"
and by using the method as follows :
[YourImageView setImageWithURL:[NSURL URLWithString:#"http://image_to_download_from_serrver.jpg"]
placeholderImage:[UIImage imageNamed:#"static_local_image.png"]
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
//ON success perform
}
failure:NULL];
hope this helps .
I had a very similar issue with my application where I had to download lot of images and along with that my UI was continuously updating. Below is the simple tutorial link which resolved my issue:
NSOperations & NSOperationQueues Tutorial
this is the good way:
-(void)updateImageViewContent {
dispatch_async(dispatch_get_main_queue(), ^{
UIImage * img = [UIImage imageNamed:#"background.jpg"];
[[self imageView] setImage:img];
});
}
Why don't you use third party library like AsyncImageView? Using this, all you have to do is declare your AsyncImageView object and pass the url or image you want to load. An activity indicator will display during the image loading and nothing will block the UI.
-(void)touchesBegan: is called in the main thread. By calling dispatch_async(dispatch_get_main_queue) you just put the block in the queue. This block will be processed by GCD when the queue will be ready (i.e. system is over with processing your touches). That's why you can't see your woodenTile loaded and assigned to self.image until you release your finger and let GCD process all the blocks that have been queued in the main queue.
Replacing :
dispatch_async(dispatch_get_main_queue(), ^{
[self setImage:[UIImage imageNamed:#"woodenTile.jpg"]];
});
by :
[self setImage:[UIImage imageNamed:#"woodenTile.jpg"]];
should solve your issue… at least for the code that exhibits it.
Consider using SDWebImage: it not only downloads and caches the image in the background, but also loads and renders it.
I've used it with good results in a tableview that had large images that were slow to load even after downloaded.
https://github.com/nicklockwood/FXImageView
This is an image view which can handle background loading.
Usage
FXImageView *imageView = [[FXImageView alloc] initWithFrame:CGRectMake(0, 0, 100.0f, 150.0f)];
imageView.contentMode = UIViewContentModeScaleAspectFit;
imageView.asynchronous = YES;
//show placeholder
imageView.processedImage = [UIImage imageNamed:#"placeholder.png"];
//set image with URL. FXImageView will then download and process the image
[imageView setImageWithContentsOfURL:url];
To get an URL for your file you might find the following interesting:
Getting bundle file references / paths at app launch
When you are using AFNetwork in an application, you do not need to use any block for load image because AFNetwork provides solution for it. As below:
#import "UIImageView+AFNetworking.h"
And
Use **setImageWithURL** function of AFNetwork....
Thanks
One way i've implemented it is the Following: (Although i do not know if it's the best one)
At first i create a queue by using Serial Asynchronous or on Parallel Queue
queue = dispatch_queue_create("com.myapp.imageProcessingQueue", DISPATCH_QUEUE_SERIAL);**
or
queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0);
**
Which ever you may find better for your needs.
Afterwards:
dispatch_async( queue, ^{
// Load UImage from URL
// by using ImageWithContentsOfUrl or
UIImage *imagename = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
// Then to set the image it must be done on the main thread
dispatch_sync( dispatch_get_main_queue(), ^{
[page_cover setImage: imagename];
imagename = nil;
});
});
There is a set of methods introduced to UIImage in iOS 15 to decode images and create thumbnails asynchronously on background thread
func prepareForDisplay(completionHandler: (UIImage?) -> Void)
Decodes an image asynchronously and provides a new one for display in views and animations.
func prepareThumbnail(of: CGSize, completionHandler: (UIImage?) -> Void)
Creates a thumbnail image at the specified size asynchronously on a background thread.
You can also use a set of similar synchronous APIs, if you need more control over where you want the decoding to happen, e.g. specific queue:
func preparingForDisplay() -> UIImage?
func preparingThumbnail(of: CGSize) -> UIImage?
First of all this is not a duplicate. I have seen some identical questions but they didn't help me as my problem varies a little bit.
Using the following code i am download the images asynchronously in my project.
{
NSURL *imageURL = [NSURL URLWithString:imageURLString];
[self downloadThumbnails:imageURL];
}
- (void) downloadThumbnails:(NSURL *)finalUrl
{
dispatch_group_async(((RSSChannel *)self.parentParserDelegate).imageDownloadGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *tempData = [NSData dataWithContentsOfURL:finalUrl];
dispatch_async(dispatch_get_main_queue(), ^{
thumbnail = [UIImage imageWithData:tempData];
});
});
}
Due to the logic of the program, i have used the above code in files other than the tableview controller which is showing all the data after getting it from the web service.
PROBLEM: On screen images does not show up until i scroll. The off screen images are refreshed first. What can i do to solve my problem.
Apple's lazy loading project is using scrollViewDidEndDragging and scrollViewDidEndDecelerating to load the images but the project is way too big to understand plus my code is in files other than the tableview controller.
NOTE: Kindly do not recommend third party libraries like SDWebImage etc.
UPDATE: As most of people are unable to get the problem, i must clarify that this problem is not associated with downloading, caching and re-loading the images in tableview. So kindly do not recommend third party libraries. The problem is that images are only showing when the user scrolls the tableview instead of loading the on screen ones.
Thanks in advance
I think what you have to do is:
display some placeholder image in your table cell while the image is being downloaded (otherwise your table will look empty);
when the downloaded image is there, send a refresh message to your table.
For 2, you have two approaches:
easy one: send reloadData to your table view (and check performance of your app);
send your table view the message:
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
Using reloadRowsAtIndexPaths is much better, but it will require you to keep track of which image is associated to which table row.
Keep in mind that if you use Core Data to store your images, then this workflow would be made much much easier by integrating NSFetchedResultController with your table view. See here for an example.
Again another approach would be using KVO:
declare this observe method in ItemsViewCell:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if ([keyPath isEqual:#"thumbnail"]) {
UIImage* newImage = [change objectForKey:NSKeyValueChangeNewKey];
if (newImage != (id)[NSNull null]) {
self.thumbContainer.image = newImage;
[self.thumbContainer setNeedsLayout];
}
}
}
then, when you configure the cell do:
RSSItem *item = [[channel items] objectAtIndex:[indexPath row]];
cell.titleLabel.text = [item title];
cell.thumbContainer.image = [item thumbnail];
[item addObserver:cell forKeyPath:#"thumbnail" options:NSKeyValueObservingOptionNew context:NULL];
By doing this, cell will be notified whenever the given item "thumbnail" keypath changes.
Another necessary change is doing the assignment like this:
self.thumbnail = [UIImage imageWithData:tempData];
(i.e., using self.).
ANOTHER EDIT:
I wanted to download and load the images just like in the LazyTableImages example by Apple. When its not decelerating and dragging, then only onscreen images are loaded, not all images are loaded at once.
I suspect we are talking different problems here.
I thought your issue here was that the downloaded images were not displayed correctly (if you do not scroll the table). This is what I understand from your question and my suggestion fixes that issue.
As to lazy loading, there is some kind of mismatch between what you are doing (downloading the whole feed and then archiving it as a whole) and what you would like (lazy loading). The two things do not match together, so you should rethink what you are doing.
Besides this, if you want lazy loading of images, you could follow these steps:
do not load the image in parser:foundCDATA:, just store the image URL;
start downloading the image in tableView:cellForRowAtIndexPath: (if you know the URL, you can use dataWithContentOfURL as you are doing on a separate thread);
the code I posted above will make the table update when the image is there;
at first, do not worry about scrolling/dragging, just make 1-2-3 work;
once it works, use the scrolling/dragging delegate to prevent the image from being downloaded (point 2) during scrolling/dragging; you can add a flag to your table view and make tableView:cellForRowAtIndexPath: download the image only if the flag says "no scrolling/dragging".
I hope this is enough for you to get to the end result. I will not write code for this, since it is pretty trivial.
PS: if you lazy load the images, your feed will be stored on disk without the images; you could as well remove the CGD group and CGD wait. as I said, there is not way out of this: you cannot do lazy loading and at the same time archive the images with the feed (unless each time you get a new image you archive the whole feed). you should find another way to cache the images.
Try using SDWebImage, it's great for using images from the web in UITableViews and handles most of the work for you.
The best idea is caching the image and use them. I have written the code for table view.
Image on top of Cell
It is a great solution.
Try downloading images using this code,
- (void) downloadThumbnails:(NSURL *)finalUrl
{
NSURLRequest* request = [NSURLRequest requestWithURL:finalUrl];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse * response,
NSData * data, NSError * error)
{
if (!error)
{
thumbnail = [UIImage imageWithData:data];
}
}];
}
In my ViewController, I'm downloading a number of png files off the internet and adding them to UIImageViews, which I add to a UIScrollView.
However, if the user presses the back button in the navigation bar, I noticed that the background thread is still continuing to download those files. Also, I noticed huge memory spikes as a result, and I don't think my objects are properly getting released.
How can I close all threads when the user presses back in the navigation bar?
xcode 4.2.1, ARC
Thanks!
EDIT -- the following is in a for loop
NSURL* url = [[NSURL alloc] initWithString:[#"someurl"]];
NSData* imageData = [[NSData alloc]initWithContentsOfURL:url];
UIImage* image = [[UIImage alloc] initWithData:imageData];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
imageView.frame = aFrame;
[self.screenshots addSubview:imageView]
You shouldn't be doing any downloading in your view controller. That's the source of the problem. Move your data into model classes. Do all of your downloading and data management there. The view controllers are just responsible for pulling data from the model and giving it to the views. View controllers are not responsible for fetching data from the network. As you've discovered, they can vanish at any time.
You don't want to abort downloads and then restart them every time someone moves between screens. You want to let the model classes know that some data is needed (because the user came to a screen), and then you want the model classes to post a notification (or provide a KVO change notification) that lets the view controllers know that new data is read.
You may want to consider NSOperationQueue and add new download operations via. NSURLConnection and add a supply your NSOperationQueue. You can have more then one source downloading at once and cancel all operations at once once you need to. Also you can set things like how many to download at a time.
Try this:
in your for loop, make a condition that would stop the downloads when you don't want them anymore:
declare class variable or properly
BOOL stopDownloading;
set it to false on when you start the downloads:
stopDownloading = NO;
modify your for loop to stop when you set it to NO:
if (!stopDownloading) {
NSURL* url = [[NSURL alloc] initWithString:[#"someurl"]];
NSData* imageData = [[NSData alloc]initWithContentsOfURL:url];
UIImage* image = [[UIImage alloc] initWithData:imageData];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
imageView.frame = aFrame;
[self.screenshots addSubview:imageView];
}
so now, when back button is pressed, set
stopDownloading = YES;
it will stop all further downloads, however due to your setup, i don't immediately see a way to stop the one that is currently downloading (only subsequent ones).
To solve this problem, i would create a NSOperationQueue and add NSOperations for each download.
Or even simpler, use some existing open source solutions like AFNetworking. specifically these two:
AFImageRequestOperation A subclass of AFHTTPRequestOperation for downloading and processing images.
UIImageView+AFNetworking Adds methods to UIImageView for loading remote images asynchronously from a URL.