use xcassets without imageNamed to prevent memory problems? - ios7

according to the apple documentation it is recommended to use xcassets for iOS7 applications and reference those images over imageNamed.
But as far as I'm aware, there were always problems with imageNamed and memory.
So I made a short test application - referencing images out of the xcassets catalogue with imageNamed and started the profiler ... the result was as expected. Once allocated memory wasn't released again, even after I removed the ImageView from superview and set it to nil.
I'm currently working on an iPad application with many large images and this strange imageView behavior leads to memory warnings.
But in my tests I wasn't able to access xcassets images over imageWithContentsOfFile.
So what is the best approach to work with large images on iOS7? Is there a way to access images from the xcassets catalogue in another (more performant) way? Or shouldn't I use xcassets at all so that I can work with imageWithContentsOfFile?
Thank you for your answers!

UPDATE: Cache eviction works fines (at least since iOS 8.3).
I decided to go with the "new Images.xcassets" from Apple, too. Things started to go bad, when I had about 350mb of images in the App and the App constantly crashed (on a Retina iPad; probably because of the size of the loaded images).
I have written a very simple test app where I load the images in three different types (watching the profiler):
imageNamed: loaded from an asset: images never gets released and the app crashes (for me I could load 400 images, but it really depends on the image size)
imageNamed: (conventionally included to the project): The memory usage is high and once in a while (> 400 images) I see a call to didReceiveMemoryWarning:, but the app is running fine.
imageWithContentsOfFile([[NSBundle mainBundle] pathForResource:...): The memory usage is very low (<20mb) because the images are only loaded once at a time.
I really would not blame the caching of the imageNamed: method for everything as caching is a good idea if you have to show your images again and again, but it is kind of sad that Apple did not implement it for the assets (or did not document it that it is not implemented). In my use-case, I will go for the non-caching imageWithData because the user won't see the images again.
As my app is almost final and I really like the usage of the loading mechanism to find the right image automatically, I decided to wrap the usage:
I removed the images.xcasset from the project-target-copy-phase and added all images "again" to the project and the copy-phase (simply add the top level folder of Images.xcassets directly and make sure that the checkbox "Add To Target xxx" is checked and "Create groups for any added folders" (I did not bother about the useless Contents.json files).
During first build check for new warnings if multiple images have the same name (and rename them in a consistent way).
For App Icon and Launch Images set "Don't use asset catalog" in project-target-general and reference them manually there.
I have written a shell script to generate a json-model from all the Contents.json files (to have the information as Apples uses it in its asset access code)
Script:
cd projectFolderWithImageAsset
echo "{\"assets\": [" > a.json
find Images.xcassets/ -name \*.json | while read jsonfile; do
tmppath=${jsonfile%.imageset/*}
assetname=${tmppath##*/}
echo "{\"assetname\":\"${assetname}\",\"content\":" >> a.json
cat $jsonfile >> a.json;
echo '},' >>a.json
done
echo ']}' >>a.json
Remove the last "," comma from json output as I did not bother to do it manually here.
I have used the following app to generate json-model-access code: https://itunes.apple.com/de/app/json-accelerator/id511324989?mt=12 (currently free) with prefix IMGA
I have written a nice category using method swizzling in order to not change running code (and hopefully removing my code very soon):
(implementation not complete for all devices and fallback mechanisms!!)
#import "UIImage+Extension.h"
#import <objc/objc-runtime.h>
#import "IMGADataModels.h"
#implementation UIImage (UIImage_Extension)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
Method imageNamed = class_getClassMethod(class, #selector(imageNamed:));
Method imageNamedCustom = class_getClassMethod(class, #selector(imageNamedCustom:));
method_exchangeImplementations(imageNamed, imageNamedCustom);
});
}
+ (IMGABaseClass*)model {
static NSString * const jsonFile = #"a";
static IMGABaseClass *baseClass = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *fileFilePath = [[NSBundle mainBundle] pathForResource:jsonFile ofType:#"json"];
NSData* myData = [NSData dataWithContentsOfFile:fileFilePath];
__autoreleasing NSError* error = nil;
id result = [NSJSONSerialization JSONObjectWithData:myData
options:kNilOptions error:&error];
if (error != nil) {
ErrorLog(#"Could not load file %#. The App will be totally broken!!!", jsonFile);
} else {
baseClass = [[IMGABaseClass alloc] initWithDictionary:result];
}
});
return baseClass;
}
+ (UIImage *)imageNamedCustom:(NSString *)name{
NSString *imageFileName = nil;
IMGAContent *imgContent = nil;
CGFloat scale = 2;
for (IMGAAssets *asset in [[self model] assets]) {
if ([name isEqualToString: [asset assetname]]) {
imgContent = [asset content];
break;
}
}
if (!imgContent) {
ErrorLog(#"No image named %# found", name);
}
if (is4InchScreen) {
for (IMGAImages *image in [imgContent images]) {
if ([#"retina4" isEqualToString:[image subtype]]) {
imageFileName = [image filename];
break;
}
}
} else {
if ( UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone ) {
for (IMGAImages *image in [imgContent images]) {
if ([#"iphone" isEqualToString:[image idiom]] && ![#"retina4" isEqualToString:[image subtype]]) {
imageFileName = [image filename];
break;
}
}
} else {
if (isRetinaScreen) {
for (IMGAImages *image in [imgContent images]) {
if ([#"universal" isEqualToString:[image idiom]] && [#"2x" isEqualToString:[image scale]]) {
imageFileName = [image filename];
break;
}
}
} else {
for (IMGAImages *image in [imgContent images]) {
if ([#"universal" isEqualToString:[image idiom]] && [#"1x" isEqualToString:[image scale]]) {
imageFileName = [image filename];
if (nil == imageFileName) {
// fallback to 2x version for iPad unretina
for (IMGAImages *image in [imgContent images]) {
if ([#"universal" isEqualToString:[image idiom]] && [#"2x" isEqualToString:[image scale]]) {
imageFileName = [image filename];
break;
}
}
} else {
scale = 1;
break;
}
}
}
}
}
}
if (!imageFileName) {
ErrorLog(#"No image file name found for named image %#", name);
}
NSString *imageName = [[NSBundle mainBundle] pathForResource:imageFileName ofType:#""];
NSData *imgData = [NSData dataWithContentsOfFile:imageName];
if (!imgData) {
ErrorLog(#"No image file found for named image %#", name);
}
UIImage *image = [UIImage imageWithData:imgData scale:scale];
DebugVerboseLog(#"%#", imageFileName);
return image;
}
#end

Related

SDWebImage Download image and store to cache for key

Hello I am using the SDWebImage framework in a project and I want to download and cache images, but I think my code is storing an image in the cache twice? Is there any way to store the image in the cache by a key only once? Here is my code.
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager downloadWithURL:[NSURL URLWithString:url] options:0 progress:^(NSUInteger receivedSize, long long expectedSize) {
} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished) {
if(image){
NSString *localKey = [NSString stringWithFormat:#"Item-%d", i];
[[SDImageCache sharedImageCache] storeImage:image forKey:localKey];
}
}];
Is there something that I missed? Looks like doing this in my allocations instrument is pilling up a lot of memory.
I'm surprised nobody answered this question, but I've had a similar question and came across this, so I'll answer it for people viewing this going forward (assuming you've sorted this out yourself by now).
To directly answer your question, yes, you are caching the image twice.
Download calls to SDWebImageManager automatically cache images with keys based on the absoluteString of the image's url. If you want your own key, you can use the download call on SDWebImageDownloader which as far as I can tell does NOT cache by default. From there you can call the sharedImageCache as you're already doing and cache with whatever key you want.
That aside, it is strange you're seeing allocations piling up in any case as SDWebImage likes to cache to disk and not memory generally. Maybe something else is going on at the same time?
In Swift use the code below to download an image and to store it in the cache:
//SDWebImageManager download image with High Priority
SDWebImageManager.sharedManager().downloadImageWithURL(NSURL(string: imageUrl), options: SDWebImageOptions.HighPriority, progress: { (receivedSize :Int, ExpectedSize :Int) in
SVProgressHUD.show()
}, completed: { (image :UIImage!, error:NSError!, cacheType :SDImageCacheType, finished :Bool,imageUrl: NSURL!) in
if(finished) {
SVProgressHUD.dismiss()
if((image) != nil) {
//image downloaded do your stuff
}
}
})
Swift 3 version:
SDWebImageManager.shared().downloadImage(with: NSURL(string: "...") as URL!, options: .continueInBackground, progress: {
(receivedSize :Int, ExpectedSize :Int) in
}, completed: {
(image : UIImage?, error : Error?, cacheType : SDImageCacheType, finished : Bool, url : URL?) in
})
Objective-C version:
[[SDWebImageDownloader sharedDownloader] downloadImageWithURL:[NSURL URLWithString:imageUrl] options:SDWebImageDownloaderUseNSURLCache progress:nil completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
if (image && finished) {
// Cache image to disk or memory
[[SDImageCache sharedImageCache] storeImage:image forKey:CUSTOM_KEY toDisk:YES];
}
}];
SWIFT 5 & Latest SDWebImage 5.2.3
SDWebImageManager.shared.loadImage(
with: album.artUrlFor(imageShape: .square),
options: .continueInBackground, // or .highPriority
progress: nil,
completed: { [weak self] (image, data, error, cacheType, finished, url) in
guard let sself = self else { return }
if let err = error {
// Do something with the error
return
}
guard let img = image else {
// No image handle this error
return
}
// Do something with image
}
)
Swift 4:
SDWebImageManager.shared().loadImage(
with: URL(string: imageUrl),
options: .highPriority,
progress: nil) { (image, data, error, cacheType, isFinished, imageUrl) in
print(isFinished)
}
SDWebImage caches the image both to disk as well as memory. Let's go through this:
You download the image from a new url.
It gets cached to memory and disk.
If you call the image in the same session, it is retrieved from the memory.
Let's say you re-run the app and then access the url, it will check the memory, where the image won't be there, then it will check the disk, and get it from there. If not, it will download it.
The image stays in disk for a week by the standard setting.
So, you don't need to worry about caching. SDWebImage takes care of it pretty damn well.
You can do custom implementation for caching as well as image refresh from the cache in case you want the settings as per your HTTP caching header as well.
You can find the complete details on their github page here.
Swift 4.0.
let url = URL(string: imageUrl!)
SDWebImageManager.shared().imageDownloader?.downloadImage(with: url, options: .continueInBackground, progress: nil, completed: {(image:UIImage?, data:Data?, error:Error?, finished:Bool) in
if image != nil {.
}
})
Try APSmartStorage (https://github.com/Alterplay/APSmartStorage) instead of SDWebImage.
APSmartStorage gets data from network and automatically caches data on disk or in memory in a smart configurable way. Should be good enough for your task.
Yes is possible to download the image using SDWebImage And store into local memory Manually.
Downloaded Image store into local memory using SDWebImage
func saveImage(url: URL, toCache: UIImage?, complation: #escaping SDWebImageNoParamsBlock) {
guard let toCache = toCache else { return }
let manager = SDWebImageManager.shared()
if let key = manager.cacheKey(for: url) {
manager.imageCache?.store(toCache, forKey: key, completion: complation)
}
}
Load image from memory using image URL
static func imageFromMemory(for url: String) -> UIImage? {
if let encoded = url.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed),
let url = URL(string: encoded) {
let manager = SDWebImageManager.shared()
if let key: String = manager.cacheKey(for: url),
let image = manager.imageCache?.imageFromMemoryCache(forKey: key) {
return image
}
}
return nil
}
//CREATE A CUSTOME IMAGEVIEW AND PASS THE IMAGE URL BY ARRAY(INDEXPATH.ROW)
(void) loadImage:(NSString *)imageLink{
imageLink = [imageLink stringByReplacingOccurrencesOfString:#" " withString:#"%20"];
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager loadImageWithURL:[NSURL URLWithString:imageLink] options:SDWebImageDelayPlaceholder progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
} completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
imageFrame.image = image;
}];
}
Swift 5.1
import UIKit
import SDWebImage
extension UIImageView{
func downloadImage(url:String){
//remove space if a url contains.
let stringWithoutWhitespace = url.replacingOccurrences(of: " ", with: "%20", options: .regularExpression)
self.sd_imageIndicator = SDWebImageActivityIndicator.gray
self.sd_setImage(with: URL(string: stringWithoutWhitespace), placeholderImage: UIImage())
}
}
How to use
let myUrl = "https://www.example.com"
myImageView.downloadImage(url: myUrl)
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager downloadImageWithURL:ImageUrl options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize)
{
} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if(image){
NSLog(#"image=====%#",image);
}
}];

Memory warning in the iPad 1st generation and crash

i am developing an iPad application witch has a functionality downloading books. The books size are about 180 Mo. The books are in server and the have the extension .zip. I download the book (.zip) then i unzip it and then remove the .zip. I am doing like this :
- (BOOL)downloadBookWithRequest:(BookDownloadRequest*)book
{
if (![book isValid])
{
NSLog(#"Couldn't launch download since request had missing parameter");
return NO;
}
if ([self bookIsCurrrentlyDownloadingWithID:book.ID]) {
NSLog(#"Book already downloaded");
return NO;
}
ASIHTTPRequest *download = [[ASIHTTPRequest alloc] initWithURL:book.URL];
download.userInfo = book.dictionary;
download.downloadDestinationPath = [self downloadPathForBookID:book.ID];
download.downloadProgressDelegate = self.downloadVC.downloadProgress;
download.shouldContinueWhenAppEntersBackground = YES;
[self.downloadQueue addOperation:download];
[download release];
// Update total requests
self.requestsCount++;
[self refreshDownloadsCount];
if(self.downloadQueue.isSuspended)
[self.downloadQueue go];
[self.downloadVC show];
return YES;
}
- (void)requestFinished:(ASIHTTPRequest*)request
{
NSString *bookStoragePath = [[BooksManager booksStoragePath] stringByAppendingPathComponent:request.downloadDestinationPath.lastPathComponent.stringByDeletingPathExtension];
NSString *bookZipPath = request.downloadDestinationPath;
// Tell not to save the zip file into iCloud
[BooksManager addSkipBackupAttributeToItemAtPath:bookZipPath];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *removeExistingError = nil;
if ([fileManager fileExistsAtPath:bookStoragePath])
{
[fileManager removeItemAtPath:bookStoragePath error:&removeExistingError];
if (removeExistingError)
{
[self bookDidFailWithRequest:request errorMessageType:DownloadErrorTypeFILE_ERROR];
NSLog(#"ERROR: Couldn't remove existing book to unzip new download (%#)", removeExistingError);
} else
NSLog(#"INFO: Removed existing book to install new download");
}
ZipArchive* zip = [[ZipArchive alloc] init];
if([self isCompatibleWithFileAtPath:bookZipPath] && [zip UnzipOpenFile:bookZipPath])
{
BOOL unzipSucceeded = [zip UnzipFileTo:bookStoragePath overWrite:YES];
if (!unzipSucceeded)
{
[self bookDidFailWithRequest:request errorMessageType:DownloadErrorTypeFILE_ERROR];
NSLog(#"ERROR: Couldn't unzip file %#\n to %#",bookZipPath,bookStoragePath);
} else {
[self bookDidInstallWithRequest:request];
NSLog(#"INFO: Successfully unziped downloaded file");
}
[zip UnzipCloseFile];
}
else
{
[self bookDidFailWithRequest:request errorMessageType:DownloadErrorTypeFILE_ERROR];
NSLog(#"ERROR: Unable to open zip file %#\n",bookZipPath);
}
[self removeZipFileAtPath:bookZipPath];
[zip release];
}
-(BOOL) removeZipFileAtPath:(NSString*) bookZipPath {
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:bookZipPath])
{
NSError *removeZipFileError = nil;
[fileManager removeItemAtPath:bookZipPath error:&removeZipFileError];
if (removeZipFileError) {
NSLog(#"ERROR: Couldn't remove existing zip after unzip (%#)", removeZipFileError);
return NO;
}
else {
NSLog(#"INFO: Removed zip downloaded after unzip");
return YES;
}
}
return NO;
}
My Problem is : This code is working fine with iPhone 4/iPhone 4s/ iPad 2G /iPad3G, but it crash with an iPad 1st Generation (when unzipping the book) and the crash reporter says that the are Memory warning.
How question is, how i can optimize this code to avoid the memory warning and avoid the crash ? Thanks for your answers;
Edit : I have found that the problem is caused by this portion of code :
NSData *bookData = [[NSData alloc]initWithContentsOfFile:bookPath];
the bookPath is the path to the .zip ( about 180 Mo) and when i am in iPad 1G this line crash my application i.e. : i receive memory warnings and the system kill the App. Du you know how i can avoid this. I an using this line to calculate the MD5 of the book (.zip)
I have a category in NSData like this :
#import <CommonCrypto/CommonDigest.h>
#implementation NSData(MD5)
- (NSString*)MD5
{
// Create byte array of unsigned chars
unsigned char md5Buffer[CC_MD5_DIGEST_LENGTH];
// Create 16 byte MD5 hash value, store in buffer
CC_MD5(self.bytes, self.length, md5Buffer);
// Convert unsigned char buffer to NSString of hex values
NSMutableString *output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
for(int i = 0; i < CC_MD5_DIGEST_LENGTH; i++)
[output appendFormat:#"%02x",md5Buffer[i]];
return output;
}
How i can avoid the crash ? thanks
EDIT:
So, it seems that the culprit is loading into memory the whole file in order to calculate its MD5 hash.
The solution to this would be calculating the MD5 without having to load into memory the whole file. You can give a look at this post explaining how to compute efficiently an MD5 or SHA1 hash, with the relative code. Or if you prefer, you can go directly to github and grab the code.
Hope it helps.
OLD ANSWER:
You should inspect your app, especially the ZipArchive class, for memory leaks or not-released memory. You can use Instruments' Leaks and Memory Allocation tools to profile your app.
The explanation of the different behavior between iPad1 and the rest of devices may lay with their different memory footprint, as well as with different memory occupation states of the devices (say, you iPad 1 has less free memory when you run the app then the iPad 2 because of the state other apps you ran on the iPad 1 left the device in). You might think of rebooting the iPad 1 to inspect its behavior out of a fresh start.
In any case, besides the possible explanation of the different behaviors, the ultimate cause is how your app manages memory and Instruments is the way to go.
I don't agree with Sergio.
You are saying the app crashes when you init the NSData object with a 180mb zip archive.
Well it's natural you run out of memory, since the 1st gen iPad has half the memory of the 2nd gen... (256MB vs 512)
The solution is to split the zip archive in smaller parts and process them one by one.

Threads are killing my app

Looking for some advice on stablizing my app. First some requirements - files with PII (personally identifying information) must be encrypted when on disk. Tumbnails and logos are in the custom TableViewCells (if available) and must be decrypted before display.
There are several layers of threading going on. There is a central function getFileData that checks to see if files are on the device or if the files need to be obtained from the network. I desire to keep the UI responsive and (I think) therein lies my problem.
Here is some code:
This is the workhorse method for processing files in my application. It decides where the file is decrypts it and hands it back to a callback:
-(void)fetchFileData:(UserSession *) session
onComplete: (void(^)(NSData* data)) onComplete
{
NSURL *url = [File urlForMail:self.fileId andSession:session];
//NSLog(#"File id: %#", self.fileId);
NSString *encryptionKey = session.encryptionKey;
dispatch_queue_t cryptoQ = dispatch_queue_create(FILE_CRYPTOGRAPHY_QUEUE, NULL);
dispatch_async(cryptoQ, ^(void){
// Get the file and d/encrypt it
NSError *error = nil;
if ([File fileExistsAtUrl:url] == YES) {
NSLog(#"file is on disk.");
NSData *localEncryptedFile = [File getDataForFile:url];
NSData *decryptedFile = [RNDecryptor decryptData:localEncryptedFile
withPassword:encryptionKey
error:&error];
onComplete(decryptedFile);
dispatch_release(cryptoQ);
} else {
//NSLog(#"File is not on disk");
NSDictionary *remoteFile = [session.apiFetcher getFileContent:self.fileId
andToken:session.token];
if (remoteFile && [[remoteFile objectForKey:#"success"] isEqualToString:#"true"]) {
NSData *remoteFileData = [remoteFile objectForKey:#"data"];
NSString *mimeType = [remoteFile objectForKey:#"mimeType"];
self.mimeType = mimeType;
NSData *encryptedData = [RNEncryptor encryptData:remoteFileData
withSettings:kRNCryptorAES256Settings
password:encryptionKey
error:&error];
[encryptedData writeToURL:url atomically:YES];
onComplete(remoteFileData);
dispatch_release(cryptoQ);
}
}
});
Here is an example of a getFileData caller:
+(void)loadThumbnailForMail: (NSNumber*)thumbnailId
session: (UserSession*)session
callback: (void(^)(NSData* data))callback
{
File *file = [File findFile:thumbnailId inContext:session.mailDatabase.managedObjectContext];
dispatch_queue_t fetchQ = dispatch_queue_create(FILE_FETCHER_QUEUE_LABEL, NULL);
dispatch_async(fetchQ, ^(void) {
if (file) {
[file fetchFileData:session onComplete:^(NSData *data) {
if (data && file.mimeType) {
callback(data);
}
}];
}
});
dispatch_release(fetchQ);
}
Here is an example of the TableViewCell that is calling loadThumbnailForMail:
-(void)loadAndShowThumbnailImage:(Mail*) mail
{
UIImage *placeHolder = [UIImage imageNamed:#"thumbnail_placeholder.png"];
[self.thumbnailImageForMail setImage:placeHolder];
dispatch_queue_t loaderQ = dispatch_queue_create(THUMBNAIL_FETCHER, NULL);
dispatch_async(loaderQ, ^ {
[File loadThumbnailForMail: mail.thumbnailId
session: [UserSession instance]
callback: ^(NSData *data) {
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *thumbnailImage = [UIImage imageWithData:data];
[self.thumbnailImageForMail setImage:thumbnailImage];
});
}];
});
dispatch_release(loaderQ);
}
I think that my issue here is the callback in my loadThumbnailImage. If the user scrolls fast enough I suspect that there could be two threads trying to access the same TableViewCell
(MyCell *cell = (MyCell*)[tableView dequeueReusableCellWithIdentifier:CellTableIdentifier];)
It doesn't always happen right away but eventually, after some scrolling the tableView list of cells the app crashes with this: * Terminating app due to uncaught exception 'NSGenericException', reason: '* Collection <__NSCFSet: 0xde6a650> was mutated while being enumerated.'
I need to have the decrypted images in the cells and the first solution (above) does this for me when the images are available but causes the app to crash. I am wondering if some sort of in memory cache would help improve this if I put images in memory there when they were decrypted and checked that cache in loadAndShowThumbnailImage before I kick off all the threads to get and decrypt them.
Thoughts? I have been banging on this for a week now trying different things and would appreciate some fresh perspective.
Thanks.
Based on Justins link and subsequent research I ended up going in this direction: In a UITableView, best method to cancel GCD operations for cells that have gone off screen?

AssetsLibrary does not get images saved in the Camera roll when I run the program on the device

I wrote a simple iOS program to get number of photo images which are saved in the camera roll by using 'Assets Library' framework provided in the SDK4.2.
The program worked well as I expected when I ran it on the iPhone simulator.
But, it didn't retrieve any images when I ran on the 'real' iPhone device (iPhone 3GS with iOS 4.2.1).
This problem looks like as same as the problem discussed in the below article:
Assets Library Framework not working correctly on 4.0 and 4.2
So, I added the "dispatch_async(dispatch_get_main_queue()..." function as below, But I couldn't solve the problem.
- (void)viewDidLoad {
[super viewDidLoad];
NSMutableArray assets = [[NSMutableArray array] retain]; // Prepare array to have retrieved images by Assets Library.
void (^assetEnumerator)(struct ALAsset *, NSUInteger, BOOL *) = ^(ALAsset *asset, NSUInteger index, BOOL *stop) {
if(asset != NULL) {
[assets addObject:asset];
dispatch_async(dispatch_get_main_queue(), ^{
// show number of retrieved images saved in the Camera role.
// The [assets count] returns always 0 when I run this program on iPhone device although it worked OK on the simulator.
NSLog(#"%i", [assets count]);
});
}
};
void (^assetGroupEnumerator)(struct ALAssetsGroup *, BOOL *) = ^(ALAssetsGroup *group, BOOL *stop) {
if(group != nil) {
[group enumerateAssetsUsingBlock:assetEnumerator];
}
};
// Create instance of the Assets Library.
ALAssetsLibrary* library = [[ALAssetsLibrary alloc] init];
[library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos // Retrieve the images saved in the Camera role.
usingBlock:assetGroupEnumerator
failureBlock: ^(NSError *error) {
NSLog(#"Failed.");
}];
}
Could you please tell me if you have any ideas to solve it?
I have 1 update:
To get error code, I modified the failureBlock of the enumerateGroupsWithTypes as below, and then reproduced the symptom again.
Then, the app returned the error code -3311 (ALAssetsLibraryAccessUserDeniedError).
However I didn't any operation to deny while my reproducing test.
What's the possible cause of the err#=-3311?
[library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos
usingBlock:assetGroupEnumerator
failureBlock: ^(NSError *error) {
NSLog(#"Failed");
resultMsg = [NSString stringWithFormat:#"Failed: code=%d", [error code]]; }];
It is strange that location services should be involved when accessing saved photos. Maybe it has to do with geo-tagging information on the photos. Anyways Apple says that enabling location services is required when using enumerateGroupsWithTypes:usingBlock:failureBlock:
Special Considerations
This method will fail with error ALAssetsLibraryAccessGloballyDeniedError if the user has not enabled Location Services (in Settings > General)."

How to get [UIImage imageWithContentsOfFile:] and High Res Images working

As many people are complaining it seems that in the Apple SDK for the Retina Display there's a bug and imageWithContentsOfFile actually does not automatically load the 2x images.
I've stumbled into a nice post how to make a function which detects UIScreen scale factor and properly loads low or high res images ( http://atastypixel.com/blog/uiimage-resolution-independence-and-the-iphone-4s-retina-display/ ), but the solution loads a 2x image and still has the scale factor of the image set to 1.0 and this results to a 2x images scaled 2 times (so, 4 times bigger than what it has to look like)
imageNamed seems to accurately load low and high res images, but is no option for me.
Does anybody have a solution for loading low/high res images not using the automatic loading of imageNamed or imageWithContentsOfFile ? (Or eventually solution how to make imageWithContentsOfFile work correct)
Ok, actual solution found by Michael here :
http://atastypixel.com/blog/uiimage-resolution-independence-and-the-iphone-4s-retina-display/
He figured out that UIImage has the method "initWithCGImage" which also takes a scale factor as input (I guess the only method where you can set yourself the scale factor)
[UIImage initWithCGImage:scale:orientation:]
And this seems to work great, you can custom load your high res images and just set that the scale factor is 2.0
The problem with imageWithContentsOfFile is that since it currently does not work properly, we can't trust it even when it's fixed (because some users will still have an older iOS on their devices)
We just ran into this here at work.
Here is my work-around that seems to hold water:
NSString *imgFile = ...path to your file;
NSData *imgData = [[NSData alloc] initWithContentsOfFile:imgFile];
UIImage *img = [[UIImage alloc] initWithData:imgData];
imageWithContentsOfFile works properly (considering #2x images with correct scale) starting iOS 4.1 and onwards.
Enhancing Lisa Rossellis's answer to keep retina images at desired size (not scaling them up):
NSString *imagePath = ...Path to your image
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfFile:imagePath] scale:[UIScreen mainScreen].scale];
I've developed a drop-in workaround for this problem.
It uses method swizzling to replace the behavior of the "imageWithContentsOfFile:" method of UIImage.
It works fine on iPhones/iPods pre/post retina.
Not sure about the iPad.
Hope this is of help.
#import </usr/include/objc/objc-class.h>
#implementation NSString(LoadHighDef)
/** If self is the path to an image, returns the nominal path to the high-res variant of that image */
-(NSString*) stringByInsertingHighResPathModifier {
NSString *path = [self stringByDeletingPathExtension];
// We determine whether a device modifier is present, and in case it is, where is
// the "split position" at which the "#2x" token is to be added
NSArray *deviceModifiers = [NSArray arrayWithObjects:#"~iphone", #"~ipad", nil];
NSInteger splitIdx = [path length];
for (NSString *modifier in deviceModifiers) {
if ([path hasSuffix:modifier]) {
splitIdx -= [modifier length];
break;
}
}
// We insert the "#2x" token in the string at the proper position; if no
// device modifier is present the token is added at the end of the string
NSString *highDefPath = [NSString stringWithFormat:#"%##2x%#",[path substringToIndex:splitIdx], [path substringFromIndex:splitIdx]];
// We possibly add the extension, if there is any extension at all
NSString *ext = [self pathExtension];
return [ext length]>0? [highDefPath stringByAppendingPathExtension:ext] : highDefPath;
}
#end
#implementation UIImage (LoadHighDef)
/* Upon loading this category, the implementation of "imageWithContentsOfFile:" is exchanged with the implementation
* of our custom "imageWithContentsOfFile_custom:" method, whereby we replace and fix the behavior of the system selector. */
+(void)load {
Method originalMethod = class_getClassMethod([UIImage class], #selector(imageWithContentsOfFile:));
Method replacementMethod = class_getClassMethod([UIImage class], #selector(imageWithContentsOfFile_custom:));
method_exchangeImplementations(replacementMethod, originalMethod);
}
/** This method works just like the system "imageWithContentsOfFile:", but it loads the high-res version of the image
* instead of the default one in case the device's screen is high-res and the high-res variant of the image is present.
*
* We assume that the original "imageWithContentsOfFile:" implementation properly sets the "scale" factor upon
* loading a "#2x" image . (this is its behavior as of OS 4.0.1).
*
* Note: The "imageWithContentsOfFile_custom:" invocations in this code are not recursive calls by virtue of
* method swizzling. In fact, the original UIImage implementation of "imageWithContentsOfFile:" gets called.
*/
+ (UIImage*) imageWithContentsOfFile_custom:(NSString*)imgName {
// If high-res is supported by the device...
UIScreen *screen = [UIScreen mainScreen];
if ([screen respondsToSelector:#selector(scale)] && [screen scale]>=2.0) {
// then we look for the high-res version of the image first
UIImage *hiDefImg = [UIImage imageWithContentsOfFile_custom:[imgName stringByInsertingHighResPathModifier]];
// If such high-res version exists, we return it
// The scale factor will be correctly set because once you give imageWithContentsOfFile:
// the full hi-res path it properly takes it into account
if (hiDefImg!=nil)
return hiDefImg;
}
// If the device does not support high-res of it does but there is
// no high-res variant of imgName, we return the base version
return [UIImage imageWithContentsOfFile_custom:imgName];
}
#end
[UIImage imageWithContentsOfFile:] doesn't load #2x graphics if you specify an absolute path.
Here is a solution:
- (UIImage *)loadRetinaImageIfAvailable:(NSString *)path {
NSString *retinaPath = [[path stringByDeletingLastPathComponent] stringByAppendingPathComponent:[NSString stringWithFormat:#"%##2x.%#", [[path lastPathComponent] stringByDeletingPathExtension], [path pathExtension]]];
if( [UIScreen mainScreen].scale == 2.0 && [[NSFileManager defaultManager] fileExistsAtPath:retinaPath] == YES)
return [[[UIImage alloc] initWithCGImage:[[UIImage imageWithData:[NSData dataWithContentsOfFile:retinaPath]] CGImage] scale:2.0 orientation:UIImageOrientationUp] autorelease];
else
return [UIImage imageWithContentsOfFile:path];
}
Credit goes to Christof Dorner for his simple solution (which I modified and pasted here).