16

I have been trying to display large image from server, but I have to display it progressively.

I used subclass of UIView and in that I have taken UIImage object, in which I used NSURLConnection and its delegate methods, I also used

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;

in which I am appending data and converting it to UIImage object, and drawing rect using the drawInRect: method of UIImage.

Everything is working fine, but the problem is, when image is being drawn on context, I cannot click anywhere else on screen until entire image is being drawn on to screen.

Is there any good solution, where I can click anywhere else even if image is being drawn on screen?

Any help will be appreciable.

Edit: Is there any efficient way of drawing image blurry progressively in didReceiveData? so drawInRect does not take too much time to draw. Or If anyone has custom drawRect method which efficiently displays image progressively as data received in didReceiveData.

Emil
  • 7,220
  • 17
  • 76
  • 135
jigneshbrahmkhatri
  • 3,627
  • 2
  • 21
  • 33
  • 2
    How about running the process in background thread ? – Raptor Feb 28 '12 at 07:58
  • I tried running process in background, but it crashes, because we cannot draw image on screen in background thread, it should be in main thread. – jigneshbrahmkhatri Feb 28 '12 at 08:00
  • the problem here is that you're redrawing the image constantly and this prevent the UI to be responsible. What you can do is to "filter" redraw, doing it each 10 or 50 iteration of didReceiveData. – bontoJR Feb 28 '12 at 08:13
  • yes, you are absolutely right, drawing image in didReceiveData so many times, creates issue. Is there any other soution to draw image progressively? – jigneshbrahmkhatri Feb 28 '12 at 08:27
  • If your images are in jpeg format, have you tried using progressive jpegs (http://www.faqs.org/faqs/jpeg-faq/part1/section-11.html)? I have no idea if that will help, but you could try. – Andrew Morton Mar 18 '12 at 20:20
  • Hi Sir, Try using "JImage.h" & "JImage.m" which I posted recently in this page may solve your problem. It will help you download and get image progressively without letting freeze the UI. – Kuldeep Mar 20 '12 at 05:50

11 Answers11

8

I have used NYXImagesKit for something similar, downloading images while not blocking the main thread and showing the image progressively. Ive written a really quick and dirty example to illustrate the basic workings. I load the image in a UITableview to show that it doesn't block the User Interface(Main Thread). You can scroll the tableview while the image is loading. Don't forget to add the correct Frameworks, there are a few. Heres the link to the project on Github:

https://github.com/HubertK/ProgressiveImageDownload

It's really easy to use,create a NYXProgressiveImageView object, set the URL and it will do all the work for you when you call:

loadImageAtURL:

It's a subclass of UIImageView, Works like magic! Here's a link to the developers site:

http://www.cocoaintheshell.com/2012/01/nyximageskit-class-nyxprogressiveimageview/

Hubert Kunnemeyer
  • 2,261
  • 1
  • 15
  • 14
7

I suggest pulling the image data in an asynchronous manner and then applying a correction in order to obtain a valid conversion from partially downloaded NSData to an UIImage:

NSURLRequest *theRequest = [NSURLRequest requestWithURL:
                                           [NSURL URLWithString: imageRequestString]
                                            cachePolicy: NSURLRequestReloadIgnoringCacheData
                                        timeoutInterval: 60.0];

NSURLConnection *theConnection = [[NSURLConnection alloc] initWithRequest: theRequest
                                                                 delegate: self];

if (theConnection)
      receivedData = [[NSMutableData data] retain];

.......

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
       [receivedData appendData: data];

       NSInvocationOperation *operation = 
              [[NSInvocationOperation alloc] initWithTarget: self
                                                   selector: @selector(loadPartialImage)
                                                     object: nil];
       [[[NSOperationQueue alloc] init] autorelease] addOperation: operation];
       [operation release];
}

- (void)loadPartialImage {
       // This is where you would call the function that would "stitch up" your partial
       // data and make it appropriate for use in UIImage's imageWithData
       NSData *validPartialData =
          [self validImageRepresentationFromPartialImageData: receivedData];

       UIImage *partialImage = [UIImage imageWithData: validPartialData];

       [imageView performSelectorOnMainThread: @selector(setImage:)
                                   withObject: partialImage
                                waitUntilDone: NO];
}


