Objective-C: UIImageWriteToSavedPhotosAlbum() + asynchronous = problems - objective-c

Update: This problem was also reported here, with a more detailed treatment of the cause:
UIImageWriteToSavedPhotosAlbum saves only 5 image out of 10. Why?
In my case as well, the error was: "Write busy" - this seems to be an issue related to device speed. There is probably some solution that involves manually handling threading or similar - but, inspired by Tommy's answer below, I serialized the saving of images, and that works around the problem.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Situation:
I'm trying to save a large-ish collection of images to the camera roll using a for-loop (number of images depends on user input - may be 1, 2, 3, or theoretically hundreds). Images are pointed to by an NSSet (though I could just as easily do an NSArray), and the NSSet is set to nil after the for-loop completes.
I'm using UIImageWriteToSavedPhotosAlbum() to save out the images on a detached thread (using NSThread's detachNewThreadSelector:toTarget:withObject: class method), and starting a UIActivityIndicator spinner on the main thread.
Problem:
When I attempt to save out more than ~5, any image after the ~5th will output this error in the log:
*** -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL
For example, let's say I exported 9 images. The for-loop will run through all 9 images (confirmed by NSLogging), but I'll get around 4 iterations of the error above (and only 5 images saved to the camera roll).
If I add a breakpoint in the loop and wait a second or two in between each iteration, however, they are all saved correctly without complaint. So..
Theory:
Based on my logs and observations, UIImageWriteToSavedPhotosAlbum() is clearly running asynchronously and is somehow 'too slow' to keep up with my application.
Is there a simple way to force it to run synchronously (ideally on the main thread)? I've been experimenting with adding reference counts to the images I'm trying to save out, but (1) this feels hacky, and (2) I haven't solved the problem regardless.
Any advice would be great. Thanks!

