I'm having a hard figuring out how to programmatically retrieve the most recent photo in the camera roll without user intervention. To be clear, I do not want to use an Image Picker, I want the app to automatically grab the newest photo when the app opens.
I know this is possible because Ive seen a similar app do it, but I cant seem to find any info on it.
One way is to use AssetsLibrary and use n - 1 as the index for enumeration.
ALAssetsLibrary *assetsLibrary = [[ALAssetsLibrary alloc] init];
[assetsLibrary enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos
usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
if (nil != group) {
// be sure to filter the group so you only get photos
[group setAssetsFilter:[ALAssetsFilter allPhotos]];
if (group.numberOfAssets > 0) {
[group enumerateAssetsAtIndexes:[NSIndexSet indexSetWithIndex:group.numberOfAssets - 1]
options:0
usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {
if (nil != result) {
ALAssetRepresentation *repr = [result defaultRepresentation];
// this is the most recent saved photo
UIImage *img = [UIImage imageWithCGImage:[repr fullResolutionImage]];
// we only need the first (most recent) photo -- stop the enumeration
*stop = YES;
}
}];
}
}
*stop = NO;
} failureBlock:^(NSError *error) {
NSLog(#"error: %#", error);
}];
Instead of messing with the index, you can enumerate through the list in reverse. This pattern works well if you want the most recent image or if you want to list the images in a UICollectionView with the most recent image first.
Example to return the most recent image:
[group enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *asset, NSUInteger index, BOOL *stop) {
if (asset) {
ALAssetRepresentation *repr = [asset defaultRepresentation];
UIImage *img = [UIImage imageWithCGImage:[repr fullResolutionImage]];
*stop = YES;
}
}];
In iOS 8, Apple added the Photos library which makes for easier querying. In iOS 9, ALAssetLibrary is deprecated.
Here's some Swift code to get the most recent photo taken with that framework.
import UIKit
import Photos
struct LastPhotoRetriever {
func queryLastPhoto(resizeTo size: CGSize?, queryCallback: (UIImage? -> Void)) {
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
// fetchOptions.fetchLimit = 1 // This is available in iOS 9.
if let fetchResult = PHAsset.fetchAssetsWithMediaType(PHAssetMediaType.Image, options: fetchOptions) {
if let asset = fetchResult.firstObject as? PHAsset {
let manager = PHImageManager.defaultManager()
// If you already know how you want to resize,
// great, otherwise, use full-size.
let targetSize = size == nil ? CGSize(width: asset.pixelWidth, height: asset.pixelHeight) : size!
// I arbitrarily chose AspectFit here. AspectFill is
// also available.
manager.requestImageForAsset(asset,
targetSize: targetSize,
contentMode: .AspectFit,
options: nil,
resultHandler: { image, info in
queryCallback(image)
})
}
}
}
}
Swift 3.0:
1) Import Photos framework in your header before your class declaration.
import Photos
2) Add the following method, which returns the last image.
func queryLastPhoto(resizeTo size: CGSize?, queryCallback: #escaping ((UIImage?) -> Void)) {
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
let requestOptions = PHImageRequestOptions()
requestOptions.isSynchronous = true
let fetchResult = PHAsset.fetchAssets(with: PHAssetMediaType.image, options: fetchOptions)
if let asset = fetchResult.firstObject {
let manager = PHImageManager.default()
let targetSize = size == nil ? CGSize(width: asset.pixelWidth, height: asset.pixelHeight) : size!
manager.requestImage(for: asset,
targetSize: targetSize,
contentMode: .aspectFit,
options: requestOptions,
resultHandler: { image, info in
queryCallback(image)
})
}
}
3) Then call this method somewhere in your app (maybe a button action):
#IBAction func pressedLastPictureAttachmentButton(_ sender: Any) {
queryLastPhoto(resizeTo: nil){
image in
print(image)
}
}
To add to Art Gillespie's answer, using the fullResolutionImage uses the original image which — depending on the device's orientation when taking the photo — could leave you with an upside down, or -90° image.
To get the modified, but optimised image for this, use fullScreenImage instead....
UIImage *img = [UIImage imageWithCGImage:[repr fullScreenImage]];
Answer to the question (in Swift):
func pickingTheLastImageFromThePhotoLibrary() {
let assetsLibrary: ALAssetsLibrary = ALAssetsLibrary()
assetsLibrary.enumerateGroupsWithTypes(ALAssetsGroupSavedPhotos,
usingBlock: { (let group: ALAssetsGroup!, var stop: UnsafeMutablePointer<ObjCBool>) -> Void in
if (group != nil) {
// Be sure to filter the group so you only get photos
group.setAssetsFilter(ALAssetsFilter.allPhotos())
group.enumerateAssetsWithOptions(NSEnumerationOptions.Reverse,
usingBlock: { (let asset: ALAsset!,
let index: Int,
var stop: UnsafeMutablePointer<ObjCBool>)
-> Void in
if(asset != nil) {
/*
Returns a CGImage representation of the asset.
Using the fullResolutionImage uses the original image which — depending on the
device's orientation when taking the photo — could leave you with an upside down,
or -90° image. To get the modified, but optimised image for this, use
fullScreenImage instead.
*/
// let myCGImage: CGImage! = asset.defaultRepresentation().fullResolutionImage().takeUnretainedValue()
/*
Returns a CGImage of the representation that is appropriate for displaying full
screen.
*/
// let myCGImage: CGImage! = asset.defaultRepresentation().fullScreenImage().takeUnretainedValue()
/* Returns a thumbnail representation of the asset. */
let myCGImage: CGImage! = asset.thumbnail().takeUnretainedValue()
// Here we set the image included in the UIImageView
self.myUIImageView.image = UIImage(CGImage: myCGImage)
stop.memory = ObjCBool(true)
}
})
}
stop.memory = ObjCBool(false)
})
{ (let error: NSError!) -> Void in
println("A problem occurred: \(error.localizedDescription)")
}
}
Using Photos Library ( Objective-C )
PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init];
fetchOptions.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"creationDate" ascending:NO]];
PHFetchResult *assetsFetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:fetchOptions];
if (assetsFetchResult.count>0) {
PHAsset *asset = [assetsFetchResult objectAtIndex:0];
CGFloat scale = [UIScreen mainScreen].scale;
CGFloat dimension = 55.0f; // set your required size
CGSize size = CGSizeMake(dimension*scale, dimension*scale);
[[PHImageManager defaultManager] requestImageForAsset:asset
targetSize:size
contentMode:PHImageContentModeAspectFit
options:nil
resultHandler:^(UIImage *result, NSDictionary *info) {
// do your thing with the image
}
];
}
Related
I want to create a method to cache an image from an URL, I got the code in Swift since I had used it before, how can I do something similar to this in Objective-C:
import UIKit
let imageCache: NSCache = NSCache<AnyObject, AnyObject>()
extension UIImageView {
func loadImageUsingCacheWithUrlString(urlString: String) {
self.image = nil
if let cachedImage = imageCache.object(forKey: urlString as AnyObject) as? UIImage {
self.image = cachedImage
return
}
let url = URL(string: urlString)
if let data = try? Data(contentsOf: url!) {
DispatchQueue.main.async(execute: {
if let downloadedImage = UIImage(data: data) {
imageCache.setObject(downloadedImage, forKey: urlString as AnyObject)
self.image = downloadedImage
}
})
}
}
}
Before you convert this, you might consider refactoring to make it asynchronous:
One should never use Data(contentsOf:) for network requests because (a) it is synchronous and blocks the caller (which is a horrible UX, but also, in degenerate cases, can cause the watchdog process to kill your app); (b) if there is a problem, there’s no diagnostic information; and (c) it is not cancelable.
Rather than updating image property when done, you should consider completion handler pattern, so caller knows when the request is done and the image is processed. This pattern avoids race conditions and lets you have concurrent image requests.
When you use this asynchronous pattern, the URLSession runs its completion handlers on background queue. You should keep the processing of the image and updating of the cache on this background queue. Only the completion handler should be dispatched back to the main queue.
I infer from your answer, that your intent was to use this code in a UIImageView extension. You really should put this code in a separate object (I created a ImageManager singleton) so that this cache is not only available to image views, but rather anywhere where you might need images. You might, for example, do some prefetching of images outside of the UIImageView. If this code is buried in the
Thus, perhaps something like:
final class ImageManager {
static let shared = ImageManager()
enum ImageFetchError: Error {
case invalidURL
case networkError(Data?, URLResponse?)
}
private let imageCache = NSCache<NSString, UIImage>()
private init() { }
#discardableResult
func fetchImage(urlString: String, completion: #escaping (Result<UIImage, Error>) -> Void) -> URLSessionTask? {
if let cachedImage = imageCache.object(forKey: urlString as NSString) {
completion(.success(cachedImage))
return nil
}
guard let url = URL(string: urlString) else {
completion(.failure(ImageFetchError.invalidURL))
return nil
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard
error == nil,
let responseData = data,
let httpUrlResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpUrlResponse.statusCode,
let image = UIImage(data: responseData)
else {
DispatchQueue.main.async {
completion(.failure(error ?? ImageFetchError.networkError(data, response)))
}
return
}
self.imageCache.setObject(image, forKey: urlString as NSString)
DispatchQueue.main.async {
completion(.success(image))
}
}
task.resume()
return task
}
}
And you'd call it like:
ImageManager.shared.fetchImage(urlString: someUrl) { result in
switch result {
case .failure(let error): print(error)
case .success(let image): // do something with image
}
}
// but do not try to use `image` here, as it has not been fetched yet
If you wanted to use this in a UIImageView extension, for example, you could save the URLSessionTask, so that you could cancel it if you requested another image before the prior one finished. (This is a very common scenario if using this in table views and the user scrolls very quickly, for example. You do not want to get backlogged in a ton of network requests.) We could
extension UIImageView {
private static var taskKey = 0
private static var urlKey = 0
private var currentTask: URLSessionTask? {
get { objc_getAssociatedObject(self, &Self.taskKey) as? URLSessionTask }
set { objc_setAssociatedObject(self, &Self.taskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
private var currentURLString: String? {
get { objc_getAssociatedObject(self, &Self.urlKey) as? String }
set { objc_setAssociatedObject(self, &Self.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
func setImage(with urlString: String) {
if let oldTask = currentTask {
currentTask = nil
oldTask.cancel()
}
image = nil
currentURLString = urlString
let task = ImageManager.shared.fetchImage(urlString: urlString) { result in
// only reset if the current value is for this url
if urlString == self.currentURLString {
self.currentTask = nil
self.currentURLString = nil
}
// now use the image
if case .success(let image) = result {
self.image = image
}
}
currentTask = task
}
}
There are tons of other things you might do in this UIImageView extension (e.g. placeholder images or the like), but by separating the UIImageView extension from the network layer, one keeps these different tasks in their own respective classes (in the spirit of the single responsibility principle).
OK, with that behind us, let us look at the Objective-C rendition. For example, you might create an ImageManager singleton:
// ImageManager.h
#import UIKit;
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, ImageManagerError) {
ImageManagerErrorInvalidURL,
ImageManagerErrorNetworkError,
ImageManagerErrorNotValidImage
};
#interface ImageManager : NSObject
// if you make this singleton, mark normal instantiation methods as unavailable ...
+ (instancetype)alloc __attribute__((unavailable("alloc not available, call sharedImageManager instead")));
- (instancetype)init __attribute__((unavailable("init not available, call sharedImageManager instead")));
+ (instancetype)new __attribute__((unavailable("new not available, call sharedImageManager instead")));
- (instancetype)copy __attribute__((unavailable("copy not available, call sharedImageManager instead")));
// ... and expose singleton access point
#property (class, nonnull, readonly, strong) ImageManager *sharedImageManager;
// provide fetch method
- (NSURLSessionTask * _Nullable)fetchImageWithURLString:(NSString *)urlString completion:(void (^)(UIImage * _Nullable image, NSError * _Nullable error))completion;
#end
NS_ASSUME_NONNULL_END
and then implement this singleton:
// ImageManager.m
#import "ImageManager.h"
#interface ImageManager()
#property (nonatomic, strong) NSCache<NSString *, UIImage *> *imageCache;
#end
#implementation ImageManager
+ (instancetype)sharedImageManager {
static dispatch_once_t onceToken;
static ImageManager *shared;
dispatch_once(&onceToken, ^{
shared = [[self alloc] initPrivate];
});
return shared;
}
- (instancetype)initPrivate
{
self = [super init];
if (self) {
_imageCache = [[NSCache alloc] init];
}
return self;
}
- (NSURLSessionTask *)fetchImageWithURLString:(NSString *)urlString completion:(void (^)(UIImage *image, NSError *error))completion {
UIImage *cachedImage = [self.imageCache objectForKey:urlString];
if (cachedImage) {
completion(cachedImage, nil);
return nil;
}
NSURL *url = [NSURL URLWithString:urlString];
if (!url) {
NSError *error = [NSError errorWithDomain:[[NSBundle mainBundle] bundleIdentifier] code:ImageManagerErrorInvalidURL userInfo:nil];
completion(nil, error);
return nil;
}
NSURLSessionTask *task = [NSURLSession.sharedSession dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(nil, error);
});
return;
}
if (!data) {
NSError *error = [NSError errorWithDomain:[[NSBundle mainBundle] bundleIdentifier] code:ImageManagerErrorNetworkError userInfo:nil];
dispatch_async(dispatch_get_main_queue(), ^{
completion(nil, error);
});
}
UIImage *image = [UIImage imageWithData:data];
if (!image) {
NSDictionary *userInfo = #{
#"data": data,
#"response": response ? response : [NSNull null]
};
NSError *error = [NSError errorWithDomain:[[NSBundle mainBundle] bundleIdentifier] code:ImageManagerErrorNotValidImage userInfo:userInfo];
dispatch_async(dispatch_get_main_queue(), ^{
completion(nil, error);
});
}
[self.imageCache setObject:image forKey:urlString];
dispatch_async(dispatch_get_main_queue(), ^{
completion(image, nil);
});
}];
[task resume];
return task;
}
#end
And you'd call it like:
[[ImageManager sharedImageManager] fetchImageWithURLString:urlString completion:^(UIImage * _Nullable image, NSError * _Nullable error) {
if (error) {
NSLog(#"%#", error);
return;
}
// do something with `image` here ...
}];
// but not here, because the above runs asynchronously
And, again, you could use this from within a UIImageView extension:
#import <objc/runtime.h>
#implementation UIImageView (Cache)
- (void)setImage:(NSString *)urlString
{
NSURLSessionTask *oldTask = objc_getAssociatedObject(self, &taskKey);
if (oldTask) {
objc_setAssociatedObject(self, &taskKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[oldTask cancel];
}
image = nil
objc_setAssociatedObject(self, &urlKey, urlString, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSURLSessionTask *task = [[ImageManager sharedImageManager] fetchImageWithURLString:urlString completion:^(UIImage * _Nullable image, NSError * _Nullable error) {
NSString *currentURL = objc_getAssociatedObject(self, &urlKey);
if ([currentURL isEqualToString:urlString]) {
objc_setAssociatedObject(self, &urlKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(self, &taskKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
if (image) {
self.image = image;
}
}];
objc_setAssociatedObject(self, &taskKey, task, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
#end
After trial and error this worked:
#import "UIImageView+Cache.h"
#implementation UIImageView (Cache)
NSCache* imageCache;
- (void)loadImageUsingCacheWithUrlString:(NSString*)urlString {
imageCache = [[NSCache alloc] init];
self.image = nil;
UIImage *cachedImage = [imageCache objectForKey:(id)urlString];
if (cachedImage != nil) {
self.image = cachedImage;
return;
}
NSURL *url = [NSURL URLWithString:urlString];
NSData *data = [NSData dataWithContentsOfURL:url];
if (data != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *downloadedImage = [UIImage imageWithData:data];
if (downloadedImage != nil) {
[imageCache setObject:downloadedImage forKey:urlString];
self.image = downloadedImage;
}
});
}
}
#end
I want to get the Thumbnail Image of my PHAsset. I already extracted a PHAsset from the Photo Library and want to get the Thumbnail Image now.
Can you help me in Objective-C?
Thanks!
In case someone is looking for a swift solution, here is an extension:
extension PHAsset {
var thumbnailImage : UIImage {
get {
let manager = PHImageManager.default()
let option = PHImageRequestOptions()
var thumbnail = UIImage()
option.isSynchronous = true
manager.requestImage(for: self, targetSize: CGSize(width: 300, height: 300), contentMode: .aspectFit, options: option, resultHandler: {(result, info)->Void in
thumbnail = result!
})
return thumbnail
}
}
}
The PHImageManagerClass has the method:
- requestImageForAsset:targetSize:contentMode:options:resultHandler:
Here is a complete answer for Swift 4 showing the function & call against it. Also, make sure you have the photos privacy flag set in your plist.
import Photos
func requestImage(for asset: PHAsset,
targetSize: CGSize,
contentMode: PHImageContentMode,
completionHandler: #escaping (UIImage?) -> ()) {
let imageManager = PHImageManager()
imageManager.requestImage(for: asset,
targetSize: targetSize,
contentMode: contentMode,
options: nil) { (image, _) in
completionHandler(image)
}
}
let asset = // your existing PHAsset
let targetSize = CGSize(width: 100, height: 100)
let contentModel = PHImageContentMode.aspectFit
requestImage(for: asset, targetSize: targetSize, contentMode: contentModel, completionHandler: { image in
// Do something with your image if it exists
})
PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
options.resizeMode = PHImageRequestOptionsResizeModeExact;
NSInteger retinaMultiplier = [UIScreen mainScreen].scale;
CGSize retinaSquare = CGSizeMake(imageView.bounds.size.width * retinaMultiplier, imageView.bounds.size.height * retinaMultiplier);
[[PHImageManager defaultManager]
requestImageForAsset:(PHAsset *)_asset
targetSize:retinaSquare
contentMode:PHImageContentModeAspectFill
options:options
resultHandler:^(UIImage *result, NSDictionary *info) {
imageView.image =[UIImage imageWithCGImage:result.CGImage scale:retinaMultiplier orientation:result.imageOrientation];
}];
i get this answer from How to fetch squared thumbnails from PHImageManager?
How can I get the size(height/width) of an image from URL in objective-C? I want my container size according to the image. I am using AFNetworking 3.0.
I could use SDWebImage if it fulfills my requirement.
Knowing the size of an image before actually loading it can be necessary in a number of cases. For example, setting the height of a tableView cell in the heightForRowAtIndexPath method while loading the actual image later in the cellForRowAtIndexPath (this is a very frequent catch 22).
One simple way to do it, is to read the image header from the server URL using the Image I/O interface:
#import <ImageIO/ImageIO.h>
NSMutableString *imageURL = [NSMutableString stringWithFormat:#"http://www.myimageurl.com/image.png"];
CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)[NSURL URLWithString:imageURL], NULL);
NSDictionary* imageHeader = (__bridge NSDictionary*) CGImageSourceCopyPropertiesAtIndex(source, 0, NULL);
NSLog(#"Image header %#",imageHeader);
NSLog(#"PixelHeight %#",[imageHeader objectForKey:#"PixelHeight"]);
Swift 4.x
Xcode 12.x
func sizeOfImageAt(url: URL) -> CGSize? {
// with CGImageSource we avoid loading the whole image into memory
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else {
return nil
}
let propertiesOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, propertiesOptions) as? [CFString: Any] else {
return nil
}
if let width = properties[kCGImagePropertyPixelWidth] as? CGFloat,
let height = properties[kCGImagePropertyPixelHeight] as? CGFloat {
return CGSize(width: width, height: height)
} else {
return nil
}
}
Use Asynchronous mechanism called GCD in iOS to dowload image without affecting your main thread.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Download IMAGE using URL
NSData *data = [[NSData alloc]initWithContentsOfURL:URL];
// COMPOSE IMAGE FROM NSData
UIImage *image = [[UIImage alloc]initWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
// UI UPDATION ON MAIN THREAD
// Calcualte height & width of image
CGFloat height = image.size.height;
CGFloat width = image.size.width;
});
});
For Swift 4 use this:
let imageURL = URL(string: post.imageBigPath)!
let source = CGImageSourceCreateWithURL(imageURL as CFURL,
let imageHeader = CGImageSourceCopyPropertiesAtIndex(source!, 0, nil)! as NSDictionary;
print("Image header: \(imageHeader)")
The header would looks like:
Image header: {
ColorModel = RGB;
Depth = 8;
PixelHeight = 640;
PixelWidth = 640;
"{Exif}" = {
PixelXDimension = 360;
PixelYDimension = 360;
};
"{JFIF}" = {
DensityUnit = 0;
JFIFVersion = (
1,
0,
1
);
XDensity = 72;
YDensity = 72;
};
"{TIFF}" = {
Orientation = 0;
}; }
So u can get from it the Width, Height.
you can try like this:
NSData *data = [[NSData alloc]initWithContentsOfURL:URL];
UIImage *image = [[UIImage alloc]initWithData:data];
CGFloat height = image.size.height;
CGFloat width = image.size.width;
I have run into an issue on iOS 8 with the Assets Library framework that appears to be a bug in iOS 8. If I create an album called 'MyMedia' and then delete it, then when I try to create the album again, this chunk of code below returns 'nil' indicating that the album 'MyMedia' exists even though it does not because I deleted it using the 'Photos' app.
__block ALAssetsGroup *myGroup = nil;
__block BOOL addAssetDone = false;
NSString *albumName = #"MyMedia";
[assetsLib addAssetsGroupAlbumWithName:albumName
resultBlock:^(ALAssetsGroup *group) {
myGroup = group;
addAssetDone = true;
} failureBlock:^(NSError *error) {
NSLog( #"failed to create album: %#", albumName);
addAssetDone = true;
}];
while (!addAssetDone) {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05f]];
}
return myGroup; // returns nil if group has previously been created and then deleted
This same method works when creating a brand new album 'MyMedia2.' Has anyone else experienced this issue and know of a workaround or solution? Is the only solution to move to the new 'Photos' framework or am I doing something incorrect here? Note that this code always works on iOS7.X
Actually the steps to reproduce this problem are as follows ->
1. Uninstall your app that takes photos and saves them to a custom album
2. Under iOS Photos delete the custom album that has saved photos in it
3. Install your app
4. If you take pictures or record videos with the app it does not create them or store them. If you look under iOS Photos albums the custom album one does not exist and none of the pictures/videos taken with the app exist.
My previous answer was incorrect. I had not really tested it out. I did finally figure out what had to be done and it was difficult but I got it to work. This is what I had to do to get my app to run on both iOS 7.x.X and iOS 8.X.x and create a custom album that had been previously deleted by the app -->
I wrote two chunks of code: one that uses the Photos framework on iOS 8.x.x and one that uses the AssetsLibrary framework on iOS 7.x.x
Sp the app could run on both iOS versions, I linked the app to the Photos framework but then changed it from required to optional so it would not be loaded on iOS 7.x.x
Because the Photos framework code could not be called directly at runtime on iOS 7.x.x, I had to change it so it loaded the classes, functions (and blocks!) dynamically at runtime
Here is the code chunk that works when running on an iPhone. This should work in the simulator too -->
// PHPhotoLibrary_class will only be non-nil on iOS 8.x.x
Class PHPhotoLibrary_class = NSClassFromString(#"PHPhotoLibrary");
if (PHPhotoLibrary_class) {
/**
*
iOS 8..x. . code that has to be called dynamically at runtime and will not link on iOS 7.x.x ...
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
[PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:title];
} completionHandler:^(BOOL success, NSError *error) {
if (!success) {
NSLog(#"Error creating album: %#", error);
}
}];
*/
// dynamic runtime code for code chunk listed above
id sharedPhotoLibrary = [PHPhotoLibrary_class performSelector:NSSelectorFromString(#"sharedPhotoLibrary")];
SEL performChanges = NSSelectorFromString(#"performChanges:completionHandler:");
NSMethodSignature *methodSig = [sharedPhotoLibrary methodSignatureForSelector:performChanges];
NSInvocation* inv = [NSInvocation invocationWithMethodSignature:methodSig];
[inv setTarget:sharedPhotoLibrary];
[inv setSelector:performChanges];
void(^firstBlock)() = ^void() {
Class PHAssetCollectionChangeRequest_class = NSClassFromString(#"PHAssetCollectionChangeRequest");
SEL creationRequestForAssetCollectionWithTitle = NSSelectorFromString(#"creationRequestForAssetCollectionWithTitle:");
[PHAssetCollectionChangeRequest_class performSelector:creationRequestForAssetCollectionWithTitle withObject:albumName];
};
void (^secondBlock)(BOOL success, NSError *error) = ^void(BOOL success, NSError *error) {
if (success) {
[assetsLib enumerateGroupsWithTypes:ALAssetsGroupAlbum usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
if (group) {
NSString *name = [group valueForProperty:ALAssetsGroupPropertyName];
if ([albumName isEqualToString:name]) {
groupFound = true;
handler(group, nil);
}
}
} failureBlock:^(NSError *error) {
handler(nil, error);
}];
}
if (error) {
NSLog(#"Error creating album: %#", error);
handler(nil, error);
}
};
// Set the success and failure blocks.
[inv setArgument:&firstBlock atIndex:2];
[inv setArgument:&secondBlock atIndex:3];
[inv invoke];
}
else {
// code that always creates an album on iOS 7.x.x but fails
// in certain situations such as if album has been deleted
// previously on iOS 8...x. .
[assetsLib addAssetsGroupAlbumWithName:albumName
resultBlock:^(ALAssetsGroup *group) {
handler(group, nil);
} failureBlock:^(NSError *error) {
NSLog( #"Failed to create album: %#", albumName);
handler(nil, error);
}];
}
Using Adam's answer, and Marin Todorov's Category on ALAssetsLibrary, ALAssetsLibrary+CustomPhotoAlbum to create photo Albums, and place photos in them, this code below replaces the main workHorse in that Category, it works on both iOS7 devices and iOS 8.1 devices for those who need to have both.
it gives two warnings about performSelector on unknown class though, any improvements are appreciated:
(it will not copy a photo from a shared album that you did not create and will fail with message, any enhancements there also would be good)
1) add the "Photos" Frameworks, set to "optional"
2) include the import line #import < Photos/PHPhotoLibrary.h >
//----------------------------------------------------------------------------------------
- (void)addAssetURL:(NSURL *)assetURL
toAlbum:(NSString *)albumName
completion:(ALAssetsLibraryWriteImageCompletionBlock)completion
failure:(ALAssetsLibraryAccessFailureBlock)failure
{
NSLog();
__block BOOL albumWasFound = NO;
//-----------------------------------------
ALAssetsLibraryGroupsEnumerationResultsBlock enumerationBlock;
enumerationBlock = ^(ALAssetsGroup *group, BOOL *stop)
{
NSLog(#" ALAssetsLibraryGroupsEnumerationResultsBlock");
// Compare the names of the albums
if ([albumName compare:[group valueForProperty:ALAssetsGroupPropertyName]] == NSOrderedSame)
{
NSLog(#"--------------Target album is found");
// Target album is found
albumWasFound = YES;
// Get a hold of the photo's asset instance
// If the user denies access to the application, or if no application is allowed to
// access the data, the failure block is called.
ALAssetsLibraryAssetForURLResultBlock assetForURLResultBlock =
[self _assetForURLResultBlockWithGroup:group
assetURL:assetURL
completion:completion
failure:failure];
[self assetForURL:assetURL
resultBlock:assetForURLResultBlock
failureBlock:failure];
// Album was found, bail out of the method
*stop = YES;
}
if (group == nil && albumWasFound == NO)
{
NSLog(#"--------------Target album does not exist");
// Photo albums are over, target album does not exist, thus create it
// Since you use the assets library inside the block,
// ARC will complain on compile time that there’s a retain cycle.
// When you have this – you just make a weak copy of your object.
ALAssetsLibrary * __weak weakSelf = self;
// If iOS version is lower than 5.0, throw a warning message
if (! [self respondsToSelector:#selector(addAssetsGroupAlbumWithName:resultBlock:failureBlock:)])
{
NSLog(#"--------------Target album does not exist and does not respond to addAssetsGroupAlbumWithName");
} else {
NSLog(#"--------------Target album does not exist addAssetsGroupAlbumWithName");
// ----------- PHPhotoLibrary_class will only be non-nil on iOS 8.x.x -----------
Class PHPhotoLibrary_class = NSClassFromString(#"PHPhotoLibrary");
NSLog(#"PHPhotoLibrary_class %# ", PHPhotoLibrary_class);
if (PHPhotoLibrary_class)
{
NSLog(#"iOS8");
// --------- dynamic runtime code -----------
id sharedPhotoLibrary = [PHPhotoLibrary_class performSelector:NSSelectorFromString(#"sharedPhotoLibrary")];
NSLog(#"sharedPhotoLibrary %# ", sharedPhotoLibrary);
SEL performChanges = NSSelectorFromString(#"performChanges:completionHandler:");
NSMethodSignature *methodSig = [sharedPhotoLibrary methodSignatureForSelector:performChanges];
NSInvocation* inv = [NSInvocation invocationWithMethodSignature:methodSig];
[inv setTarget:sharedPhotoLibrary];
[inv setSelector:performChanges];
void(^firstBlock)() = ^void()
{
NSLog(#"firstBlock");
Class PHAssetCollectionChangeRequest_class = NSClassFromString(#"PHAssetCollectionChangeRequest");
SEL creationRequestForAssetCollectionWithTitle = NSSelectorFromString(#"creationRequestForAssetCollectionWithTitle:");
NSLog(#"PHAssetCollectionChangeRequest_class %# ", PHAssetCollectionChangeRequest_class);
[PHAssetCollectionChangeRequest_class performSelector:creationRequestForAssetCollectionWithTitle withObject:albumName];
};
void (^secondBlock)(BOOL success, NSError *error) = ^void(BOOL success, NSError *error)
{
NSLog(#"secondBlock");
if (success)
{
NSLog(#"success");
[self enumerateGroupsWithTypes:ALAssetsGroupAlbum usingBlock:^(ALAssetsGroup *group, BOOL *fullStop)
{
if (group)
{
NSLog(#"group %# ", group);
NSString *name = [group valueForProperty:ALAssetsGroupPropertyName];
if ([albumName isEqualToString:name])
{
NSLog(#"[albumName isEqualToString:name] %# ", name);
ALAssetsLibraryAssetForURLResultBlock assetForURLResultBlock =
[self _assetForURLResultBlockWithGroup:group
assetURL:assetURL
completion:completion
failure:failure];
[self assetForURL:assetURL
resultBlock:assetForURLResultBlock
failureBlock:failure];
*fullStop = YES;
}
}
} failureBlock:failure];
}
if (error)
{
NSLog(#"Error creating album: %#", error);
}
};
// Set the success and failure blocks.
[inv setArgument:&firstBlock atIndex:2];
[inv setArgument:&secondBlock atIndex:3];
[inv invoke];
} else {
NSLog(#"iOS7");
[self addAssetsGroupAlbumWithName:albumName resultBlock:^(ALAssetsGroup *createdGroup)
{
// Get the photo's instance, add the photo to the newly created album
ALAssetsLibraryAssetForURLResultBlock assetForURLResultBlock =
[weakSelf _assetForURLResultBlockWithGroup:createdGroup
assetURL:assetURL
completion:completion
failure:failure];
[weakSelf assetForURL:assetURL
resultBlock:assetForURLResultBlock
failureBlock:failure];
}
failureBlock:failure];
}
}
// Should be the last iteration anyway, but just in case
*stop = YES;
}
};
// Search all photo albums in the library
[self enumerateGroupsWithTypes:ALAssetsGroupAlbum
usingBlock:enumerationBlock
failureBlock:failure];
}
You can try My below Method for Create Album for iOS 7 and iOS 8
#define PHOTO_ALBUM_NAME #"AlbumName Videos"
#pragma mark - Create Album
-(void)createAlbum{
// PHPhotoLibrary_class will only be non-nil on iOS 8.x.x
Class PHPhotoLibrary_class = NSClassFromString(#"PHPhotoLibrary");
if (PHPhotoLibrary_class) {
// iOS 8..x. . code that has to be called dynamically at runtime and will not link on iOS 7.x.x ...
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
[PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:PHOTO_ALBUM_NAME];
} completionHandler:^(BOOL success, NSError *error) {
if (!success) {
NSLog(#"Error creating album: %#", error);
}else{
NSLog(#"Created");
}
}];
}else{
[self.library addAssetsGroupAlbumWithName:PHOTO_ALBUM_NAME resultBlock:^(ALAssetsGroup *group) {
NSLog(#"adding album:'Compressed Videos', success: %s", group.editable ? "YES" : "NO");
if (group.editable == NO) {
}
} failureBlock:^(NSError *error) {
NSLog(#"error adding album");
}];
}}
Just wanted to update everyone I should have updated sooner but I got kind of swamped with work. This issue is/was an issue with iOS 8 but has been fixed with iOS 8.0.2 so all you need to do to fix it is update your iOS to iOS 8.0.2
I used the below code to check whether a specific album exists, and if it does not exist, create it and add a couple of images to it. After creating an Asset from a UIImage, I use its placeholder to add it to the album without leaving the block.
//Will enter only in iOS 8+
Class PHPhotoLibrary_class = NSClassFromString(#"PHPhotoLibrary");
if (PHPhotoLibrary_class)
{
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^
{
//Checks for App Photo Album and creates it if it doesn't exist
PHFetchOptions *fetchOptions = [PHFetchOptions new];
fetchOptions.predicate = [NSPredicate predicateWithFormat:#"title == %#", kAppAlbumName];
PHFetchResult *fetchResult = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:fetchOptions];
if (fetchResult.count == 0)
{
//Create Album
PHAssetCollectionChangeRequest *albumRequest = [PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:kAppAlbumName];
//Add default photos to it
NSMutableArray *photoAssets = [[NSMutableArray alloc] init];
for (UIImage *image in albumDefaultImages)
{
PHAssetChangeRequest *imageRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:image];
[photoAssets addObject:imageRequest.placeholderForCreatedAsset];
}
[albumRequest addAssets:photoAssets];
}
}
completionHandler:^(BOOL success, NSError *error)
{
NSLog(#"Log here...");
}];
}
As none of the above suggestions helped me, this is how I went about solving the issues with saving assets (photos) to a custom album name.
This code: "fetchCollectionResult.count==0" specifically handles the situation when you have deleted your custom album once and trying to save to it again, as I suppose fetchCollectionResult might stop being 'nil'.
You can easily change this to support saving of videos/movies too.
This code is for iOS 8 only!
You must make sure not to call it if the device is running on earlier versions!
#define PHOTO_ALBUM_NAME #"MyPhotoAlbum"
NSString* existingAlbumIdentifier = nil;
-(void)saveAssetToAlbum:(UIImage*)myPhoto
{
PHPhotoLibrary* photoLib = [PHPhotoLibrary sharedPhotoLibrary];
__block NSString* albumIdentifier = existingAlbumIdentifier;
__block PHAssetCollectionChangeRequest* collectionRequest;
[photoLib performChanges:^
{
PHFetchResult* fetchCollectionResult;
if ( albumIdentifier )
fetchCollectionResult = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:#[albumIdentifier] options:nil];
// Create a new album
if ( !fetchCollectionResult || fetchCollectionResult.count==0 )
{
NSLog(#"Creating a new album.");
collectionRequest = [PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:PHOTO_ALBUM_NAME];
albumIdentifier = collectionRequest.placeholderForCreatedAssetCollection.localIdentifier;
}
// Use existing album
else
{
NSLog(#"Fetching existing album, of #%d albums found.", fetchCollectionResult.count);
PHAssetCollection* exisitingCollection = fetchCollectionResult.firstObject;
collectionRequest = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:exisitingCollection];
}
NSLog(#"Album local identifier = %#", albumIdentifier);
PHAssetChangeRequest* createAssetRequest;
createAssetRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:myPhoto];
[collectionRequest addAssets:#[createAssetRequest.placeholderForCreatedAsset]];
}
completionHandler:^(BOOL success, NSError *error)
{
if (success)
{
existingAlbumIdentifier = albumIdentifier;
NSLog(#"added image to album:%#", PHOTO_ALBUM_NAME);
}
else
NSLog(#"Error adding image to album: %#", error);
}];
}
I would like to get all of the images from the camera roll and create an array of UIImages from them.
I have been trying to figure out how to do this for about a day now and I've gotten nowhere. I can't seem to figure out how to retrieve only items from the Camera Roll. It appears that all of the samples that I've seen all enumerate over all of the photo albums. I might be wrong about that though.
Any help would be appreciated. Thanks!
Have you tried ALAssetsLibrary? like this:
assets = [[NSMutableArray array] init]; // Prepare array to have retrieved images by Assets Library.
void (^assetEnumerator)(ALAsset *, NSUInteger, BOOL *) = ^(ALAsset *asset, NSUInteger index, BOOL *stop) {
if(asset != NULL) {
[assets addObject:asset];
dispatch_async(dispatch_get_main_queue(), ^{
[self insertArray];
});
}
};
void (^assetGroupEnumerator)(ALAssetsGroup *, BOOL *) = ^(ALAssetsGroup *group, BOOL *stop) {
if(group != nil) {
[group enumerateAssetsUsingBlock:assetEnumerator];
}
};
// Create instance of the Assets Library.
library = [[ALAssetsLibrary alloc] init];
[library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos // Retrieve the images saved in the Camera roll.
usingBlock:assetGroupEnumerator
failureBlock: ^(NSError *error) {
NSLog(#"Failed.");
}];
that'll nab them. then do this to render them (this is wicked hacky. you'll want to import them as needed and not all at once like this, or you'll run into memory issues and crash)
-(void) insertArray {
int i = assetCount++;
if (i>20) {
return;
}
ALAssetRepresentation *rep = [[assets objectAtIndex:i] defaultRepresentation];
CGImageRef iref = [rep fullResolutionImage];
CGSize cSize = CGSizeMake(75,75);
UIImage *largeimage = [UIImage imageWithCGImage:iref];
UIImage *resizedimage = (UIImage *)[largeimage resizedImage:cSize interpolationQuality:kCGInterpolationHigh];
UIImageView *newView = [[UIImageView alloc] initWithImage:resizedimage];
if((i>0)&&(i%4 == 0)){
rowCount++;
}
colCount = i%4;
newView.frame = CGRectMake(4+(colCount*(75+4)), 4+(rowCount*(75+4)), 75, 75);
[sv addSubview:newView];
[sv setContentSize:CGSizeMake(320, 85+(rowCount*(75+4)))];
NSLog(#"sv frame size is %# and i is %i", NSStringFromCGRect(sv.frame), i);
}