+ (void)connectionDidFinishLoading:(NSURLConnection *)connection {
       [connection release];

           UIImage *fullImage = [UIImage imageWithData: receivedData];

           imageView.image = fullImage;
}

Note that I did not provide the code for validImageRepresentationFromPartialImageData, as, at the moment, I have no clear, specific idea, on how to implement such a correction, or if the [UIImage imageWithData:] wouldn't actually accept partial data as input by default. As you can see, the coercion and UIImage creation would happen on a different thread, while the main thread would only display the updates as they come.

If you are receiving too frequent updates and they are still blocking the interface, you could:

a. Make the image requests on a different thread as well. b. Reduce the frequency of the UIImageView's updates, by only calling setImage once in 10 or 100 updates, according to the zise of your image.

luvieere
  • 37,065
  • 18
  • 127
  • 179
  • Thanks for your response. Yes, I am doing "a." option mentioned by you, but I am concerning is there any better way to draw images on screen without blocking the interface. May be a better way to draw images so that it takes few time to draw. – jigneshbrahmkhatri Mar 03 '12 at 06:55
2

I usually use a really simple GCD pattern for async image loading:

  1. Create a GCD queue in which you load the image data form your web server
  2. Set the image data in your main queue

Example:

dispatch_queue_t image_queue = dispatch_queue_create("com.company.app.imageQueue", NULL);
dispatch_queue_t main_queue = dispatch_get_main_queue();

dispatch_async(image_queue, ^{
  NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[record imageURLString]];
  dispatch_async(main_queue, ^{
    [imageView setImage:[UIImage imageWithData:imageData]];
  });
});
dom
  • 11,894
  • 10
  • 51
  • 74
  • Thanks for your answer, but I have problem when image is being drawn in didReceiveData: method, I cannot click anywhere else on screen. and drawing image should be in main thread, as your answer suggest main queue will also be in main thread, so it will hang until image is not being drawn, because didReceiveData: will be called so many times.... – jigneshbrahmkhatri Feb 28 '12 at 08:16
  • @JigneshBrahmkhatri Can't you just collect all the image data first and then draw it to the screen? I guess that's way more efficient – dom Feb 28 '12 at 08:39
  • @JigneshBrahmkhatri Mhh, ok. Maybe you can show a spinner, which indicates a loading progress? I think that's an better approach then blocking your UI thread. – dom Feb 28 '12 at 09:57
  • My first requirement is to show image progressively from server. – jigneshbrahmkhatri Feb 28 '12 at 11:09
  • Can you give me more code? I don't have time to write down your approach from scratch – it would be easier to help you if I had the controller you actually use – dom Feb 28 '12 at 11:14
  • I tried your above code inside didReceiveData and apply dispatch_async(main_queue, ^{    [self setNeedsDisplay]];  }); but this did not work. Same issue. – jigneshbrahmkhatri Feb 28 '12 at 13:07
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/8312/discussion-between-moosgummi-and-jignesh-brahmkhatri) – dom Feb 28 '12 at 15:26
2

Probably didReceiveData is called too often! Just use a NSTimerand update the image regularly in 1-2second steps. That should work more efficiently.

Also you can use performSelectorInBackgroundto convert your NSData to an UIImage; And then call performSelectorOnMainThreadto set the image into the UIImage View. So the converting stuff won't block the main thread.