I use ALAssetsLibrary and a dispatch_queue to make sure that images are saved sequentially:
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
dispatch_queue_t queue = dispatch_queue_create("com.myApp.saveToCameraRoll", NULL);
[images enumerateObjectsUsingBlock:^(UIImage *image, NSUInteger idx, BOOL *stop) {
dispatch_async(queue, ^{
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[library writeImageToSavedPhotosAlbum:image.CGImage metadata:metaData completionBlock:^(NSURL *assetURL, NSError *writeError) {
if (writeError) {
// handle the error
}
else {
if (image == [images lastObject]) {
dispatch_async(dispatch_get_main_queue(), ^{
// perhaps indicate to the user that save has finished
});
}
}
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_release(sema);
});
}];

If you're desperate not to write multiple images at once then you could use UIImageWriteToSavedPhotosAlbum's completion target and selector to implement a form of tail recursion. Something like:
- (void)writeSetToSavedPhotosAlbum:(NSMutableSet *)images
{
if(![images count]) return;
UIImage *imageToSave = [[[images anyObject] retain] autorelease];
[images removeObject:imageToSave];
NSLog(#"I shall now write image %#", imageToSave);
UIImageWriteToSavedPhotosAlbum(
imageToSave,
self,
#selector(writeSetToSavedPhotosAlbum:),
images);
}
EDIT: it may also be worth seeing whether you get the same results with ALAssetsLibrary's -writeImageToSavedPhotosAlbum:orientation:completionBlock:, which takes a block for completion so is even more straightforward to work with.

The docs do mention you'll be notified asynchronously. I wonder if this might work: for each image you want to save create a block operation and add it to main operation queue. Kind of like:
for (UIImage *toSave in imagesToSave)
{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
UIImageWriteToSavedPhotosAlbum(toSave, target, selector, ctx);
}];
}
Might be worth a shot, let me know if it helps!

In this Code Work Save image :
UIImageWriteToSavedPhotosAlbum([self screenshot], nil, nil, nil);
UIAlertView *alert=[[UIAlertView alloc]initWithTitle:#"Save" message:#"Photo saved to album" delegate:self cancelButtonTitle:#"OK" otherButtonTitles:nil, nil];
[alert show];

Related

Incrementing a Variable from an Asynchronous Block in Objective-C

I have run into a bit of a conundrum with a service I am working on in objective-c. The purpose of the service is to parse through a list of core-data entities and download a corresponding image file for each object. The original design of the service was choking my web-server with too many simultaneous download requests. To get around that, I moved the code responsible for executing the download request into a recursive method. The completion handler for each download request will call the method again, thus ensuring that each download will wait for the previous one to complete before dispatching.
Where things get tricky is the code responsible for actually updating my core-data model and the progress indicator view. In the completion handler for the download, before the method recurses, I make an asynchronous call the a block that is responsible for updating the core data and then updating the view to show the progress. That block needs to have a variable to track how many times the block has been executed. In the original code, I could simply have a method-level variable with block scope that would get incremented inside the block. Since the method is recursive now, that strategy no longer works. The method level variable would simply get reset on each recursion. I can't simply pass the variable to the next level either thanks to the async nature of the block calls.
I'm at a total loss here. Can anyone suggest an approach for dealing with this?
Update:
As matt pointed out below, the core issue here is how to control the timing of the requests. After doing some more research, I found out why my original code was not working. As it turns out, the timeout interval starts running as soon as the first task is initiated, and once the time is up, any additional requests would fail. If you know exactly how much time all your requests will take, it is possible to simply increase the timeout on your requests. The better approach however is to use an NSOperationQueue to control when the requests are dispatched. For a great example of how to do this see: https://code-examples.net/en/q/19c5248
If you take this approach, keep in mind that you will have to call the completeOperation() method of each operation you create on the completion handler of the downloadTask.
Some sample code:
-(void) downloadSkuImages:(NSArray *) imagesToDownload onComplete:(void (^)(BOOL update,NSError *error))onComplete
{
[self runSerializedRequests:imagesToDownload progress:weakProgress downloaded:0 index:0 onComplete:onComplete ];
}
-(void)runSerializedRequests:(NSArray *) skuImages progress:(NSProgress *) progress downloaded:(int) totalDownloaded index:(NSUInteger) index onComplete:(void (^)(BOOL update,NSError *error))onComplete
{
int __block downloaded = totalDownloaded;
TotalDownloadProgressBlock totalDownloadProgressBlock = ^BOOL (SkuImageID *skuImageId, NSString *imageFilePath, NSError *error) {
if(error==nil) {
downloaded++;
weakProgress.completedUnitCount = downloaded;
//save change to core-data here
}
else {
downloaded++;
weakProgress.completedUnitCount = downloaded;
[weakSelf setSyncOperationDetail:[NSString stringWithFormat:#"Problem downloading sku image %#",error.localizedDescription]];
}
if(weakProgress.totalUnitCount==weakProgress.completedUnitCount) {
[weakSelf setSyncOperationIndicator:SYNC_INDICATOR_WORKING];
[weakSelf setSyncOperationDetail:#"All product images up to date"];
[weakSelf setSyncOperationStatus:SYNC_STATUS_SUCCESS];
weakProgress.totalUnitCount = 1;
weakProgress.completedUnitCount = 1;
onComplete(false,nil);
return true;
}
return false;
};
NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:nil destination:nil
completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
NSLog(#"finished download %u of %lu", index +1, (unsigned long)skuImages.count);
if(error != nil)
{
NSLog(#"Download failed for URL: %# with error: %#",skuImage.url, error.localizedDescription);
}
else
{
NSLog(#"Download succeeded for URL: %#", skuImage.url);
}
dispatch_async(dispatch_get_main_queue(), ^(void){
totalDownloadProgressBlock(skuImageId, imageFilePath, error);
});
[self runSerializedRequests:manager skuImages:skuImages progress:progress downloaded:downloaded index:index+1 onComplete:onComplete ];
}];
NSLog(#"Starting download %u of %lu", index +1, (unsigned long)skuImages.count);
[downloadTask resume];
}
The original design of the service was choking my web-server with too many simultaneous download requests. To get around that, I moved the code responsible for executing the download request into a recursive method.
But that was never the right way to solve the problem. Use a single persistent custom NSURLSession with your own configuration, and set the configuration's httpMaximumConnectionsPerHost.

Save multiple images quickly in iOS 6 (custom album)

I'm writing an application that will take several images from URL's, turn them into a UIImage and then add them to the photo library and then to the custom album. I don't believe its possible to add them to a custom album without having them in the Camera Roll, so I'm accepting it as impossible (but it would be ideal if this is possible).
My problem is that I'm using the code from this site and it does work, but once it's dealing with larger photos it returns a few as 'Write Busy'. I have successfully got them all to save if I copy the function inside its own completion code and then again inside the next one and so on until 6 (the most I saw it take was 3-4 but I don't know the size of the images and I could get some really big ones) - this has lead to the problem that they weren't all included in the custom album as they error'd at this stage too and there was no block in place to get it to repeat.
I understand that the actual image saving is moved to a background thread (although I don't specifically set this) as my code returns as all done before errors start appearing, but ideally I need to queue up images to be saved on a single background thread so they happen synchronously but do not freeze the UI.
My code looks like this:
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:singleImage]]];
[self.library saveImage:image toAlbum:#"Test Album" withCompletionBlock:^(NSError *error) {
if (error!=nil) {
NSLog(#"Error");
}
}];
I've removed the repetition of the code otherwise the code sample would be very long! It was previously where the NSLog code existed.
For my test sample I am dealing with 25 images, but this could easily be 200 or so, and could be very high resolution, so I need something that's able to reliably do this over and over again without missing several images.
thanks
Rob
I've managed to make it work by stripping out the save image code and moving it into its own function which calls itself recursively on an array on objects, if it fails it re-parses the same image back into the function until it works successfully and will display 'Done' when complete. Because I'm using the completedBlock: from the function to complete the loop, its only running one file save per run.
This is the code I used recursively:
- (void)saveImage {
if(self.thisImage)
{
[self.library saveImage:self.thisImage toAlbum:#"Test Album" withCompletionBlock:^(NSError *error) {
if (error!=nil) {
[self saveImage];
}
else
{
[self.imageData removeObject:self.singleImageData];
NSLog(#"Success!");
self.singleImageData = [self.imageData lastObject];
self.thisImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:self.singleImageData]]];
[self saveImage];
}
}];
}
else
{
self.singleImageData = nil;
self.thisImage = nil;
self.imageData = nil;
self.images = nil;
NSLog(#"Done!");
}
}
To set this up, I originally used an array of UIImages's but this used a lot of memory and was very slow (I was testing up to 400 photos). I found a much better way to do it was to store an NSMutableArray of URL's as NSString's and then perform the NSData GET within the function.
The following code is what sets up the NSMutableArray with data and then calls the function. It also sets the first UIImage into memory and stores it under self.thisImage:
NSEnumerator *e = [allDataArray objectEnumerator];
NSDictionary *object;
while (object = [e nextObject]) {
NSArray *imagesArray = [object objectForKey:#"images"];
NSString *singleImage = [[imagesArray objectAtIndex:0] objectForKey:#"source"];
[self.imageData addObject:singleImage];
}
self.singleImageData = [self.imageData lastObject];
self.thisImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:self.singleImageData]]];
[self saveImage];
This means the rest of the getters for UIImage can be contained in the function and the single instance of UIImage can be monitored. I also log the raw URL into self.singleImageData so that I can remove the correct elements from the array to stop duplication.
These are the variables I used:
self.images = [[NSMutableArray alloc] init];
self.thisImage = [[UIImage alloc] init];
self.imageData = [[NSMutableArray alloc] init];
self.singleImageData = [[NSString alloc] init];
This answer should work for anyone using http://www.touch-code-magazine.com/ios5-saving-photos-in-custom-photo-album-category-for-download/ for iOS 6 (tested on iOS 6.1) and should result in all pictures being saved correctly and without errors.
If saveImage:toAlbum:withCompletionBlock it's using dispatch_async i fear that for i/o operations too many threads are spawned: each write task you trigger is blocked by the previous one (bacause is still doing I/O on the same queue), so gcd will create a new thread (usually dispatch_async on the global_queue is optimized by gcd by using an optimized number of threads).
You should either use semaphores to limit the write operation to a fixed number at the same time or use dispatch_io_ functions that are available from iOS 5 if i'm not mistaken.
There are plenty example on how to do this with both methods.
some on the fly code for giving an idea:
dispatch_semaphore_t aSemaphore = dispatch_semaphore_create(4);
dispatch_queue_t ioQueue = dispatch_queue_create("com.customqueue", NULL);
// dispatch the following block to the ioQueue
// ( for loop with all images )
dispatch_semaphore_wait(aSemaphore , DISPATCH_TIME_FOREVER);
[self.library saveImage:image
toAlbum:#"Test Album"
withCompletionBlock:^(NSError *error){
dispatch_semaphore_signal(aSemaphore);
}];
so every time you will have maximum 4 saveImage:toAlbum, as soon as one completes another one will start.
you have to create a custom queue, like above (the ioQueue) where to dispatch the code that does the for loop on the images, so when the semaphore is waiting the main thread is not blocked.

App crashes when saving UIManagedDocument

I have an application that first loads some data into an UIManagedDocument, then executes saveToURL:forSaveOperation:completionHandler:. Inside the completionHandler block, it does an update of various elements of this database, and when it's done, it does another saving.
Besides that, the app has 3 buttons that reload the data, re-update the data, and delete one entity of the database, respectively. In every button method, the last instruction is a saving as well.
When I run all this in the simulator, all goes smoothly. But in the device doesn't. It constantly crashes. I have observed that, normally, it crashes when pressing the "delete" button, or when reloading or re-updating the database. And it's always in the saveToURL operation.
In my opinion, the problem comes when there are multiple threads saving the database. As the device executes the code slower, maybe multiple savings come at same time and the app can't handle them correctly. Also, sometimes the delete button doesn't delete the entity, and says that doesn't exist (when it does).
I'm totally puzzled with this, and all this saving operations must be done...In fact, if I remove them, the app behaves even more incoherently.
Any suggestions of what could I do to resolve this problem? Thank you very much!
[Edit] Here I post the problematic code. For first loading the data, I use a helper class, with this two methods in particular:
+ (void)loadDataIntoDatabase:(UIManagedDocument *)database
{
[database.managedObjectContext performBlock:^{
// Read from de plist file and fill the database
[database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) {
[DataHelper completeDataOfDatabase:database];
}];
}
+ (void)completeDataOfDatabase:(UIManagedDocument *)database
{
[database.managedObjectContext performBlock:^{
// Read from another plist file and update some parameters of the already existent data (uses NSFetchRequest and works well)
// [database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:nil];
[database updateChangeCount:UIDocumentChangeDone];
}];
}
And in the view, I have 3 action methods, like these:
- (IBAction)deleteButton {
[self.database.managedObjectContext performBlock:^{
NSManagedObject *results = ;// The item to delete
[self.database.managedObjectContext deleteObject:results];
// [self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
[self.database updateChangeCount:UIDocumentChangeDone];
}];
}
- (IBAction)reloadExtraDataButton {
[DataHelper loadDataIntoDatabase:self.database];
// [self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
[self.database updateChangeCount:UIDocumentChangeDone];
}
- (IBAction)refreshDataButton {
[DataHelper completeDataOfDatabase:self.database];
//[self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
[self.database updateChangeCount:UIDocumentChangeDone];
}
[Edit 2] More code: First of all, the initial view executes viewDidLoad this way:
- (void)viewDidLoad{
[super viewDidLoad];
self.database = [DataHelper openDatabaseAndUseBlock:^{
[self setupFetchedResultsController];
}];
}
This is what the setupFetchedResultsController method looks like:
- (void)setupFetchedResultsController
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Some entity name"];
request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES selector:#selector(localizedCaseInsensitiveCompare:)]];
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.database.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
}
Each view of the app (it has tabs) has a different setupFetchedResultsController in order to show the different entities the database contains.
Now, in the helper class, this is the first class method that gets executed, via the viewDidLoad of each view:
+ (UIManagedDocument *)openDatabaseAndUseBlock:(completion_block_t)completionBlock
{
NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:#"Database"];
UIManagedDocument *database = [[UIManagedDocument alloc] initWithFileURL:url];
if (![[NSFileManager defaultManager] fileExistsAtPath:[database.fileURL path]]) {
[database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
[self loadDataIntoDatabase:database];
completionBlock();
}];
} else if (database.documentState == UIDocumentStateClosed) {
// Existe, pero cerrado -> Abrir
[database openWithCompletionHandler:^(BOOL success) {
[self loadDataIntoDatabase:database];
completionBlock();
}];
} else if (database.documentState == UIDocumentStateNormal) {
[self loadDataIntoDatabase:database];
completionBlock();
}
return database;
}
You didn't really provide much code. The only real clue you gave was that you are using multiple threads.
UIManagedDocument has two ManagedObjectContexts (one specified for the main queue, and the other for a private queue), but they still must each only be accessed from within their own thread.
Thus, you must only use managedDocument.managedObjectContext from within the main thread. If you want to use it from another thread, you have to use either performBlock or performBlockAndWait. Similarly, you can never know you are running on the private thread for the parent context, so if you want to do something specifically to the parent, you must use performBlock*.
Finally, you really should not be calling saveToURL, except when you initially create the database. UIManagedDocument will auto-save (in its own time).
If you want to encourage it to save earlier, you can send it updateChangeCount: UIDocumentChangeDone to tell it that it has changes that need to be saved.
EDIT
You should only call saveToURL when you create the file for the very first time. With UIManagedDocument, there is no need to call it again (and it can actually cause some unintended issues).
Basically, when you create the document DO NOT set your iVar until the completion handler executes. Otherwise, you could be using a document in a partial state. In this case, use a helper, like this, in the completion handler.
- (void)_document:(UIManagedDocument*)doc canBeUsed:(BOOL)canBeUsed
{
dispatch_async(dispatch_get_main_queue(), ^{
if (canBeUsed) {
_document = doc;
// Now, the document is ready.
// Fire off a notification, or notify a delegate, and do whatever you
// want... you really should not use the document until it's ready, but
// as long as you leave it nil until it is ready any access will
// just correctly do nothing.
} else {
_document = nil;
// Do whatever you want if the document can not be used.
// Unfortunately, there is no way to get the actual error unless
// you subclass UIManagedDocument and override handleError
}
}];
}
And to initialize your document, something like...
- (id)initializeDocumentWithFileURL:(NSURL *)url
{
if (!url) {
url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:#"Default_Project_Database"];
}
UIManagedDocument *doc = [[UIManagedDocument alloc] initWithFileURL:url];
if (![[NSFileManager defaultManager] fileExistsAtPath:[doc.fileURL path]]) {
// The file does not exist, so we need to create it at the proper URL
[doc saveToURL:doc.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
[self _document:doc canBeUsed:success];
}];
} else if (doc.documentState == UIDocumentStateClosed) {
[doc openWithCompletionHandler:^(BOOL success) {
[self _document:doc canBeUsed:success];
}];
} else {
// You only need this if you allow a UIManagedDocument to be passed
// in to this object -- in which case the code above that initializes
// the <doc> variable will be conditional on what was passed...
BOOL success = doc.documentState == UIDocumentStateNormal;
[self _document:doc canBeUsed:success];
}
}
The "pattern" above is necessary to make sure you do not use the document until it is fully ready for use. Now, that piece of code should be the only time you call saveToURL.
Note that by definition, the document.managedObjectContext is of type NSMainQueueConcurrencyType. Thus, if you know your code is running on the main thread (like all your UI callbacks), you do not have to use performBlock.
However, if you are actually doing loads in the background, consider..
- (void)backgroundLoadDataIntoDocument:(UIManagedDocument*)document
{
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
moc.parentContext = document.managedObjectContext;
[moc performBlock:^{
// Do your loading in here, and shove everything into the local MOC.
// If you are loading a lot of stuff from the 'net (or elsewhere),
// consider doing it in strides, so you deliver objects to the document
// a little at a time instead of all at the end.
// When ready to save, call save on this MOC. It will shove the data up
// into the MOC of the document.
NSrror *error = nil;
if ([moc save:&error]) {
// Probably don't have to synchronize calling updateChangeCount, but I do it anyway...
[document.managedObjectContext performBlockAndWait:^{
[document updateChangeCount:UIDocumentChangeDone];
}];
} else {
// Handle error
}
}];
}
Instead of parenting your background MOC to the mainMOC, you can parent it to the parentContext. Loading and then saving into it will put the changes "above" the main MOC. The main MOC will see those changes the next time it does a fetch operation (note the properties of NSFetchRequest).
NOTE: Some people have reported (and it also appears as a note in Erica Sadun's book), that after the very first saveToURL, you need to close, then open to get everything working right.
EDIT
This is getting really long. If you had more points, I'd suggest a chat. Actually, we can't do it through SO, but we could do it via another medium. I'll try to be brief, but please go back and reread what I posted, and pay careful attention because your code is still violating several tenants.
First, in viewDidLoad(), you are directly assigning your document to the result of calling openDatabaseAndUseBlock. The document is not in a usable state at that time. You do not want the document accessible until the completion handlers fire, which will not happen before openDatabaseAndUseBlock() returns.
Second, only call saveToURL the very first time you create your database (inside openDatabaseAndUseBlock()). Do not use it anywhere else.
Third. Register with the notification center to receive all events (just log them). This will greatly assist your debugging, because you can see what's happening.
Fourth, subclass UIManagedDocument, and override the handleError, and see if it is being called... it's the only way you will see the exact NSError if/when it happens.
3/4 are mainly to help you debug, not necessary for your production code.
I have an appointment, so have to stop now. However, address those issues, and here's on

Correct way to multithread in objective-c?

I have a UITableView which displays images. Every cell has an image and every time a cell loads, I call a selector (from the cellForRowAtIndexPath) in the background like this:
[self performSelectorInBackground:#selector(lazyLoad:) withObject:aArrayOfData];
The only problem is that sometimes I get a crash (because I am changing data in the background while it's trying to be read elsewhere). Here's the error:
*** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <CALayerArray: 0xce1a920> was mutated while being enumerated.'
When updating the data in the background, should I move it to the main selector and change it? Or should I call the #selector() differently?
Thanks!
If you can leave the operation on the main thread and have no lagginess nor problems you are done.
However: Let's assume you've already done that and encounter problems. The answer is: don't modify the array in the lazy load. Switch to the main thread to modify the array. See Brad's answer here:
https://stackoverflow.com/a/8186206/8047
for a way to do it with blocks, so you can send your objects over to the main queue (you should probably also use GCD for the call to the lazy load in the first place, but it's not necessary).
You can use #synchronized blocks to keep the threads from walking over each other. If you do
#synchronized(array)
{
id item = [array objectAtIndex:row];
}
in the main thread and
#synchronized(array)
{
[array addObject:item];
}
in the background, you're guaranteed they won't happen at the same time. (Hopefully you can extrapolate from that to your code—I'm not sure what all you're doing with the array there..)
It seems, though, like you'd have to notify the main thread anyway that you've loaded the data for a cell (via performSelectorOnMainThread:withObject:waitUntilDone:, say), so why not pass the data along, too?
Given the term 'lazy load' I am assuming that means you are pulling your images down from a server. (If the images are local then there is really no need for multithreading).
If you are downloading images off a server I would suggest using something along these lines (using ASIHTTPRequest)
static NSCache *cellCache; //Create a Static cache
if (!cellCache)//If the cache is not initialized initialize it
{
cellCache = [[NSCache alloc] init];
}
NSString *key = imageURL;
//Look in the cache for image matching this url
NSData *imageData = [cellCache objectForKey:key];
if (!imageData)
{
//Set a default image while it's loading
cell.icon.image = [UIImage imageNamed:#"defaultImage.png"];'
//Create an async request to the server to get the image
__unsafe_unretained ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:imageURL]];
//This code will run when the request finishes
[request setCompletionBlock:^{
//Put downloaded image into the cache
[cellCache setObject:[request responseData] forKey:key];
//Display image
cell.icon.image = [UIImage imageWithData:[request responseData]];
}];
[request startAsynchronous];
}
else
{
//Image was found in the cache no need to redownload
cell.icon.image = [UIImage imageWithData:imageData];
}

UITableView Refresh Data

I have a UITableViewController that when opened displays a table of the following object:
class {
NSString *stringVal;
int value;
}
However, whenever this controller opens, I want it to download the data from the internet and display "Connecting..." in the status bar and refresh the stringVal and value of all of the objects. I do this by refreshing the array in the UITableViewController. However, to do this the UI hangs sometimes or even displays "blank" table cells until the operation has ended. I'm doing this in an NSOperationQueue to download the data, but I'm wondering if there's a better way to refresh the data without those weird UI bugs.
EDIT:
the UI no longer displays blank cells. This was because cellForRowAtIndexPath was setting nil values for my cellText. However, it still seems somewhat laggy when tableView.reloadData is called even though I'm using NSOperationQueue.
EDIT2:
Moreover, I have two problems: 1. the scrolling prevents the UI from being updated and 2. when the scrolling does stop and the UI starts to update, it hangs a little bit. A perfect example of what I'm trying to do can be found in the native Mail app when you view a list of folders with their unread count. If you constantly scroll the tableview, the folders unread count will be updated without any hanging at all.
Based on your response in the question comments, it sounds like you are calling [tableView reloadData] from a background thread.
Do not do this. UIKit methods, unless otherwise specified, always need to be called from the main thread. Failing to do so can cause no end of problems, and you are probably seeing one of them.
EDIT: I misread your comment. It sounds like you are not updating the UI from a background thread. But my comments about the architecture (i.e. why are you updating in a background thread AFTER the download has finished?).
You state that "when the data comes back from the server, I call a background operation..." This sounds backwards. Normally you would have your NSURLConnection (or whatever you are using for the download) run on the background thread so as not to block to UI, then call out to the main thread to update the data model and refresh the UI. Alternatively, use an asynchronous NSURLConnection (which manages its own background thread/queue), e.g.:
[NSURLConnection sendAsynchronousRequest:(NSURLRequest *)
requestqueue:(NSOperationQueue *)queue
completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler];
And just make sure to use [NSOperationQueue mainQueue] for the queue.
You can also use GCD, i.e., nested dispatch_async() calls (the outer to a background queue for handling a synchronous connection, the inner on the main queue to handle the connection response).
Finally, I will note that you in principle can update your data model on the background thread and just refresh the UI from the main thread. But this means that you need to take care to make your model code thread-safe, which you are likely to mess up at least a couple times. Since updating the model is probably not a time consuming step, I would just do it on the main thread too.
EDIT:
I am adding an example of how one might use GCD and synchronous requests to accomplish this. Clearly there are many ways to accomplish non-blocking URL requests, and I do not assert that this is the best one. It does, in my opinion, have the virtue of keeping all the code for processing a request in one place, making it easier to read.
The code has plenty of rough edges. For example, creating a custom dispatch queue is not generally necessary. It blindly assumes UTF-8 encoding of the returned web page. And none of the content (save the HTTP error description) is localized. But it does demonstrate how to run non-blocking requests and detect errors (both at the network and HTTP layers). Hope this is helpful.
NSURL *url = [NSURL URLWithString:#"http://www.google.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
dispatch_queue_t netQueue = dispatch_queue_create("com.mycompany.netqueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(netQueue,
^{
// We are on a background thread, so we won't block UI events (or, generally, the main run loop)
NSHTTPURLResponse *response;
NSError *error;
NSData *data = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response
error:&error];
dispatch_async(dispatch_get_main_queue(),
^{
// We are now back on the main thread
UIAlertView *alertView = [[UIAlertView alloc] init];
[alertView addButtonWithTitle:#"OK"];
if (data) {
if ([response statusCode] == 200) {
NSMutableString *body = [[NSMutableString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
[alertView setTitle:#"Success"];
[alertView setMessage:body];
}
else {
[alertView setTitle:#"HTTP Error"];
NSString *status = [NSHTTPURLResponse localizedStringForStatusCode:[response statusCode]];
[alertView setMessage:status];
}
}
else {
[alertView setTitle:#"Error"];
[alertView setMessage:#"Unable to load URL"];
}
[alertView show];
[alertView release];
});
});
dispatch_release(netQueue);
EDIT:
Oh, one more big rough edge. The above code assumes that any HTTP status code != 200 is an error. This is not necessarily the case, but handling this is beyond the scope of this question.