3

I'm trying to read in EXIF data from images in the iOS camera roll using the fantastic code here:

http://blog.codecropper.com/2011/05/getting-metadata-from-images-on-ios/

Unfortunately, on the first attempt to read the data, nil is returned ... but every subsequent attempt works fine.

The author of the blog is aware of this and states a solution, but I just don't understand it at all! I'm new to "blocks" and I'm just not getting it even though I've read: http://thirdcog.eu/pwcblocks/

Can anyone translate for me?

This is the code being used to read in the data:

NSMutableDictionary *imageMetadata = nil;
NSURL *assetURL = [info objectForKey:UIImagePickerControllerReferenceURL];

ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
[library assetForURL:assetURL
resultBlock:^(ALAsset *asset)  {
    NSDictionary *metadata = asset.defaultRepresentation.metadata;
    imageMetadata = [[NSMutableDictionary alloc] initWithDictionary:metadata];
    [self addEntriesFromDictionary:metadata];
}
failureBlock:^(NSError *error) {
}];
[library autorelease];

which is conveniently put into an init method and called like:

NSMutableDictionary *metadata = [[NSMutableDictionary alloc] initWithInfoFromImagePicker:info];

The authors description of the first attempt problem is:

One caveat on using this: because it uses blocks, there’s no guarantee that your imageMetadata dictionary will be populated when this code runs. In some testing I’ve done it sometimes runs the code inside the block even before the [library autorelease] is executed. But the first time you run this, the code inside the block will only run on another cycle of the apps main loop. So, if you need to use this info right away, it’s better to schedule a method on the run queue for later with:

[self performSelectorOnMainThread:SELECTOR withObject:SOME_OBJECT waitUntilDone:NO];

.. and it's this line I'm stuck at! I don't know what to do with it?

Any help greatly appreciated!

Steven Elliott
  • 3,214
  • 5
  • 29
  • 40

2 Answers2

3

Without knowing the library, I can only speculate that the loading is done asynchronously, and is thus not guaranteed to be available after assetForURL:... returns (it's being performed in the background).

The correct solution to this is to place into resultBlock all the code that deals with the result instead of placing it after the call to assetForURL:.... A good way to ensure that you are doing the right thing is to move the declaration of imageMetaData into the block, so that there's no chance of accidentally using it outside the block's scope (it is only inside the scope that this variable is guaranteed to be valid).

I get the impression that the author doesn't quite grok what's going on and is suggesting that by using performSelectorOnMainThread:..., you will give assetForURL:... a chance to complete. Again, without knowing the library, I can only speculate, but this sounds like it might be just a way to reduce the likelihood of reading the variable prematurely, without actually solving the problem. Having said that, calling performSelectorOnMainThread:... from inside the block is a good way to ensure that the processing of the result continues on the main thread, just in case resultBlock is called from another thread within the library. It might look something like this:

[library assetForURL:assetURL
    resultBlock:^(ALAsset *asset)  {
        [self performSelectorOnMainThread:@selector(onAssetRetrieved:)
                               withObject:asset
                            waitUntilDone:NO];
    }
    ...];
...
- (void) onAssetRetrieved:(ALAsset *asset) {
    NSDictionary *metadata = asset.defaultRepresentation.metadata;
    imageMetadata = [[NSMutableDictionary alloc] initWithDictionary:metadata];
    [self addEntriesFromDictionary:metadata];
    // Do whatever you planned to do immediately after the asset was retrieved.
}

EDIT: I didn't know about the dispatch_… functions when I wrote the above. Do something like this instead:

[library assetForURL:assetURL
    resultBlock:^(ALAsset *asset)  {
        dispatch_async(dispatch_get_main_queue(), ^{
            NSDictionary *metadata = asset.defaultRepresentation.metadata;
            imageMetadata = [[NSMutableDictionary alloc] initWithDictionary:metadata];
            [self addEntriesFromDictionary:metadata];
            // Do whatever you planned to do immediately after the asset was retrieved.
        });
    }
    ...];
Marcelo Cantos
  • 181,030
  • 38
  • 327
  • 365
  • Thank you for your answer. I've been trying to make this same code (http://blog.codecropper.com/2011/05/getting-metadata-from-images-on-ios/) more synchronous without much success. I either get stuck in a dead lock or get exceptions due to the fact that the block executes some moments later. I was tempted to try and rewrite it to not use blocks and get more asynchronous execution, but it seems it is built into the actual API call. – Sebastian Dwornik Aug 15 '12 at 03:51
  • @SebastianDwornik: No problem. I should note that my answer was unnecessarily complicated, since I wasn't aware of the dispatch_… functions. Instead of putting downstream code in a separate method invoked via performSelector…, you can simply wrap the code in `dispatch_async(dispatch_get_main_queue(), ^{ … })`. – Marcelo Cantos Aug 15 '12 at 04:02
  • Excuse my noob inexperience with hacking GCD + blocks. Might you kindly elaborate the dispatch_async code alternative with an update to your answer text? As I tried this method in some fashion and it didn't quite work for me. :/ – Sebastian Dwornik Aug 15 '12 at 04:05
3

The thing is that the code in the block is called asynchrnously (that's why blocks can be useful for, even if this is not their only typical usage).

You may think of it as another way to do what is usually done with delegate, namely asynchronouly being told when the data is available.

In the case of your code, the ALAssetsLibrary will make a request to get and decode the EXIF metadata of the asset URL you are requesting, but the code will continue (after the block, w/o the block being executed right away), thus going to the next lines (in your case the [library release]).

Later, once the ALAssetsLibrary finally have retrieved the asset info and the ALAsset your were requesting, it will finally trigger the code you set in the block (quite like when you use delegates and the delegate methods are called later when the data is available).


This explains why the code in the "block" will be executed asynchrnously, and may then be executed before or after the [library release], and by the time you will hit the [library release] line (or any code you put after the assetForURL call), the code in the block will probably not had time to execute.

The solution is no to use performSelector but better put the code that makes everything needed to update your interface or to process the EXIF data in the block itself.

For example, you may either directly put code to update your interface here (like [self.tableView reloadData] if you display EXIF data in a tableview), or fire an NSNotification to inform the rest of your code that new EXIF data has been added using addEntriesFromDictionary and needs to be displayed, etc)

AliSoftware
  • 32,623
  • 6
  • 82
  • 77