calimarkus
  • 9,955
  • 2
  • 28
  • 48
  • Thanks for replying, I am using alternate solution as you said, but I need proper solution to get it done, because, I will call 40-50 UIImageViews to load from server and will display it progressively. so this solution will also not work. The problem is, it takes too much time in drawing image on screen. – jigneshbrahmkhatri Mar 02 '12 at 10:17
  • the drawing takes too long!? did you measure that? just reduce the update count like i said. that should work, no? it doesnt matter how much views you load in the end, if you get it right for one of them. just don't redraw on EVERY update from didReceiveData. – calimarkus Mar 02 '12 at 10:22
  • I update redrawing every 20 times, control comes in didReceiveData, but this is for one image, if 100 images are there then control can come anytime and image will be drawn in main thread, that hangs application until image is being drawn fully. – jigneshbrahmkhatri Mar 02 '12 at 12:38
  • ok we need more details here. about what imge sizes are you talking? how many of them will be loaded at once? why do you need progressive display? – calimarkus Mar 02 '12 at 13:54
  • I need progressive display to ensure user that image is being loaded, and This is the first requirement which I cannot leave this requirement. About images, they are coming from server and it can be 10 or 50 – jigneshbrahmkhatri Mar 02 '12 at 15:06
2

Have you considered chopping up your images into smaller chunks on the server, then redrawing whenever a complete chunk has been received? This would give you control over the "progressiveness" of the load and the frequency of redraws by changing the chunk size. Not sure this is the kind of progressive load you're after, though.

shawkinaw
  • 3,190
  • 2
  • 27
  • 30
2

If you have control of the server, split the image into tiles and also create a low res image. Display the low res version first in the lowest layer, and load the tiles on top drawing them as they load?

Steven Veltema
  • 2,140
  • 15
  • 18
2

You can create a subclass of UIImageView with the URL of the image and a startDownload method. It's a basic sample it must be improved.

@property (nonatomic, strong) NSURL *imageURL;
- (void)startDownload;

@implementation ImgeViewSubClass
{
    NSURLConnection *connection; 
    NSMutableData *imageData;

}

The start download method:

- (void)startDownload
{ 
    NSURLRequest *request = [NSURLRequest requestWithURL:imageURL];
    connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
    [connection start];
    imageData = [NSMutableData data];

}

Delegate method from NSURLConnectionDataDelegate

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        @synchronized(imageData)
        {
            [imageData appendData:data];
        }

        // this part must be improved using CGImage instead of UIImage because we are not on main thread
        UIImage *image = [UIImage imageWithData:imageData];
        if (image) {
            [self performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO];
        }
    });

}
Julien
  • 963
  • 5
  • 8
1

The Answer is in ImageIO.framework , its very simple actually

  1. first you create a CGImageSourceRef mySource ,instantiate it using CGImageSourceCreateIncremental() .

  2. setup and start an NSURLConnection with the image Url.

  3. in connection:didReceiveData: , append the received data to your placeholder data , and update the image source by calling

CGImageSourceUpdateData(imageSource, (CFDataRef)imageData, NO);

then load the partially loaded part of the image to your UIImageView

self.image = [UIImage imageWithCGImage:CGImageSourceCreateImageAtIndex(imageSource, 0, nil)];
  1. in connectionDidFinishLoading: finalise by calling

    CGImageSourceUpdateData(imageSource, (CFDataRef)imageData, YES);

    self.image = [UIImage imageWithCGImage:CGImageSourceCreateImageAtIndex(imageSource, 0, nil)];

    CFRelease(imageSource);

    imageData = nil;

here is a sample code i wrote :

https://github.com/mohammedDehairy/MDIncrementalImageView

m.eldehairy
  • 695
  • 8
  • 10
1

Why don't you use ASIHTTPRequest request:

#import "ASIHTTPRequest.h"

This will help to load/draw in background, can perform other task too.

Try this one:

#import "ASIHTTPRequest.h"

[self performSelectorInBackground:@selector(DownLoadImageInBackground:)
   withObject:YOUR IMAGE ARRAY];

-(void) DownLoadImageInBackground:(NSArray *)imgUrlArr1
{
 NSURL * url = [Image URL];
 ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
 [request setDelegate:self];
 [request startAsynchronous];
}

-(void)requestFailed:(ASIHTTPRequest *)request
{
 NSLog(@"URL Fail : %@",request.url);
 NSError *error = [request error];
 // you can give here alert too..
}

-(void)requestFinished:(ASIHTTPRequest *)request
{

///////////  Drawing Code Here////////////////////
NSData *responseData = [request responseData];
UIImage *imgInBackground = [[UIImage alloc] initWithData:responseData];
[imageView setImage: imgInBackground];
}
matsjoyce
  • 5,744
  • 6
  • 31
  • 38
