I'm currently working with the Three20 library to implement some caching for an iPhone project. It has been a huge help for speeding up my table views and page loads, however I'm running into a small issue:
I have subclassed TTURLImageResponse to resize/crop images that are retrieved remotely before caching them so that I don't have to resize them each time I get them back from the server/cache. This works very well and the images load quite quickly, however when images are returned from the cache in the Three20 library they are returned synchronously. This results in a noticeable delay when firing off 10-20 image requests that come back from the cache. Since they are coming back synchronously, they are blocking my UI thread and the user is left waiting for a couple seconds before they see the images that came back from the cache.
I have attempted to thread the operation by doing the following:
- (void) setupImages
{
if(imageList != nil &&
[imageList count] > 0)
{
[NSThread detachNewThreadSelector:#selector(fillImages) toTarget:self withObject:nil];
}
else
{
lblMessage.hidden = NO;
lblMessage.text = #"There are no images. Try refreshing the current list.";
lblTitle.text = #"";
lblAuthor.text = #"";
}
}
- (void)fillImages
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
int i=0;
TTURLRequest *request;
for (MyImage *imageObj in imageList)
{
NSString *imageUrl = [[[imageObj thumbnail_lg] image_url] absoluteString];
if(imageUrl != nil)
{
request = [TTURLRequest requestWithURL:imageUrl delegate:self];
MyTTURLImageResponse *responseObj = [[[MyTTURLImageResponse alloc] init] autorelease];
responseObj.index = i;
request.cachePolicy = TTURLRequestCachePolicyDisk;
request.response = responseObj;
request.httpMethod = #"GET";
[request send];
i++;
}
}
[pool release];
}
//When the fillImages selector is spawned on its own thread it does not callback to this method, therefore images are never loaded!!
- (void)requestDidFinishLoad:(TTURLRequest*)request
{
MyTTURLImageResponse *response = request.response;
UIImage *loadedImage = response.image;
int imageIndex = response.index;
//this now happens as the image is cached :)
//loadedImage = [loadedImage cropCenterAndScaleImageToSize:CGSizeMake(200, 200)];
[myImageDisplay setImage:loadedImage forIndex:imageIndex];
}
However the threading doesn't appear to be working (the callback is on the main thread...). Does anyone know of a way to retrieve from the Three20 Cache either ASYNCHRONOUSLY or a way of threading the Three20 TTURLRequest so that it's at least doing this in the background?
Related
I have a loadImages method
- (void)loadImages {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//this method loads images from a url, processes them
//and then adds them to a property of its view controller
//#property (nonatomic, strong) NSMutableArray *storedImages;
});
}
When a button is clicked a view enters the screen, and all the images that currently exist in _storedImages are displayed
- (void)displayImages {
for (NSString *link in _storedImages) {
//displayImages
}
}
The problem with this setup is, that if the user clicks the button before all the images are loaded, not all the images are presented on the screen.
Hence, I would like to display an SVProgressHUD if the button is clicked, and the loadImages dispatch_async method is still running.
So, how do I keep track of when this dispatch_async is completed? Because if I know this, then I can display an SVProgressHUD until it is completed.
On a side note, if you know how to load/display the images dynamically that info would be helpful too, i.e. you click the button and then as you see the current images, more images are downloaded and displayed
Thank you from a first time iOS developer!
Ok I found a solution but it is incredibly inefficient, I am sure there's a better way to do this
1. Keep a boolean property doneLoadingImages which is set to NO
2. After the dispatch method finishes, set it to YES
3. In the display images method, have a while (self.doneLoadingImages == NO)
//display progress HUD until all the images a loaded
Keep in mind that NSMutableArray is not thread-safe. You must ensure that you don't try to access it from two threads at once.
Using a boolean to track whether you're still loading images is fine. Make loadImages look like this:
- (void)loadImages {
self.doneLoadingImages = NO;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (1) {
UIImage *image = [self bg_getNextImage];
if (!image)
break;
dispatch_async(dispatch_get_main_queue(), ^{
[self addImage:image];
});
}
dispatch_async(dispatch_get_main_queue(), ^{
[self didFinishLoadingImages];
});
});
}
So we send ourselves addImage: on the main queue for each image. The addImage: method will only be called on the main thread, so it can safely access storedImages:
- (void)addImage:(UIImage *)image {
[self.storedImages addObject:image];
if (storedImagesViewIsVisible) {
[self updateStoredImagesViewWithImage:image];
}
}
We send ourselves didFinishLoadingImages when we've loaded all the images. Here, we can update the doneLoadingImages flag, and hide the progress HUD if necessary:
- (void)didFinishLoadingImages {
self.doneLoadingImages = YES;
if (storedImagesViewIsVisible) {
[self hideProgressHUD];
}
}
Your button action can then check the doneLoadingImages property:
- (IBAction)displayImagesButtonWasTapped:(id)sender {
if (!storedImagesViewIsVisible) {
[self showStoredImagesView];
if (!self.doneLoadingImages) {
[self showProgressHUD];
}
}
}
What I usually do with this kind of problem is basically the following (rough sketch):
- (void)downloadImages:(NSArray*)arrayOfImages{
if([arrayOfImages count] != 0)
{
NSString *urlForImage = [arrayOfImages objectAtIndex:0];
// Start downloading the image
// Image has been downloaded
arrayOfImages = [arrayOfImages removeObjectAtIndex:0];
// Ok let's get the next ones...
[self downloadImages:arrayOfImages];
}
else
{
// Download is complete, use your images..
}
}
You can pass the number of downloads that failed, or even a delegate that will receive the images after.
You put a "note" and this may help, it allows you to display the images as they come down, I store the URLs in an array (here it is strings but you could do array of NSURL).
for(NSString *urlString in imageURLs)
{
// Add a downloading image to the array of images
[storedImages addObject:[UIImage imageNamed:#"downloading.png"]];
// Make a note of the image location in the array
__block int imageLocation = [storedImages count] - 1;
// Setup the request
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[request setTimeoutInterval: 10.0];
request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue currentQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
// Check the result
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if (data != nil && error == nil && [httpResponse statusCode] == 200)
{
storedImages[imageLocation] = [UIImage imageWithData:data];
[self reloadImages];
}
else
{
// There was an error
recommendedThumbs[imageLocation] = [UIImageimageNamed:#"noimage.png"];
[self reloadImages]
}
}];
}
You then need another method which reloads the display. If the images are in a table then [[tableview] reloaddata];
-(void)reloadImages
I have an app that needs to download a sound file in order to work correctly.
I am using NSURLConnection asynchronously to download my file which is over 20Mb in size.
I placed a progressBarView in order to track the percentage of the downloading and I am using the delegate methods of NSUrlConnection as suggested by Apple.
NSURLRequest *theRequest=[NSURLRequest requestWithURL:soundFileURL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:20.0];
// create the connection with the request
// and start loading the data
NSURLConnection *theConnection;
theConnection = [[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
//[theConnection cancel];
//[theConnection start];
if (theConnection) {
// Create the NSMutableData that will hold
// the received data
// receivedData is declared as a method instance elsewhere
receivedData=[[NSMutableData data] retain];
} else {
// inform the user that the download could not be made
}
and delegate methods
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
[receivedData appendData:data];
}
and So ...
When I start downloading, the interface hangs from time to time and the progressView hangs as well.
One notable thing, and maybe another question:
I disabled the user interface so the user has to wait until the download finishes and, of course, I give him a message to tell him that.
Would Apple reject my app for that? I am terribly worried about that
Thank you for reading my question :)
NSUrlConnection send events of NSURLConnectionDelegate to main thread by default. You should create new pool and runloop for this connection and be sure that it processed in background. Here are the example of downloading images in background. It using modified NSOperationQueue and NSOperation but you can easy modify it for downloading your file. LinkedImageFetcher on developer.apple.com
//1 First allocate NSOperationQueue object and set number of concurrent operations to execute at a time
NSOperationQueue *thumbnailQueue = [[NSOperationQueue alloc] init];
thumbnailQueue.maxConcurrentOperationCount = 3;
// load photo images in the background
__weak BHCollectionViewController *weakSelf = self;
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
UIImage *image = [photo image];
dispatch_async(dispatch_get_main_queue(), ^{
// then set them via the main queue if the cell is still visible.
cell.imageView.image = image;
}
});
}];
operation.queuePriority = (indexPath.item == 0) ?
NSOperationQueuePriorityHigh : NSOperationQueuePriorityNormal;
[thumbnailQueue addOperation:operation];
Create Photo class of NSObject and add below method
- (UIImage *)image
{
if (!_image && self.imageURL) {
NSData *imageData = [NSData dataWithContentsOfURL:self.imageURL];
UIImage *image = [UIImage imageWithData:imageData scale:[UIScreen mainScreen].scale];
_image = image;
}
return _image;
}
I want to perform a image upload while the running application at the background. I am able to upload the image to the server using the code on this link.
How can I upload a photo to a server with the iPhone?
I heard the NSUrlConnection can be asynchronous and it was used in the EPUploader. In my code, I add some extra method that will create a file in the application directory used for the EPUploader. During this creation of the file, I don't want it to create on the main thread of the application so I wrap all the code including the EPUploader itself with the
dispatch_async on the global queue. That way I won't block the main thread while the file are creating.
It has no problem if I use dispatch_sync but dispatch_async I find something weird when I placed the breakpoint at NSUrlConnection connection :
- (void)upload
{
NSData *data = [NSData dataWithContentsOfFile:filePath];
//ASSERT(data);
if (!data) {
[self uploadSucceeded:NO];
return;
}
if ([data length] == 0) {
// There's no data, treat this the same as no file.
[self uploadSucceeded:YES];
return;
} /* blah blah */
NSURLConnection * connection = [[NSURLConnection alloc] initWithRequest:urlRequest delegate:self];
if (!connection) {
[self uploadSucceeded:NO];
return;
}
else
return;
I went to debug at the breakpoint and instead of going to if statement, the debugger jumps to the first return statement of this method. After that, the selectors I passed on to this class never gets called. This happen only on dispatch_async and it work on dispatch_sync on the global queue.
Does anybody know how to solve this issue?
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
dispatch_async(queue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
self.uploadIndex = 0;
ALAsset *asset = [self.assets objectAtIndex:0];
[[FileUploader alloc] initWithAsset:[NSURL URLWithString:#"http://192.168.0.3:4159/default.aspx"]
asset:asset
delegate:self
doneSelector:#selector(onUploadDone:)
errorSelector:#selector(onUploadError:)];
//[self singleUpload:self.uploadIndex];
[pool release];
});
There are a couple of things that should be changed.
Remove the NSAutoreleasePool, it is not needed.
Copy the block to the heap because it's life will probably exceed that of the calling code.
Example:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
dispatch_async(queue, [[^{
self.uploadIndex = 0;
ALAsset *asset = [self.assets objectAtIndex:0];
[[FileUploader alloc] initWithAsset:[NSURL URLWithString:#"http://192.168.0.3:4159/default.aspx"]
asset:asset
delegate:self
doneSelector:#selector(onUploadDone:)
errorSelector:#selector(onUploadError:)];
} copy] autorelease]);
If you are using ARC (which you certainly are since you should be) there is no need for the copy or autorelease.
I have a table view and I'd like to download an icon image (100x75) to each row asynchronously. I've followed many tutorials so far but I can't seem to figure it out. How should I do it?
Does anyone recommend just doing it using the standard NSURLConnection API or should I use one of those classes/libraries that are available online to do so? If so, what do you recommend?
Of course, I need it to be fast, efficient and does not leak. I also don't want the downloading to affect the scrolling.
Thank you!
Two options I can think of:
(1) Use ASIHTTPRequest.
(2) A custom implementation that spawns a thread in which you load the image using a combination of NSURL/NSData. Once the image is loaded, send it to a method on the main UI thread using performSelectorOnMainThread:withObject:.
NSThread *t = [[NSThread alloc] initWithTarget:self selector:#selector(loadImage:) object:nil];
[t start];
[t release];
-(void) updateImage:(id) obj {
// do whatever you need to do
}
-(void) loadImage:(id) obj {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSURL *url = [NSURL URLWithString:#"imageurl"];
NSData *imageData = [[NSData alloc]initWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:imageData];
[imageData release];
[self performSelectorOnMainThread:#selector(updateImage:) withObject:data waitUntilDone:YES];
[pool release];
}
I'd recommend you using ASIHTTPRequest. Its simple and pretty fast.
Here is a link to the documentation - ASIHTTPRequest
EDIT
You need to download images for visible cells only.
Heres a sample:
- (void)loadContentForVisibleCells
{
NSArray *cells = [self.tableView visibleCells];
[cells retain];
for (int i = 0; i < [cells count]; i++)
{
...
// Request should be here
...
}
[cells release];
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
{
if (!decelerate)
{
[self loadContentForVisibleCells];
}
}
Anyway u'll need to code a lot to make things work good and fast.
I'm writing test cases for a wrapper class written around ASIHTTPRequest. For reasons I can't determine, my test cases complete with failure before the ASIHTTPRequest finishes.
Here's how the program flow works.
Start in my test case.
Init my http engine object, instruct it to create a new list
Create the new ASIHTTPRequest object and set it up.
Add the request to an operation queue.
Wait until that queue is empty
Check to see if my delegate methods were called and fail the test if they weren't.
Now, most of the time everything works fine and the test passes, but some of the time it fails because my delegate methods were called AFTER the operation queue returned control to my wait method.
Test Case
// Set my flags to 'NO'
- (void)setUp {
requestDidFinish = NO;
requestDidFail = NO;
}
- (void)testCreateList {
NSString *testList = #"{\"title\": \"This is a list\"}";
JKEngine *engine = [[JKEngine alloc] initWithDelegate:self];
NSString *requestIdentifier = [engine createList:jsonString];
[self waitUntilEngineDone:engine];
NSString *responseString = responseString_;
[engine release];
GHAssertNotNil(requestIdentifier, nil);
GHAssertTrue(requestDidFinish, nil);
GHAssertTrue([responseString hasPrefix:#"{\"CreateOrEditListResult\""], nil);
}
// Puts the test into a holding pattern until the http request is done
- (void)waitUntilEngineDone:(JKEngine *)engine {
[engine waitUntilFinishedRunning];
}
// The delegate method called on successful completion
- (void)requestFinished:(NSString *)requestIdentifier withResponse:(NSString *)response {
NSLog(#"request did finish");
requestDidFinish = YES;
responseIdentifier_ = [requestIdentifier retain];
responseString_ = [response retain];
}
Engine Code
- (NSString *)createList:(NSString *)list {
ASIHTTPRequest *request = [[ASIHTTPRequest alloc] initWithURL:[NSURL URLWithString:url]];
[request addRequestHeader:#"Content-Type" value:kContentType];
[request setRequestMethod:kPOST];
request.delegate = self;
[request appendPostData:[list dataUsingEncoding:NSUTF8StringEncoding]];
NSString *requestIdentifier = [NSString stringWithNewUUID];
[operationQueue_ addOperation:request];
[operationDictionary_ setObject:request forKey:requestIdentifier];
return requestIdentifier;
}
// This is the ASIHTTPRequest delegate method that's called on success
// but it sometimes isn't called until AFTER the operationQueue finishes running
- (void)requestFinished:(ASIHTTPRequest *)request {
DLog([request responseString]);
BOOL canNotifiyDelegate = [self.delegate respondsToSelector:#selector(requestFinished:withResponse:)];
if (canNotifiyDelegate) {
NSArray *keyArray = [operationDictionary_ allKeysForObject:request];
NSString *requestIdentifier = [keyArray objectAtIndex:0];
[operationDictionary_ removeObjectForKey:requestIdentifier];
if ([keyArray count] != 1) {
ALog(#"It looks like a request was added to the operation dictionary multiple times. There's a bug somewhere.", nil);
}
[self.delegate requestFinished:requestIdentifier withResponse:[request responseString]];
}
}
- (void)waitUntilFinishedRunning {
[operationQueue_ waitUntilAllOperationsAreFinished];
}
This is the way ASIHTTPRequest works. Delegate methods are called on the main thread, and calls to delegates do not block the request thread, so it's perfectly possible your delegates will be called after the queue finishes.
ASIHTTPRequest calls delegate methods on the main thread, by default GH-Unit runs its tests on a background thread. I'm still a little hazy on exactly what was going on, but forcing my network tests to run on the main thread fixed the problem.
I implemented the following method in my network test class.
- (BOOL)shouldRunOnMainThread {
return YES;
}