I am writing a simple application for an artist who wants to market his work on iTunes.
Because there are only twelve images, which can be deleted more rapidly than they could be selected from an image picker, the user does not select the images individually. Instead, the images are copied programmatically from the application bundle into the photo library. This minimizes the size and complexity of the application and saves the end-user from the tedium of having to select each image individually.
Note: this problem occurs only on the physical device (iPhone 3Gs/3.1).
Not surprisingly, my implementation involves calling UIImageWriteToSavedPhotosAlbum inside a loop that iterates over an array of 12 full paths to the corresponding image files (all are JPEGs). Minus the details (e.g., error checking, logging, optional callback parameters),
the loop boils down to:
for ( NSString* path in paths ) {
image = [UIImage imageWithContentsOfFile:path];
if (image) {
[NSThread sleepForTimeInterval:1.0]; // unacceptable
UIImageWriteToSavedPhotosAlbum(image, ...);
}
}
Note the line marked "unacceptable." Why must the current thread sleep for a full second prior to each invocation of UIImageWriteToSavedPhotosAlbum(image, ...)? Well, because that is quite literally how long it takes for iPhoneOS 3.1 (running on my 3Gs device) to get around to the business of actually writing a buffered UIImage object to the persistent store that represents the user's photo library! How do I know this? Well, because without the call to NSThread sleepForTimeInterval:, no error of any kind is ever reported even though only a fraction of the 12 images (which appears to vary randomly between 1 and 9) are in fact copied to the photo library. In addition, results do improve as the sleep interval increases, but, as I have discovered, the interval must be increased dramatically before the improvement becomes noticeable. Even at as high a value as 0.8 seconds, I have observed missing files following repeated trials. At 1 second, I have observed up to three consecutive "correct" runs in which all files are fully transferred before losing all patience.
I have googled this issue to death, checked every posting on this board that mentions UIImageWriteToSavedPhotosAlbum, and turned up nothing. Am I the only person in the world who has ever called that function inside a loop? Is there some reason why it should be self-evident to me that I should not do so?
Facing the same issue I found a way to get around it.
When you call UIImageWriteToSavedPhotosAlbum you can pass completion selector as an argument. Thus no need to call UIImageWriteToSavedPhotosAlbum in a loop. When the first invocation completely finished its saving (your completion method is called), proceed further. Use some counter to control the process.
Here is my code:
-(IBAction) SaveAllPictures {
UIImage *the_image;
if (images_to_photos_album < numberOfPhotos) {
the_image= [UIImage //get your image here]
UIImageWriteToSavedPhotosAlbum(the_image, self, #selector(imageSaved: error: context:), the_image);
}
if (images_to_photos_album == numberOfPhotos) {
the_image= [UIImage //get your image here
UIImageWriteToSavedPhotosAlbum(the_image, self, #selector(image: didFinishSavingWithError: contextInfo:), the_image);
}
}
-(void) imageSaved: (UIImage *) image error:(NSError *) error context: (void *) contextInfo {
NSLog(#"image saved, error = %#",error);
if (!error) {
images_to_photos_album = images_to_photos_album+1;
[self SaveAllPictures];
} else {
//handle error here
}
}
- (void) image: (UIImage *)image didFinishSavingWithError: (NSError *)error contextInfo: (void *)contextInfo {
images_to_photos_album =0;
if (error) {
// handle error here
} else {
// handle success here
}
}
A quick search of the developer forums at Apple turned up the following post:
https://devforums.apple.com/message/112119#112119 for the full thread.
This thread suggests to me that there is no safe way to write an image to the photo library programmatically.
Related
When the user starts the app for the first time he gets a pop up and has to save an image. It works on the simulator and on the 4S. But when I start it with my 3G it gives me a SIGABRT error as soon as I chose a picture. I assume its because of the size of the picture which is too pick and therefore claiming all of the ram - But that is rather weird, because I make it much smaller. Here is the code:
- (void)viewDidLoad
{
[super viewDidLoad];
if ([[NSUserDefaults standardUserDefaults] objectForKey:#"bild"] == nil) {
picker = [[UIImagePickerController alloc] init];
picker.delegate = self;
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
[self presentModalViewController:picker animated:YES];
}
}
- (void)imagePickerController:(UIImagePickerController *) Picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
double compressionRatio=1;
NSData *imageData=UIImageJPEGRepresentation([info objectForKey:#"bild"],compressionRatio);
while ([imageData length]>5000) {
compressionRatio=compressionRatio*0.20;
imageData=UIImageJPEGRepresentation([info objectForKey:#"bild"],compressionRatio);
}
UIImage *yourUIImage;
yourUIImage = [info objectForKey:UIImagePickerControllerOriginalImage];
imageData = [NSKeyedArchiver archivedDataWithRootObject:yourUIImage];
[[NSUserDefaults standardUserDefaults] setObject:imageData forKey:#"bild"];
[[NSUserDefaults standardUserDefaults] synchronize];
[self dismissModalViewControllerAnimated:YES];
}
I get the SIGABRT error at this line imageData = [NSKeyedArchiver archivedDataWithRootObject:yourUIImage];
Should I use another method to resize the image? Or should I save it as a local file and retrieve it everytime the app starts? (If its possible)
You are using NSCoding to archive an UIImage into a NSData object.
Before iOS5 UIImage didn't implement the methods for NSCoding. You simply can't use archivedDataWithRootObject: with UIImages before iOS5.
That's why you get an exception on the iPhone 3G.
This is just a very educated guess because I can't confirm that with documentation or a test on a device but there are a lot of questions and forum posts around that ask how to implement NSCoding on UIImage.
and this whole block of code isn't called at all because [info objectForKey:#"bild"] will return nil. The info dictionary does not contain an object for the key bild.
Documentation for UIImagePickerControllerDelegate_Protocol contains a List of valid keys
NSData *imageData=UIImageJPEGRepresentation([info objectForKey:#"bild"],compressionRatio);
while ([imageData length]>5000) {
compressionRatio=compressionRatio*0.20;
imageData=UIImageJPEGRepresentation([info objectForKey:#"bild"],compressionRatio);
}
you probably want to use something like that:
NSData *data;
if ([[UIImage class] conformsToProtocol:#protocol(NSCoding)]) {
// >= iOS5
data = [NSKeyedArchiver archivedDataWithRootObject:image];
// save so you know it's an archived UIImage object
}
else {
// < iOS5
data = /* your UIImageJPEGRepresentation method but this time with the key UIImagePickerControllerOriginalImage instead of "bild" */
// save so you know it's raw JPEG data
}
Edit: Another error is probably that you're looking for an object for the key "bild" within the info dictionary you're passed from the image picker controller. It will have no such key. You should pass the key UIImagePickerControllerOriginalImage to get at the image instead.
--
You're not throwing away the old imageData. If your problem is high memory consumption, you'll have to do away with the image data as you progressively shrink the image.
Additionally, an image may be too big anyway to fit within 5000 bytes. Your current code doesn't handle this gracefully. Eventually, the compression ratio will grow beyond 1.0 at which point the function will probably throw an exception.
I'd suggest resizing the image before trying to compress it heavily.
No matter how you make the image data smaller, your current code keeps all the old copies around until the next time the autorelease pool is drained. Since the data returned from UIImageJPEGRepresentation is autoreleased, you may try using an inline autorelease pool around every loop iteration to drain the data immediately.
If the SIGABRT is consistently happening at imageData = [NSKeyedArchiver archivedDataWithRootObject:yourUIImage]; then my feeling would be that it is likely to be that specific line, rather than a memory issue.
UIImage never used to conform to NSCoding, but the current docs say that it does. It could be that the 3G, which will be limited to iOS 4.2.1 is using a non-conformant version and so crashes when you try to archive, whereas the 4S on iOS 5 uses the new conformant version.
Try adding a category for NSCoding to UIImage and then rerun on the 3G. Example at http://iphonedevelopment.blogspot.com/2009/03/uiimage-and-nscoding.html
Im attempting to save some images to a camera roll, all the images are in a array.
if (buttonIndex == 1) {
int done = 0;
NSMutableArray *copyOfImages = [[NSMutableArray alloc]initWithArray:saveImagesToArray];
while (!done == 1){
if (copyOfImages.count == 0) {
done = 1;
}
if (copyOfImages.count > 0){
[copyOfImages removeLastObject];
}
UIImage *image = [[UIImage alloc]init];
image = [copyOfImages lastObject];
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);
}
}
because i dont know how many images there can be i use a while loop
Im testing this with 8 images, the array shows a count of 8 so thats good.
When i try saving the images this comes up in the console.
-[NSKeyedUnarchiver initForReadingWithData:]: data is NULL
Out of the 8 images im trying, only 5 show up in the camera roll.
Any help would be great.
Thanks :)
Why are you calling [copyOfImages removeLastObject]?
Every time you go through that look are you destroying the last object, which is strange because you haven't added it to the roll yet. Take out that line and see if you look works.
Also, rather than using a for loop, use the following pattern:
for (id object in array) {
// do something with object
}
This will enumerate though the objects in the array, stopping when it reaches the end. Just be sure not to modify the array while you are doing this.
I had this same issue and I resolved it by ensuring that the images are saved sequentially. I think there may be some kind of race condition going on.
Basically, I did:
UIImageWriteToSavedPhotosAlbum([self.images objectAtIndex:self.currentIndex], self,
#selector(image:didFinishSavingWithError:contextInfo:), nil);
Then in image:didFinishSavingWithError:contextInfo:, I increment currentIndex and try the next image, if there are any left.
This has worked for me in every case so far.
I had the same exact problem. No matter how much pictures i added to the share activityController, maximum of 5 were saved.
i have found that the problem was to send the real UIImage and not URL:
NSMutableArray *activityItems = [[NSMutableArray alloc] init];
for(UIImage *image in imagesArray) {
[activityItems addObject:image];
}
UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:nil];
To expand on Bill's answer, UIImageWriteToSavedPhotosAlbum seems to do its writing on some unknown thread asynchronously - but also there is some hidden limit to the number of images it can write at once. You can even tease out write busy-type errors if you dig in deep.
Tons more info here:
Objective-C: UIImageWriteToSavedPhotosAlbum() + asynchronous = problems
I also agree with Bill that serializing your writes is the only reasonable/reliable answer
I have seen.
Grabbing album art for current song and using it to change a certain imageView.image generates an error, but no longer crashes. (It did before because I left out the if (!artwork) error handling. Eheh.)
This method:
- (void)handleNowPlayingItemChanged:(id)notification {
MPMediaItem *item = self.musicPlayer.nowPlayingItem;
CGSize albumCoverSize = self.albumCover.bounds.size;
MPMediaItemArtwork *artwork =
[item valueForProperty:MPMediaItemPropertyArtwork];
if (artwork) {
self.albumCover.image = [artwork imageWithSize:albumCoverSize];
} else {
self.albumCover.image = nil;
}
}
Explodes like this:
CPSqliteStatementPerform: attempt to write a readonly database for
UPDATE ddd.ext_container SET orig_date_modified = (SELECT date_modified
FROM container WHERE pid=container_pid) WHERE orig_date_modified=0
CPSqliteStatementReset: attempt to write a readonly database for
UPDATE ddd.ext_container SET orig_date_modified = (SELECT date_modified
FROM container WHERE pid=container_pid) WHERE orig_date_modified=0
But only on launch. And it still shows the image (or lack thereof). Weird.
Edit: The iPod Library is readonly (apps can't change anything, only iTunes), so maybe it's yelling at
me for writing a readonly something, but still allowing it because nothing readonly is being modified?
And after that's fixed, I need to get resizing working (for Landscape support) instead of IB's stretching.
Not vital, but still a nice thing to have.
Here's what I do. It creates no errors, and produces an image every time. If the song doesn't have an image, it defaults to the one I provide. I think because you're not checking for an image with a specific size (320 by 320, matching the screen width for me), it fails to figure it out correctly. I don't know why you're getting the SQLite error, but hopefully this fixes it!
MPMediaItemArtwork *artworkItem = [self.musicPlayer.nowPlayingItem valueForProperty: MPMediaItemPropertyArtwork];
if ([artworkItem imageWithSize:CGSizeMake(320, 320)]) {
[self.currentlyPlayingArtworkView setImage:[artworkItem imageWithSize:CGSizeMake (320, 320)]];
}
else {
[self.currentlyPlayingArtworkView setImage:[UIImage imageNamed:#"NoArtworkImage"]];
}
Link here - Why am I getting this CPSqliteStatementPerform error in xcode console
Putting this here so the question can be marked as Answered.
I used the codes below to save images to photo librabry
for(int i=0;i<[imageNameArray count];i++)
{
NSMutableString *sss=[[NSMutableString alloc] initWithString: myAppPath];
[sss appendString:#"/"] ;
[sss appendString: [NSString stringWithFormat:#"%#",[imageNameArray objectAtIndex:i] ] ] ;
UIImage *image = [[[UIImage alloc] initWithContentsOfFile:sss]autorelease];
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);
[sss release];
}
but sometimes, it can not save images correctly, I found that in photo library, there are one or more black blocks replace the saved images in thumbnail mode, but if I click the black blocks, the original images can display correctly.
Is there anyone met the same problem?
Welcome any comment
Thanks
interdev
It could be a problem that you're saving the images in quick succession, and as a result, the save calls are overlapping. (It takes longer to write an image to the disk than it does to return from the function).
As far as I understand the save image function is asynchronous, and allows a delegate/call back to be registered (in the places where you're passing nil).
You should probably wait until one image has finished writing until you start another.
You can engineer this using the delegate and callbacks. To make it better for the user and to give them an idea of whats happening, you can display a progress bar and other information such as "1/3 images saved" etc.
Edit:
For a better context, I looked up the documentation:
The UIImageWriteToSavedPhotosAlbum function is declared as such:
void UIImageWriteToSavedPhotosAlbum (
UIImage *image,
id completionTarget,
SEL completionSelector,
void *contextInfo
);
you should set the completionTarget (likely to be self) and the completionSelector which should have the following signature:
- (void) image: (UIImage *) image
didFinishSavingWithError: (NSError *) error
contextInfo: (void *) contextInfo;
In the completion selector, you can perform the logic of whether or not you need to perform another save, and update your 'progress' UI.
I'm trying to improve the performance of my image-intensive iPhone app by using a disk-based image cache instead of going over the network. I've modeled my image cache after SDImageCache (http://github.com/rs/SDWebImage/blob/master/SDImageCache.m), and is pretty much the same but without asynchronous cache in/out operations.
I have some scroll views and table views that load these images asynchronously. If the image is on the disk, it's loaded from the image cache, otherwise a network request is made and the subsequent result is stored in the cache.
The problem I'm running into is that as I scroll through the scroll views or table views, there's a noticeable lag as the image is loaded from disk. In particular, the animation of going from one page to another on a scroll view has a small freeze in the middle of the transition.
I've tried to fix this by:
Using an NSOperationQueue and NSInvocationOperation objects to make the disk access requests (in the same manner as SDImageCache), but it doesn't help with the lag at all.
Tweaking the scroll view controller code so that it only loads images when the scroll view is no longer scrolling. This means the disk access only fires when the scroll view stops scrolling, but if I immediately try to scroll to the next page I can notice the lag as the image loads from disk.
Is there a way to make my disk accesses perform better or have less of an effect on the UI?
Note that I'm already caching the images in memory as well. So once everything is loaded into memory, the UI is nice and responsive. But when the app starts up, or if low memory warnings are dispatched, I'll experience many of these UI lags as images are loaded from disk.
The relevant code snippets are below. I don't think I'm doing anything fancy or crazy. The lag doesn't seem to be noticeable on an iPhone 3G, but it's pretty apparent on an 2nd-gen iPod Touch.
Image caching code:
Here's a relevant snippet of my image caching code. Pretty straightforward.
- (BOOL)hasImageDataForURL:(NSString *)url {
return [[NSFileManager defaultManager] fileExistsAtPath:[self cacheFilePathForURL:url]];
}
- (NSData *)imageDataForURL:(NSString *)url {
NSString *filePath = [self cacheFilePathForURL:url];
// Set file last modification date to help enforce LRU caching policy
NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
[attributes setObject:[NSDate date] forKey:NSFileModificationDate];
[[NSFileManager defaultManager] setAttributes:attributes ofItemAtPath:filePath error:NULL];
return [NSData dataWithContentsOfFile:filePath];
}
- (void)storeImageData:(NSData *)data forURL:(NSString *)url {
[[NSFileManager defaultManager] createFileAtPath:[self cacheFilePathForURL:url] contents:data attributes:nil];
}
Scroll view controller code
Here's a relevant snippet of the code that I use for displaying images in my scroll view controllers.
- (void)scrollViewDidScroll:(UIScrollView *)theScrollView {
CGFloat pageWidth = theScrollView.frame.size.width;
NSUInteger index = floor((theScrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
[self loadImageFor:[NSNumber numberWithInt:index]];
[self loadImageFor:[NSNumber numberWithInt:index + 1]];
[self loadImageFor:[NSNumber numberWithInt:index - 1]];
}
- (void)loadImageFor:(NSNumber *)index {
if ([index intValue] < 0 || [index intValue] >= [self.photoData count]) {
return;
}
// ... initialize an image loader object that accesses the disk image cache or makes a network request
UIView *iew = [self.views objectForKey:index];
UIImageView *imageView = (UIImageView *) [view viewWithTag:kImageViewTag];
if (imageView.image == nil) {
NSDictionary *photo = [self.photoData objectAtIndex:[index intValue]];
[loader loadImage:[photo objectForKey:#"url"]];
}
}
The image loader object is just a lightweight object that checks the disk cache and decides whether or not to fetch an image from disk or network. Once it's done, it calls a method on the scroll view controller to display the image:
- (void)imageLoadedFor:(NSNumber *)index image:(UIImage *)image {
// Cache image in memory
// ...
UIView *view = [self.views objectForKey:index];
UIImageView *imageView = (UIImageView *) [view viewWithTag:kImageViewTag];
imageView.contentMode = UIViewContentModeScaleAspectFill;
imageView.image = image;
}
UPDATE
I was experimenting with the app, and I disabled the image cache and reverted to always making network requests. It looks like simply using network requests to fetch images is also causing the same lag when scrolling through scroll views and table views! That is, when a network request finishes and the image is shown in the scroll view page or table cell, the UI slightly lags a bit and has a few split seconds of lag as I try to drag it.
The lag seems to be just more noticeable when using the disk cache, since the lag always occurs right at the page transition. Perhaps I'm doing something wrong when assigning the loaded image to the appropriate UIImageView?
Also - I've tried using small images (50x50 thumbnails) and the lag seems to improve. So it seems that the performance hit is due to either loading a large image from disk or loading a large image into an UIImage object. I guess one improvement would be to reduce the size of the images being loaded into the scroll view and table views, which was what I was planning to do nonetheless. However, I just don't understand how other photo-intensive apps are able to present what looks like pretty high-res photos in scrollable views without performance problems from going to disk or over the network.
You should check out the LazyTableImages sample app.
If you've narrowed it down to network activity I would try encapsulating your request to ensure it is 100% off of the main thread. While you can use NSURLConnection asynchronously and respond to it's delegate methods, I find it easier to wrap a synchronous request in a background operation. You can use NSOperation or grand central dispatch if your needs are more complex. An (relatively) simple example in an imageLoader implementation could be:
// imageLoader.m
// assumes that that imageCache uses kvp to look for images
- (UIImage *)imageForKey:(NSString *)key
{
// check if we already have the image in memory
UImage *image = [_images objectForKey:key];
// if we don't have an image:
// 1) start a background task to load an image from a file or URL
// 2) return a default image to display while loading
if (!image) {
[self performSelectorInBackground:#selector(loadImageForKey) withObject:key];
image = [self defaultImage];
}
return image;
}
- (void)loadImageForKey:(NSString *)key
{
NSAutoReleasePool *pool = [[NSAutoReleasePool alloc] init];
// attempt to load the image from the file cache
UIImage *image = [self imageFromFileForKey:key];
// if no image, load the image from the URL
if (!image) {
image = [self imageFromURLForKey:key];
}
// if no image, return default or imageNotFound image
if (!image) {
image = [self notFoundImage];
}
if ([_delegate respondsTo:#selector(imageLoader:didLoadImage:ForKey:)]) {
[_delegate imageLoader:self didLoadImage:image forKey:key];
}
[pool release];
}
- (UIImage *)imageFromURLForKey:(NSString *)key
{
NSError *error = nil;
NSData *imageData = [NSData dataWithContentsOfURL:[self imageURLForKey:key]
options:0
error:&error];
UIImage *image;
// handle error if necessary
if (error) {
image = [self errorImage];
}
// create image from data
else {
image = [UIImage imageWithData:imageData];
}
return image;
}
The image from the disk is actually read while drawing the image on the imageview. Even if we cache the image reading from the disk it does not affect since it just keeps reference to the file. You might have to use tiling of larger images for this purpose.
Regards,
Deepa
I've had this problem - you are hitting the limit of how fast the UI can load an image while scrolling - so i'd just work around the problem and improve the user experience.
Only load images when the scroll is at a new 'page' (static rectangle) and
Put an activity indicator behind a transparent scroll view to handle the case where the user is scrolling faster than the app can load content
It's typically the image decoding which takes time, and that causes the UI to freeze (since it's all happening on the main thread). Every time you call [UIImage imageWithData:] on a large image, you'll notice a hiccup. Decoding a smaller image is far quicker.
Two options:
You can load a thumbnail version of each image first, then 'sharpen up' on didFinishScrolling. The thumbnails should decode quickly enough such that you don't skip any frames.
You can load a thumbnail version of each image or show a loading indicator first, then decode the full-resolution image on another thread, and sub it in when it's ready. (This is the technique that is employed in the native Photos app).
I prefer the second approach; it's easy enough with GDC nowadays.