Rohit Wankhede
  • 506
  • 5
  • 15
  • Thanks for your reply, But I want image to be displayed progressively. Your code will display image only after I receive all data. – jigneshbrahmkhatri Mar 02 '12 at 15:01
  • I don't have much experience... i think then you can use web view instead of image view. which have its delegate methods . will work on this to display UIImage to be displayed progressively from server... – Rohit Wankhede Mar 06 '12 at 06:17
0

I am not sure how the other parts of your code(reg this module) is implemented but give the following a try,

Try to use this selector with the run loop mode set to NSDefaultRunLoopMode

[self performSelectorOnMainThread:@selector(processImage:)
                   withObject:objParameters
               waitUntillDone:NO
                        modes:[NSArray arrayWithObject:NSDefaultRunLoopMode]]

This execution will free up your UI interactions, let me know if it helped please.

For more info : APPLE DOCS

Futur
  • 8,444
  • 5
  • 28
  • 34
  • As you have mentioned in the above threads, that you will need to download some where around 40-50 images async, this should help. Let me know abt it, can be a best practice when successful. – Futur Mar 20 '12 at 04:56
0
//JImage.h

#import <Foundation/Foundation.h>


@interface JImage : UIImageView {

    NSURLConnection *connection;

    NSMutableData* data;

    UIActivityIndicatorView *ai;
}

-(void)initWithImageAtURL:(NSURL*)url;  

@property (nonatomic, retain) NSURLConnection *connection;

@property (nonatomic, retain) NSMutableData* data;

@property (nonatomic, retain) UIActivityIndicatorView *ai;

@end



//JImage.m

#import "JImage.h"

@implementation JImage
@synthesize ai,connection, data;

-(void)initWithImageAtURL:(NSURL*)url {


    [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;

    [self setContentMode:UIViewContentModeScaleToFill];

    if (!ai){

        [self setAi:[[UIActivityIndicatorView alloc]   initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]]; 

        [ai startAnimating];

        [ai setFrame:CGRectMake(27.5, 27.5, 20, 20)];

        [ai setColor:[UIColor blackColor]];

        [self addSubview:ai];
    }
    NSURLRequest* request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60];

    connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];    
}


- (void)connection:(NSURLConnection *)theConnection didReceiveData:(NSData *)incrementalData {

   if (data==nil) 
       data = [[NSMutableData alloc] initWithCapacity:5000];

   [data appendData:incrementalData];

   NSNumber *resourceLength = [NSNumber numberWithUnsignedInteger:[data length]];

   NSLog(@"resourceData length: %d", [resourceLength intValue]);

}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error 
{
    NSLog(@"Connection error...");

    [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;

    [ai removeFromSuperview];

}
- (void)connectionDidFinishLoading:(NSURLConnection*)theConnection 
{
    [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;

    [self setImage:[UIImage imageWithData: data]];

    [ai removeFromSuperview];   
}
@end



//Include the definition in your class where you want to use the image
-(UIImageView*)downloadImage:(NSURL*)url:(CGRect)frame {

    JImage *photoImage=[[JImage alloc] init]; 

    photoImage.backgroundColor = [UIColor clearColor]; 

   [photoImage setFrame:frame];

   [photoImage setContentMode:UIViewContentModeScaleToFill]; 

   [photoImage initWithImageAtURL:url];

   return photoImage;
}



//call the function
UIImageView *imagV=[self downloadImage:url :rect]; 

//you can call the downloadImage function in looping statement and subview the returned  imageview. 
//it will help you in lazy loading of images.


//Hope this will help
Kuldeep
  • 2,589
  • 1
  • 18
  • 28
  • Hi Kuldeep, thanks for sharing answer, but this is not the way I want. I want to display image progressively as it retrieves in didReceiveData method. So I want to set image in didReceiveData, which hangs the application till image is set. I want the same functionality without hanging interaction. – jigneshbrahmkhatri Mar 20 '12 at 06:04