I am ultimately trying to convert an array of photos that are in a dictionary in url rep form to base64 to be sent over json.
Here is the dictionary code and log of it:
NSDictionary *dict = [self.form dictionaryWithValuesForKeys:keys];
NSLog(#"dict::%#",dict);
NSLog:
dict::{
boardLodgingFurnished = "<null>";
caption = "<null>";
cars = "";
photos = (
{
caption = "";
urlRep = "assets-library://asset/asset.JPG?id=CE8A426B-3B59-4172-8761-CC477F3BB3EE&ext=JPG";
},
{
caption = "";
urlRep = "assets-library://asset/asset.JPG?id=F4B68A42-1CA0-4880-9FB5-177CB091A28C&ext=JPG";
}
);
yearsAtLocation = "";
yearsInTheBusiness = "";
}
So for each photo in the dictionary I would like to take the urlRep and convert that to a base64 string and replace the urlRep with it in the dictionary.
What I have right now..not sure if I am going in the right direction:
for (id imageURL in [dict objectForKey:#"photos"])
{
ALAssetsLibrary *library = [ALAssetsLibrary new];
ALAsset *ourAsset = [self assetForURL:imageURL withLibrary:library];
/* Check out ALAssets */
NSLog(#"%#", ourAsset);
ALAssetRepresentation *representation = [ourAsset defaultRepresentation];
CGImageRef imageRef = [representation fullResolutionImage];
//TODO: Deal with JPG or PNG
NSData *imageData = UIImageJPEGRepresentation([UIImage imageWithCGImage:imageRef], 0.1);
NSLog(#"imagedata??%#", [imageData base64EncodedString]);
//need to know how to add this back to dict
}
the below method is called from above but crashes on the while loop with
-[__NSDictionaryI scheme]: unrecognized selector sent to instance 0x166dd090
2014-01-03 10:57:27.361 Inspection App[2728:60b] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSDictionaryI scheme]: unrecognized selector sent to instance 0x166dd090'
method
- (ALAsset *)assetForURL:(NSURL *)url withLibrary:(ALAssetsLibrary *)assetsLibrary {
__block ALAsset *result = nil;
__block NSError *assetError = nil;
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[assetsLibrary assetForURL:url resultBlock:^(ALAsset *asset) {
result = asset;
dispatch_semaphore_signal(sema);
} failureBlock:^(NSError *error) {
assetError = error;
dispatch_semaphore_signal(sema);
}];
if ([NSThread isMainThread]) {
while (!result && !assetError) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}
else {
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}
return result;
}
edit:
if (photoUrls.count) {
for (id photos in photoUrls){
NSString *urlString = photos;
[self base64ImageAtUrlString:urlString result:^(NSString *base64) {
NSLog(#"imagedata??%#", base64);
}];
}
}
else {
NSLog(#"where are my urls?");
}
NSMutableDictionary *jsonWithPhotos = [dict mutableCopy];
[jsonWithPhotos setObject:convertedImages forKey:#"photo64"];
NSLog(#"jjson photos::%#", jsonWithPhotos);
updated method
- (void)base64ImageAtUrlString:(NSString *)urlString result:(void (^)(NSString *))completion {
NSURL *url = [NSURL URLWithString:urlString];
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
[library assetForURL:url resultBlock:^(ALAsset *asset) {
// borrowing your code, here... didn't check it....
ALAssetRepresentation *representation = [asset defaultRepresentation];
CGImageRef imageRef = [representation fullResolutionImage];
//TODO: Deal with JPG or PNG
NSData *imageData = UIImageJPEGRepresentation([UIImage imageWithCGImage:imageRef], 0.1);
NSString *base64 = [imageData base64EncodedString];
completion(base64);
[convertedImages addObject:base64];
// NSLog(#"converted::%#",convertedImages);
} failureBlock:^(NSError *error) {
NSLog(#"that didn't work %#", error);
}];
}
When i log jsonWithPhotos the object photo64 is just a blank array
The crash is due to a false assumption in the code about the dictionary. Given the posted description of the dictionary parsed as json, you'd need to get the urls like this:
// collect the photo urls in an array
NSMutableArray *photoUrls = [NSMutableArray array];
// photos is an array of dictionaries in the dictionary
NSArray *photos = dict[#"photos"];
for (NSDictionary *photo in photos) {
// photo is a dictionary containing a "caption" and a "urlRep"
[photoUrls addObject:photo[#"urlRep"]];
}
Now you can proceed with a method whose job is just the conversion. Your question might contain a lot more questions about how to do this. I'd recommend starting simple. See if you can do one conversion. Test it by writing the reverse, from base64 back to an image.
Edit 0: Without deeply checking it, I'd restructure your encoding attempt to look like this:
- (void)base64ImageAtUrlString:(NSString *)urlString result:(void (^)(NSString *))completion {
NSURL *url = [NSURL URLWithString:urlString];
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
[library assetForURL:url resultBlock:^(ALAsset *asset) {
// borrowing your code, here... didn't check it....
ALAssetRepresentation *representation = [asset defaultRepresentation];
CGImageRef imageRef = [representation fullResolutionImage];
//TODO: Deal with JPG or PNG
NSData *imageData = UIImageJPEGRepresentation([UIImage imageWithCGImage:imageRef], 0.1);
NSString *base64 = [imageData base64EncodedString];
completion(base64);
} failureBlock:^(NSError *error) {
NSLog(#"that didn't work %#", error);
}];
}
Call it like this:
if (photoUrls.count) {
NSString *urlString = photoUrls[0];
[self base64ImageAtUrlString:urlString result:^(NSString *base64) {
NSLog(#"imagedata??%#", base64);
}];
} else {
NSLog(#"where are my urls?");
}
Once it's working, see if you can reverse it, making an image out of the base64 data. Finally, once all that's working, you can deal with potential memory issues. My advice there is to consider encoding one at a time, post one at a time to the server and release everything in between.
Edit 1 - Per followup question, if you want to replace all of the urls in the url array with base64 encodings, it might go something like this (remember that this might use a lot of memory):
- (void)base64ImagesAtUrls:(NSMutableArray *)urls result:(void (^)(void))completion {
__block NSInteger completed = 0; // this is how we'll know that we're done
// this approach doesn't depend on the asset library retrievals completing
// sequentially, even though they probably will
for (int i=0; i<urls.count; i++) {
NSString *urlString = urls[i];
[self base64ImageAtUrlString:urlString result:^(NSString *base64) {
[urls replaceObjectAtIndex:i withObject:base64];
if (++completed == urls.count) completion();
}];
}
}
Related
Usually when i want to read some image metadata in iOS i use imagePickerController to choose the image and Photos Framework and imagePickerController:didFinishPickingMediaWithInfo:
to get image info and extract the metadata like this
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info{
UIImagePickerControllerSourceType pickerType = picker.sourceType;
if(pickerType == UIImagePickerControllerSourceTypePhotoLibrary)
{
NSURL *url = [info objectForKey:UIImagePickerControllerReferenceURL];
PHFetchResult *fetchResult = [PHAsset fetchAssetsWithALAssetURLs:#[url,] options:nil];
PHAsset *asset = fetchResult.firstObject;
[self metaDataFromPhotoLibrary:asset];
[self dismissViewControllerAnimated:YES completion:NULL];
}
}
-(void)metaDataFromPhotoLibrary:(PHAsset*)asset{
// NSLog(#"Start metadata");
PHContentEditingInputRequestOptions *options = [[PHContentEditingInputRequestOptions alloc] init];
options.networkAccessAllowed = YES; //download asset metadata from iCloud if needed
[asset requestContentEditingInputWithOptions:options completionHandler:^(PHContentEditingInput *contentEditingInput, NSDictionary *info) {
CIImage *fullImage = [CIImage imageWithContentsOfURL:contentEditingInput.fullSizeImageURL];
NSDictionary *metadata = fullImage.properties;
NSMutableDictionary *imageMetadata = nil;
imageMetadata = [[NSMutableDictionary alloc] initWithDictionary:metadata];
NSString *dateString = metadata [#"{TIFF}"] [#"DateTime"];
NSString *latitude = metadata [#"{GPS}"][#"Latitude"];
NSString *longitude = metadata [#"{GPS}"][#"Longitude"];
//etc etc etc
}
But can't do the same thing from an Action Extension
in the extension code i use a code like this to get the selected image
- (void)viewDidLoad {
[super viewDidLoad];
// Get the item[s] we're handling from the extension context.
// For example, look for an image and place it into an image view.
// Replace this with something appropriate for the type[s] your extension supports.
BOOL imageFound = NO;
for (NSExtensionItem *item in self.extensionContext.inputItems) {
for (NSItemProvider *itemProvider in item.attachments) {
if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]) {
// This is an image. We'll load it, then place it in our image view.
__weak UIImageView *immagine = self.immagine;
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeImage options:nil completionHandler:^(UIImage *image, NSError *error) {
if(image) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[immagine setImage:image];
}];
}
}];
imageFound = YES;
break;
}
}
if (imageFound) {
// We only handle one image, so stop looking for more.
break;
}
}
}
Using UIImagePNGRepresentation or UIImageJPEGRepresentation i lost many metadata and i can read only a few data !
How can i get all image metadata from the image selected from action Extension ?
Thank you so mush
Note: i 've found some app in the AppsStore that read all metadata dictionary from extension , so there must be a solution for my problem !
Thank you again
Vanni
I have fetched the images and videos from gallery, it works fine when the number of videos and images is minimum,but when the number of images and videos is large, it blocks the main thread.
-(void)viewDidAppear:(BOOL)animated{
[NSThread sleepForTimeInterval:1];
// call the same method on a background thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self getAllVideosAndImages];
});
}
To gat Images and Videos From Asset
-(void)getAllVideosAndImages{
NSMutableArray *assetGroups = [[NSMutableArray alloc] init];
assetGroups = [[NSMutableArray alloc] init];
library = [[ALAssetsLibrary alloc] init];
NSUInteger groupTypes = ALAssetsGroupAll;
void (^assetEnumerator)
( ALAsset *, NSUInteger, BOOL *) = ^(ALAsset *result, NSUInteger index, BOOL *stop) {
if(result != nil) {
if([[result valueForProperty:ALAssetPropertyType] isEqualToString:ALAssetTypeVideo]||[[result valueForProperty:ALAssetPropertyType] isEqualToString:ALAssetTypePhoto]) {
ALAssetRepresentation *rep = [result defaultRepresentation];
CGImageRef iref = [rep fullResolutionImage];
if (iref) {
//GET DATE
NSDate *myDate = [result valueForProperty:ALAssetPropertyDate];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:#"MMM dd,YYYY"];
NSString *realDateStr = [dateFormatter stringFromDate:myDate];
//GET IMG AND VIDEO
UIImage *largeimage = [UIImage imageWithCGImage:iref];
NSData *webData = UIImagePNGRepresentation(largeimage);
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *localFilePath = [documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:#"img%zd.mov",imgName]];
imgName=imgName+1;
[webData writeToFile:localFilePath atomically:YES];
Para_ImageSet *objImgSet=[Para_ImageSet new];
objImgSet.imagePath=localFilePath;
objImgSet.imageDate=realDateStr;
// [yArrayOfAllImges addObject:objImgSet];
if ([[result valueForProperty:ALAssetPropertyType] isEqualToString:ALAssetTypeVideo]) {
objImgSet.is_video=YES;
}
else
{
objImgSet.is_video=NO;
}
if ([tempDate isEqualToString:#""]) {
yArrayOfImagesWithDate=[NSMutableArray new];
[yArrayOfImagesWithDate addObject:objImgSet];
tempDate=realDateStr;
}else
{
//GATE IMAGE COUNT
if ([realDateStr isEqualToString:tempDate]) {
//DO NOTHING
[yArrayOfImagesWithDate addObject:objImgSet];
}
else
{
[yArrayOFNNestedImgs addObject:yArrayOfImagesWithDate];
NSLog(#" ----------ImagesWithDate =%zd, %#",yArrayOfImagesWithDate.count,tempDate);
yArrayOfImagesWithDate=[NSMutableArray new];
[yArrayOfImagesWithDate addObject:objImgSet];
tempDate=realDateStr;
}
}
}
}
}
[self.tableView reloadData];
};
void (^ assetGroupEnumerator) ( ALAssetsGroup *, BOOL *)= ^(ALAssetsGroup *group, BOOL *stop) {
if(group != nil) {
[group enumerateAssetsUsingBlock:assetEnumerator];
[assetGroups addObject:group];
// NSNumber *countObj = [NSNumber numberWithInt:count];
NSLog(#" ----------ImagesWithDate =%zd, %#",yArrayOfImagesWithDate.count,tempDate);
[yArrayOFNNestedImgs addObject:yArrayOfImagesWithDate];
NSLog(#" ----------Count =%zd",yArrayOFNNestedImgs.count);
[NSThread sleepForTimeInterval:10];
// update UI on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
}
};
[library enumerateGroupsWithTypes:groupTypes
usingBlock:assetGroupEnumerator
failureBlock:^(NSError *error) {NSLog(#"A problem occurred");}];
}
Please Suggest..Thanks in advance.
[NSThread sleepForTimeInterval:1]; is blocking main thread for a second. It's not needed.
You could use [self performSelectorInBackground:#selector(getAllVideosAndImages) withObject:nil];
Also, wrap first [self.tableView reloadData]; in
dispatch_async(dispatch_get_main_queue(), ^{
...
}); too
In order to lazily load the resources, here is the recommended approach:
Download URLs of all resources and store them into your container (array / objects etc)
Fire a NSURLSessionTask (post iOS 7 only) which runs async on background queue. If you are below iOS 7, you can use NSURLConnection SendAsynchronousRequest API - it's deprecated in iOS 9 so you better get rid of that soon.
Process your downloaded resources while on background queue - store audio/image files, create UIImage from NSData, and so on.
Come back to main queue, then update the relevant UI part with downloaded content. If audio/video, play it. If UIImage, display it.
Here - the entire approach is described in my tutorial.
I am trying to get the name of the image which I have just captured from camera with following code. But [info objectForKey:#"UIImagePickerControllerReferenceURL"] always returning nil. How can I get the URL?
- (void)imagePickerController:(UIImagePickerController *)picker
didFinishPickingMediaWithInfo:(NSDictionary *)info {
self.myinfo = info;
NSLog(#"Dismissing camera ui...");
[self.cameraUI dismissViewControllerAnimated:YES completion:nil];
NSLog(#"Getting media url...");
NSString *mediaURL = [info objectForKey:UIImagePickerControllerMediaURL];
NSLog(#"Media url = %#", mediaURL);
NSLog(#"Getting media type...");
NSString *mediaType = [info objectForKey:UIImagePickerControllerMediaType];
NSLog(#"Selected mediaType: %#", mediaType);
if(mediaURL) {
NSLog(#"This is a video = %#", mediaURL);
if (![mediaType isEqualToString:(NSString*)kUTTypeVideo]) {
UISaveVideoAtPathToSavedPhotosAlbum(mediaURL, self, #selector(video:didFinishSavingWithError:contextInfo:), NULL);
}
} else {
NSLog(#"This is a photo...");
self.originalImage = (UIImage *) [info objectForKey:UIImagePickerControllerOriginalImage];
if (self.source == UIImagePickerControllerSourceTypeCamera && [mediaType isEqualToString:(NSString*)kUTTypeImage]) {
// Image captured from camera
NSLog(#"Saving new image...");
if (self.source != UIImagePickerControllerSourceTypePhotoLibrary) {
UIImageWriteToSavedPhotosAlbum(self.originalImage, self,
#selector(image:didFinishSavingWithError:usingContextInfo:), nil);
}
}
// Image selected from previous images.
else {
NSLog(#"Getting reference url...");
self.referenceURL = [info objectForKey:#"UIImagePickerControllerReferenceURL"];
NSLog(#"Reference url = %#", [self.referenceURL absoluteString]);
[self saveAssetData:self.originalImage :info];
}
}
}
- (void)image:(UIImage *)image
didFinishSavingWithError:(NSError *)error
usingContextInfo:(void*)ctxInfo {
if (error) {
NSLog(#"Resim kaydedilemedi: %#", [error localizedDescription]);
NSString *title = #"Resim kaydedilemedi!";
NSString* message = #"Resim kaydedilirken hata oluştu!";
[self alertStatus:message:title];
} else {
NSLog(#"Save asset data...");
[self saveAssetData:image :self.myinfo];
}
}
- (void)saveAssetData:(UIImage*)originalImage :(NSDictionary*)info {
self.assetLibrary = [[ALAssetsLibrary alloc] init];
NSURL *url = [info objectForKey:#"UIImagePickerControllerReferenceURL"];
ALAssetsLibraryAssetForURLResultBlock resultblock = ^(ALAsset *asset)
{
ALAssetRepresentation *assetRep = [asset defaultRepresentation];
NSString *filename = [assetRep filename];
NSLog(#"File name = %#", filename);
if(self.selectedMediaNames == nil)
self.selectedMediaNames = [[NSMutableArray alloc] init];
[self.selectedMediaNames addObject:filename];
[self.tableView reloadData];
[self.activitIndicator stopAnimating];
[self.activitIndicator setHidden:true];
HMXSharedDataManager *sharedDataManager =
[HMXSharedDataManager sharedManager];
[sharedDataManager.uploaMedias addObject:originalImage];
[sharedDataManager.uploaMediaNames addObject:filename];
};
ALAssetsLibraryAccessFailureBlock failureblock = ^(NSError *error)
{
NSLog(#"%#", error);
};
[self.assetLibrary assetForURL:url resultBlock:resultblock failureBlock:failureblock];
}
UPDATE:
It is a little bit late but here how I get the name of the image or video:
Check UIImagePickerControllerMediaURL, if it is null the media is an image if not it is a video
If the image or the video is just taken or recorded save it to photos album
Use ALAssetsLibrary to query file name.
Here is the code for saving and getting media:
- (void)imagePickerController:(UIImagePickerController *)picker
didFinishPickingMediaWithInfo:(NSDictionary *)info {
#try {
[self.cameraUI dismissViewControllerAnimated:YES completion:nil];
mediaURL = [info objectForKey:UIImagePickerControllerMediaURL];
// If mediaURL is not null this should be a video
if(mediaURL) {
// This video is new just recorded with camera
if (self.source == UIImagePickerControllerSourceTypeCamera) {
// First save the video to photos album
ALAssetsLibrary *library = [ALAssetsLibrary new];
[library writeVideoAtPathToSavedPhotosAlbum:mediaURL completionBlock:^(NSURL *assetURL, NSError *error){
if (error) {
DDLogDebug(#"Failed to save the photo to photos album...");
} else {
// Get the name of the video
[self getMediaName:nil url:assetURL];
}
}];
} else { // This is a video that recorded before
// Get the name of the video
[self getMediaName:nil url:[info objectForKey:UIImagePickerControllerReferenceURL]];
}
}
// This is an image
else {
self.originalImage = (UIImage*)[info objectForKey:UIImagePickerControllerOriginalImage];
// This image is new just taken with camera
if (self.source == UIImagePickerControllerSourceTypeCamera) {
// First save the image to photos album
ALAssetsLibrary *library = [ALAssetsLibrary new];
[library writeImageToSavedPhotosAlbum:[self.originalImage CGImage]
orientation:(ALAssetOrientation)[self.originalImage imageOrientation]
completionBlock:^(NSURL *assetURL, NSError *error){
if (error) {
DDLogDebug(#"Failed to save the vide to photos album...");
} else {
// Get the name of the image
[self getMediaName:self.originalImage url:assetURL];
}
}];
} else { // This is an image that taken before
// Get the name of the image
[self getMediaName:self.originalImage
url:[info objectForKey:#"UIImagePickerControllerReferenceURL"]];
}
}
}
#catch (NSException *exception) {
DDLogError(#"%#", [exception description]);
}
}
Actual method that gets the media name:
- (void)getMediaName:(UIImage*)originalImage url:(NSURL*)url {
#try {
ALAssetsLibraryAssetForURLResultBlock resultblock = ^(ALAsset *asset) {
if (asset == nil) return;
ALAssetRepresentation *assetRep = [asset defaultRepresentation];
NSString *fileName = [assetRep filename];
// Do what you need with the file name here
};
ALAssetsLibraryAccessFailureBlock failureblock = ^(NSError *error) {
DDLogError(#"Failed to get image or video name : %#", error);
};
ALAssetsLibrary *library = [ALAssetsLibrary new];
[library assetForURL:url resultBlock:resultblock failureBlock:failureblock];
}
#catch (NSException *exception) {
DDLogError(#"%#", [exception description]);
}
}
The image that you capture with the camera from within the application has no name. It is always nil. You have to programmatically save that image in the photo gallery and you can save with any name you want.
Put the following code in didFinishPickingMediaWithInfo:
NSURL *mediaUrl;
NSString *imageURLString;
self.selectImage = [info valueForKey:UIImagePickerControllerEditedImage];
if (mediaUrl == nil) {
if (self.selectImage == nil) {
self.selectImage = [info valueForKey:UIImagePickerControllerOriginalImage];
DebugLog(#"Original image picked.");
}else {
DebugLog(#"Edited image picked.");
}
}
mediaUrl = (NSURL *)[info valueForKey:UIImagePickerControllerMediaURL];
imageURLString=[mediaUrl absoluteString];
DebugLog(#"Hi Image URL STRING : - %#",imageURLString);
if ([StringUtils string:imageURLString contains:#"PNG"] || [StringUtils string:imageURLString contains:#"png"]) {
self.isJPG = NO;
self.profileImageName = #"profileImageName.png";
} else if ([StringUtils string:imageURLString contains:#"JPG"] || [StringUtils string:imageURLString contains:#"jpg"]) {
self.isJPG = YES;
self.profileImageName = #"profileImageName.jpg";
}
When you set camera for kUTTypeMovie , then only you will get referenceurl and mediaurl. It will return null for kUTTypeImage.
For Xamarin.iOS developers: store image capture from camera and get its data using ALAssetsLibrary
var originalImage = e.Info[UIImagePickerController.OriginalImage] as UIImage;
var meta = e.Info[UIImagePickerController.MediaMetadata] as NSDictionary;
//Get image bytes
if (originalImage != null)
{
using (NSData imageData = originalImage.AsPNG())
{
myByteArray = new Byte[imageData.Length];
System.Runtime.InteropServices.Marshal.Copy(imageData.Bytes, myByteArray, 0, Convert.ToInt32(imageData.Length));
}
//This bit of code saves image to the Photo Album with metadata
ALAssetsLibrary library = new ALAssetsLibrary();
library.WriteImageToSavedPhotosAlbum(originalImage.CGImage, meta, (assetUrl, error) =>
{
library.AssetForUrl(assetUrl, delegate (ALAsset asset)
{
ALAssetRepresentation representation = asset.DefaultRepresentation;
if (representation != null)
{
string fileName = representation.Filename;
var filePath = assetUrl.ToString();
var extension = filePath.Split('.')[1].ToLower();
var mimeData = string.Format("image/{0}", extension);
var mimeType = mimeData.Split('?')[0].ToLower();
var documentName = assetUrl.Path.ToString().Split('/')[1];
}
}, delegate (NSError err) {
Console.WriteLine("User denied access to photo Library... {0}", err);
});
});
}
So I'm building an app that the user takes pictures of themselves, it saves them to the camera roll, and I'm saving references to the asset URLs to display them in the app. At first this model seemed to work fine, but as I took more and more pictures it started receiving memory warnings and eventually crashed. Is there a better way to approach this?
This is how I load up the saved photos at the launch of the app (which freezes the app for up to 10 seconds depending on how many are being loaded):
- (void) loadPhotosArray
{
_photos = [[NSMutableArray alloc] init];
NSData* data = [[NSUserDefaults standardUserDefaults] objectForKey: #"savedImages"];
if (data)
{
NSArray* storedUrls = [[NSArray alloc] initWithArray: [NSKeyedUnarchiver unarchiveObjectWithData: data]];
// reverse array
NSArray* urls = [[storedUrls reverseObjectEnumerator] allObjects];
for (NSURL* assetUrl in urls)
{
// Block to handle image handling success
ALAssetsLibraryAssetForURLResultBlock resultblock = ^(ALAsset *myasset)
{
ALAssetRepresentation *rep = [myasset defaultRepresentation];
CGImageRef iref = [rep fullResolutionImage];
if (iref) {
UIImage* tempImage = [UIImage imageWithCGImage:iref];
UIImage* image = [[UIImage alloc] initWithCGImage: tempImage.CGImage scale: 1.0 orientation: UIImageOrientationRight];
// Set image in imageView
[_photos addObject: image];
[[NSNotificationCenter defaultCenter] postNotificationName: #"PhotosChanged" object: self];
}
};
// Handles failure of getting image
ALAssetsLibraryAccessFailureBlock failureblock = ^(NSError *myerror)
{
NSLog(#"Can't get image - %#",[myerror localizedDescription]);
};
// Load image then call appropriate block
ALAssetsLibrary* assetslibrary = [[ALAssetsLibrary alloc] init];
[assetslibrary assetForURL: assetUrl
resultBlock: resultblock
failureBlock: failureblock];
}
}
else
{
NSLog(#"Photo storage is empty");
}
}
And saving photos:
- (void) addImageToPhotos: (UIImage*)image
{
// Store image at front of array
NSMutableArray* temp = [[NSMutableArray alloc] initWithObjects: image, nil];
// load rest of images onto temp array
for (UIImage* image in _photos)
{
[temp addObject: image];
}
_photos = nil;
_photos = [[NSMutableArray alloc] initWithArray: temp];
// [self.photos addObject: image];
[[NSNotificationCenter defaultCenter] postNotificationName: #"PhotosChanged" object: self.photos];
// save to cache
ALAssetsLibrary* library = [[ALAssetsLibrary alloc] init];
[library saveImage: image toAlbum: #kAlbumeName withCompletionBlock:^(NSError *error) {
if (error)
{
NSLog(#"Error saving");
}
}];
}
I think have 2 methods to optimize this problem.
U should just save image name string instead of saving UIImage object, then when need to display the image, use pagination to display image according to saved image name string.
U should use multi-thread to deal with this long time task, recommend u to use gcd to load image name string.
I have a singleton that loads up an a bunch of ALAssets when my app launches. This is causing the main thread to freeze for more then 10 seconds while it loads each image into memory. Obviously a big no no.
I tried to put it on a background thread, but it only partially executes.
+ (CCPhotos*) sharedPhotos
{
static CCPhotos* shared = nil;
if (!shared)
{
shared = [[CCPhotos alloc] init];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
[shared loadPhotosArray];
});
}
return shared;
}
- (void) loadPhotosArray
{
NSLog(#"Loading photos");
_photos = [[NSMutableArray alloc] init];
NSData* data = [[NSUserDefaults standardUserDefaults] objectForKey: #"savedImages"];
if (data)
{
NSArray* storedUrls = [[NSArray alloc] initWithArray: [NSKeyedUnarchiver unarchiveObjectWithData: data]];
// reverse array
NSArray* urls = [[storedUrls reverseObjectEnumerator] allObjects];
for (NSURL* assetUrl in urls)
{
NSLog(#"Looking up %#", assetUrl);
// Block to handle image handling success
// This initializes, but doesn't get called
////-->>
ALAssetsLibraryAssetForURLResultBlock resultblock = ^(ALAsset *myasset)
{
ALAssetRepresentation *rep = [myasset defaultRepresentation];
CGImageRef iref = [rep fullScreenImage];
if (iref) {
UIImage* tempImage = [UIImage imageWithCGImage:iref];
UIImage* image = [[UIImage alloc] initWithCGImage: tempImage.CGImage scale: 1.0 orientation: UIImageOrientationUp];
// Set image in imageView
[_photos addObject: image];
NSLog(#"Added photo with url: %#", [rep url]);
[[NSNotificationCenter defaultCenter] postNotificationName: #"PhotosChanged" object: self];
}
};
// Handles failure of getting image
ALAssetsLibraryAccessFailureBlock failureblock = ^(NSError *myerror)
{
NSLog(#"Can't get image - %#",[myerror localizedDescription]);
};
// Load image then call appropriate block
ALAssetsLibrary* assetslibrary = [[ALAssetsLibrary alloc] init];
[assetslibrary assetForURL: assetUrl
resultBlock: resultblock
failureBlock: failureblock];
}
}
else
{
NSLog(#"Photo storage is empty");
}
}
I've narrowed down the problem to ALAssetsLibraryAssetForURLResultBlock resultblock which doesn't get called. Multiple threads spawn at the beginning, and each one gets to this line, initializes the result block, but doesn't call it. I think it has to do with the thread safety on the block. Any thoughts?
Try doing this when you post the notification:
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName: #"PhotosChanged" object: self];
});
I believe the reason it is not working for you is because this is not being called on the main thread. All UI changes/updates (which I am assuming this leads to) must be executed on the main thread. This will force the notification to happen on the necessary thread and should work properly!
This is a pretty common thing to do. I actually created a code snippet because I found myself typing this out so often:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
<#on back thread#>
dispatch_async(dispatch_get_main_queue(), ^{
<#on main thread#>
});
});
I then set the completion short cut to dispatch_async
From what I'm understanding, you are spinning up multiple instances of your singleton. In that case you will probably want to put a #synchronized block in there. For example:
+ (CCPhotos*) sharedPhotos
{
static CCPhotos* shared = nil;
#synchronized ([CCPhotos class])
{
if (!shared)
{
shared = [[CCPhotos alloc] init];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
[shared loadPhotosArray];
});
}
}
return shared;
}
Hope that helps.