ALAssetsLibrary - crash after receiving ALAssetsLibraryChangedNotification - iphone

Part of my app has a photo browser, somewhat similar to Apple's Photos app, with an initial view controller to browse photo thumbnails and a detail view that's shown when you tap on a photo.
I'm using ALAssetsLibrary to access photos, and I pass an array of ALAsset URL's to my detail view controller so you can swipe from one photo to the next.
Everything works great, until I receive an ALAssetsLibraryChangedNotification while swiping from one photo to another (in the detail view controller), which often results in a crash:
NOTIFICATION: the asset library changed // my own NSLog for when the
notification occurs
loading assets... // my own NSLog for when I start reloading assets in
the thumbnail browser
Assertion failed: (size == bytesRead), function
-[ALAssetRepresentation _imageData], file /SourceCache/AssetsLibrary/MobileSlideShow-1373.58.1/Sources/ALAssetRepresentation.m,
line 224.
The specific line of code it crashes on, is in calling [currentRep metadata] as shown here:
- (void)someMethod {
NSURL *assetURL = [self.assetURLsArray objectAtIndex:index];
ALAsset *currentAsset;
[self.assetsLibrary assetForURL:assetURL resultBlock:^(ALAsset *asset) {
[self performSelectorInBackground:#selector(configureDetailViewForAsset:) withObject:asset];
} failureBlock:^(NSError *error) {
NSLog(#"failed to retrieve asset: %#", error);
}];
}
- (void)configureDetailViewForAsset:(ALAsset *)currentAsset {
ALAssetRepresentation *currentRep = [currentAsset defaultRepresentation];
if (currentAsset != nil) {
// do some stuff
}
else {
NSLog(#"ERROR: currentAsset is nil");
}
NSDictionary *metaDictionary;
if (currentRep != nil) {
metaDictionary = [currentRep metadata];
// do some other stuff
}
else {
NSLog(#"ERROR: currentRep is nil");
}
}
I understand that once a notification is received, it invalidates any references to ALAsset and ALAssetRepresentation objects... but how am I supposed to deal with the situation where it invalidates something right in the middle of trying to access it?
I've tried setting a BOOL, right when receiving the notification to completely abort and prevent [currentRep metadata] from ever being called, but even that doesn't catch it every time:
if (self.receivedLibraryChangeNotification) {
NSLog(#"received library change notification, need to abort");
}
else {
metaDictionary = [currentRep metadata];
}
Is there anything I can do? At this point I'm almost ready to give up on using the ALAssetsLibrary framework.
(note this unresolved thread on the Apple dev forums describing the same issue: https://devforums.apple.com/message/604430 )

It seems the problem is around here:
[self.assetsLibrary assetForURL:nextURL
resultBlock:^(ALAsset *asset) {
// You should do some stuff with asset at this scope
ALAssetRepresentation *currentRep = [asset defaultRepresentation];
// Assume we have a property for that
self.assetRepresentationMetadata = [currentRep metadata];
...
// assume we have a method for that
[self updateAssetDetailsView];
}
failureBlock:^(NSError *error) {
NSLog(#"failed to retrieve asset: %#", error);
}];
Once you have got user asset it is better to copy asset information by providing necessary data to your details controller subviews or by caching for later use. It can be helpful for avoiding ALAsset invalidation troubles. When notification ALAssetsLibraryChangedNotification sent you may need to discard details controller and query the Library content from the beginning.

Related

Detect if a dispatch_async method is still running

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

Saving image to custom album only

I've been following this tutorial http://www.touch-code-magazine.com/ios5-saving-photos-in-custom-photo-album-category-for-download/ to save an image to a custom album. This works fine but it's saving to both the camera roll and my custom album.
Looking at the code it seems like this is a necessary step, as after saving the image to the camera roll we have an ALAsset that can be used with the ALAssetsGroup addAsset: method.
Is there a way that I can add an image to a custom album without adding it to the camera roll?
Currently using this code:
-(void)saveImage:(UIImage*)image toAlbum:(NSString*)albumName withCompletionBlock:(SaveImageCompletion)completionBlock
{
//write the image data to the assets library (camera roll)
[self writeImageToSavedPhotosAlbum:image.CGImage orientation:(ALAssetOrientation)image.imageOrientation
completionBlock:^(NSURL* assetURL, NSError* error) {
//error handling
if (error!=nil) {
completionBlock(error);
return;
}
//add the asset to the custom photo album
[self addAssetURL: assetURL
toAlbum:albumName
withCompletionBlock:completionBlock];
}];
}
-(void)addAssetURL:(NSURL*)assetURL toAlbum:(NSString*)albumName withCompletionBlock:(SaveImageCompletion)completionBlock
{
__block BOOL albumWasFound = NO;
//search all photo albums in the library
[self enumerateGroupsWithTypes:ALAssetsGroupAlbum
usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
//compare the names of the albums
if ([albumName compare: [group valueForProperty:ALAssetsGroupPropertyName]]==NSOrderedSame) {
//target album is found
albumWasFound = YES;
//get a hold of the photo's asset instance
[self assetForURL: assetURL
resultBlock:^(ALAsset *asset) {
//add photo to the target album
[group addAsset: asset];
//run the completion block
completionBlock(nil);
} failureBlock: completionBlock];
//album was found, bail out of the method
return;
}
if (group==nil && albumWasFound==NO) {
//photo albums are over, target album does not exist, thus create it
__weak ALAssetsLibrary* weakSelf = self;
//create new assets album
[self addAssetsGroupAlbumWithName:albumName
resultBlock:^(ALAssetsGroup *group) {
//get the photo's instance
[weakSelf assetForURL: assetURL
resultBlock:^(ALAsset *asset) {
//add photo to the newly created album
[group addAsset: asset];
//call the completion block
completionBlock(nil);
} failureBlock: completionBlock];
} failureBlock: completionBlock];
//should be the last iteration anyway, but just in case
return;
}
} failureBlock: completionBlock];
}
Turns out there isn't - images will always be written to the camera roll by default.

Saving Image To Custom Library

I have an App which stores images to Custom Library in Photo Album in iphone.
I called the following Function of ALAssetLibrary
-(void)saveImage:(UIImage*)image toAlbum:(NSString*)albumName withCompletionBlock:(SaveImageCompletion)completionBlock
{
//write the image data to the assets library (camera roll)
[self writeImageToSavedPhotosAlbum:image.CGImage orientation:(ALAssetOrientation)image.imageOrientation
completionBlock:^(NSURL* assetURL, NSError* error) {
//error handling
if (error!=nil) {
completionBlock(error);
return;
}
//add the asset to the custom photo album
[self addAssetURL: assetURL
toAlbum:albumName
withCompletionBlock:completionBlock];
}];
}
in my SaveImage IBAction
-(IBAction)saveToLib:(id)sender
{
UIImage *savedImage = imgPicture.image;
NSLog(#"Saved Image%#",savedImage);
[self.library saveImage:savedImage toAlbum:#"Touch Code" withCompletionBlock:^(NSError *error) {
if (error!=nil) {
NSLog(#"Big error: %#", [error description]);
}
}];
}
but my application keep getting Crashed
Help me out
Thanks in Advance
It seems that you are not calling the [self.library saveImage:savedImage toAlbum:#....] correctly. If you are following this web site, add #import "ALAssetsLibrary+CustomPhotoAlbum.h" in your view controller.
Not sure if it is the problem. Sorry if this workaround is the wrong approach.
Problem is the file from the website he downloaded.
I had the same issue and rewriting the code myself solved this.
Maybe encoding or something like this...

Zeroing ALAssetRepresentation after saving image captured via UIImagePickerController in iOS5

In my app (which worked under iOS 4) I collect pictures selected via UIImagePickerController. Unfortunately, I have a strange problem after upgrading to iOS 5.
In a nutshell, I store ALAssetRepresentation in NSMutableArray. When I add photos from Library, everything is ok. However, when I capture and save a picture, all ALAssetRepresentations (including a new one) become 0-sized. ALAssetRepresentation.size and ALAssetRepresentation.getBytes:fromOffset:length:error: return 0 and getBytes:error is nil.
I init ALAssetsLibrary in AppDelegate, so the
“The lifetimes of objects you get back from a library instance are tied to the lifetime of the library instance.” condition is OK.
Is there a way to prevent ALAssetRepresentation from zeroing? Or how can I read image by bytes after this?
My code:
-(void) imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info{
if ([picker sourceType] == UIImagePickerControllerSourceTypePhotoLibrary){
[self addPhoto:[info valueForKey:UIImagePickerControllerReferenceURL]];
}
else if ([picker sourceType] == UIImagePickerControllerSourceTypeCamera){
[self savePhoto:[info valueForKey:UIImagePickerControllerOriginalImage]];
}
[self dismissModalViewControllerAnimated:YES];
}
-(ALAssetsLibrary*) getLibrary{
if (!library){
testAppDelegate *appDelegate = (testAppDelegate *)[[UIApplication sharedApplication] delegate];
library = appDelegate.library;
}
NSLog(#"getLibrary: %#", library);
return library;
}
-(void) addPhoto:(NSURL*) url{
ALAssetsLibraryAssetForURLResultBlock successBlock = ^(ALAsset *asset_){
ALAssetRepresentation *assetRepresentation = [[asset_ defaultRepresentation] retain];
[photos addObject: assetRepresentation];
};
ALAssetsLibraryAccessFailureBlock failureBlock = ^(NSError *error){
NSLog(#"Error: Cannot get image. %#", [error localizedDescription]);
};
[[self getLibrary] assetForURL:url resultBlock:successBlock failureBlock:failureBlock];
}
- (void)savePhoto:(UIImage *)image {
[[self getLibrary] writeImageToSavedPhotosAlbum:[image CGImage] orientation:[image imageOrientation] completionBlock:^(NSURL *assetURL, NSError *error) {
if (error) {
NSLog(#"Error: Cannot save image. %#", [error localizedDescription]);
} else {
NSLog(#"photo saved");
[self addPhoto:assetURL];
}
}];
}
I solved it!
ALAssetsLibraryChangedNotification
Sent when the contents of the assets library have changed from under the app that is using the data.
When you receive this notification, you should discard any cached information and query the assets library again. You should consider invalid any ALAsset, ALAssetsGroup, or ALAssetRepresentation objects you are referencing after finishing processing the notification.
Have you tried retaining the ALAssets instead of the ALAssetRepresentation? This should work, I have used this approach before.

AVPlayerLayer goes blank after several presentModalView/dismissModalView calls

I have a video playing app which displays nothing in the AVPlayerLayer after repeatedly presenting and hiding the modal view which contains it. If I dismiss the modal view when this happens, the next load usually displays fine (?!!). The black screen issue happens roughly 20% of the time.
I build an AVMutableComposition to make the AVPlayerItem, but this bug happens even if there's only a single sample involved.
The issue can also be reproduced with a lot of app switching and turning music on and off. I do include music controls in my app (along with a simple view which displays the currently playing iTunes track).
This only happens on iOS 4. It used to happen on iOS 5 as well, but when I started recycling the view which contains the AVPlayerLayer, it worked fine. The only things I don't recycle are the AVPlayer and the relevant AVPlayerItem.
Here's how I load the assets and build a player:
- (void)loadAssetsFromFiles:(id)sender {
NSLog(#"loadAssetsFromFiles called");
assert ([assetURL2clipID count] > 0);
self.isWaitingToLoadAssets = NO;
composition = [AVMutableComposition new];
videoComposition = [AVMutableVideoComposition new];
[self releaseAssets];
//We're going to add this asset to a composition, so we'll need to have random access available
//WARNING: This can cause slow initial loading, so consider loading files later and as needed.
NSDictionary *assetOptions = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES]
forKey:AVURLAssetPreferPreciseDurationAndTimingKey];
//iterate through the asset urls we know we need to load
for (NSURL *fileURL in [assetURL2clipID allKeys])
{
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:fileURL options:assetOptions];
assert(asset);
//index assets by clipID
[assets setObject:asset forKey:[assetURL2clipID objectForKey:fileURL]];
NSString *tracksKey = #"tracks";
[asset loadValuesAsynchronouslyForKeys:[NSArray arrayWithObject:tracksKey] completionHandler:
^{
NSLog(#"an asset completed loading values for keys.");
NSLog(#"Tracks loaded:");
[asset.tracks enumerateObjectsUsingBlock:
^(AVAssetTrack *obj, NSUInteger index, BOOL *stop)
{
NSLog(#"\n mediaType: %#\n trackID: %d\n", obj.mediaType, obj.trackID);
}];
NSArray *metadata = [asset commonMetadata];
for ( AVMetadataItem* item in metadata ) {
NSString *key = [item commonKey];
NSString *value = [item stringValue];
NSLog(#" metadata key = %#, value = %#", key, value);
}
if (!viewIsActive)
{
NSLog(#"An asset finished loading while the player view was inactive! Did you make sure cancelLoading called on this asset?");
}
// Completion handler block.
NSError *error = nil;
AVKeyValueStatus status = [asset statusOfValueForKey:tracksKey error:&error];
if (status == AVKeyValueStatusLoaded && error == nil) {
//if we've loaded all of our assets, it's time to build the composition and prepare the player!
loadedAssets++;
if (loadedAssets == [assets count])
{
CGSize videoSize = [asset naturalSize];
//every video composition needs these set
videoComposition.renderSize = videoSize;
videoComposition.frameDuration = CMTimeMake(1, 30); // 30 fps. TODO: Set this to the framerate of one of the assets
//using the assets we've already got
[self buildCompositions];
self.playerItem = [AVPlayerItem playerItemWithAsset:composition];
self.playerItem.videoComposition = videoComposition;
//TODO: Adding observer stuff should be on the main thread to prevent a partial notification from happening
[playerItem addObserver:self forKeyPath:#"status"
options:0 context:&ItemStatusContext];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(playerItemDidReachEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:playerItem];
self.player = [AVPlayer playerWithPlayerItem:playerItem];
[playerView setPlayer:player];
[self.player addObserver:self forKeyPath:#"status" options:NSKeyValueObservingOptionNew context:nil];
self.isObservingPlayerStatus = YES;
}
} else if (error != nil) {
// Deal with the error appropriately.
NSLog(#"WARNING: An asset's tracks were not loaded, so the composition cannot be completed. Error:\n%#\nstatus of asset: %d", [error localizedDescription], status);
}
else
{
//There was no error but we don't know what the problem was.
NSLog(#"WARNING: An asset's tracks were not loaded, so the composition cannot be completed. No error was reported.\nstatus of asset: %d", status);
}
}];
}
}
That [self buildCompositions] function you see builds an AVVideoComposition to do opacity ramps, but I tried bypassing it and get the same problem.
When profiling the program, CoreAnimation reports a framerate of ~45 FPS when everything is working correctly, and 0-4 FPS when the blank screen rears its presumably ugly head.
This guy seems to have had a similar problem, but for me recycling the views really only fixed things for iOS 5:
Playing many different videos on iphone using AVPlayer