I'm trying to retrieve a PHAsset however PHAsset.fetchAssets(withALAssetURLs:options:) is deprecated from iOS 8 so how can I properly retrieve a PHAsset?
I had the same the issue, first check permissions and request access:
let status = PHPhotoLibrary.authorizationStatus()
if status == .notDetermined {
PHPhotoLibrary.requestAuthorization({status in
})
}
Just hook that up to whatever triggers your UIImagePickerController. The delegate call should now include the PHAsset in the userInfo.
guard let asset = info[UIImagePickerControllerPHAsset] as? PHAsset
Here is my solution:
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if #available(iOS 11.0, *) {
let asset = info[UIImagePickerControllerPHAsset]
} else {
if let assetURL = info[UIImagePickerControllerReferenceURL] as? URL {
let result = PHAsset.fetchAssets(withALAssetURLs: [assetURL], options: nil)
let asset = result.firstObject
}
}
}
The PHAsset will not appear in the didFinishPickingMediaWithInfo: info result unless the user has authorized, which did not happen for me just by presenting the picker. I added this in the Coordinator init():
let status = PHPhotoLibrary.authorizationStatus()
if status == .notDetermined {
PHPhotoLibrary.requestAuthorization({status in
})
}
I am not sure what you want.
Are you trying to target iOS 8?
This is how I fetch photos and it works in iOS (8.0 and later), macOS (10.11 and later), tvOS (10.0 and later).
Code is commented where it may be confusing
The first functions sets the options to fetch the photos
The second function will actually fetch them
//import the Photos framework
import Photos
//in these arrays I store my images and assets
var images = [UIImage]()
var assets = [PHAsset]()
fileprivate func setPhotoOptions() -> PHFetchOptions{
let fetchOptions = PHFetchOptions()
fetchOptions.fetchLimit = 15
let sortDescriptor = NSSortDescriptor(key: "creationDate", ascending: false)
fetchOptions.sortDescriptors = [sortDescriptor]
return fetchOptions
}
fileprivate func fetchPhotos() {
let allPhotos = PHAsset.fetchAssets(with: .image, options: setPhotoOptions())
DispatchQueue.global(qos: .background).async {
allPhotos.enumerateObjects({ (asset, count, stop) in
let imageManager = PHImageManager.default()
let targetSize = CGSize(width: 200, height: 200)
let options = PHImageRequestOptions()
options.isSynchronous = true
imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFit, options: options, resultHandler: { (image, info) in
if let image = image {
self.images.append(image)
self.assets.append(asset)
}
if count == allPhotos.count - 1 {
DispatchQueue.main.async {
//basically, here you can do what you want
//(after you finish retrieving your assets)
//I am reloading my collection view
self.collectionView?.reloadData()
}
}
})
})
}
}
Edit based on OP's clarification
You need to set the delegate UIImagePickerControllerDelegate
then implement the following function
func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) {
within said method, get the image like this:
var image : UIImage = info[UIImagePickerControllerEditedImage] as! UIImage
Related
I use an UIImagePicker to choose an image from the photos app and would like to store the url and later reuse the same image.
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let chosenImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
let asset = info[UIImagePickerController.InfoKey.phAsset]
let url = info[UIImagePickerController.InfoKey.referenceURL] as! URL
...
Why is asset nil?
referenceURL is deprecated but contains something like assets-library://asset/asset.JPG?id=95554EBE-DAB8-4A36-9BEA-00BAB0174777&ext=JPG
private func loadImage() -> UIImage? {
let manager = PHImageManager.default()
let fetchOptions = PHFetchOptions()
fetchOptions.predicate = NSPredicate(format: "? = %#", "?")
let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
var image: UIImage? = nil
let requestOptions = PHImageRequestOptions()
requestOptions.isSynchronous = true
requestOptions.deliveryMode = .highQualityFormat
manager.requestImage(for: fetchResult.object(at: 0), targetSize: CGSize(width: 500, height: 500), contentMode: .aspectFill, options: requestOptions) { img, err in
image = img
}
return image
}
What should the predicate look like? Thanks!
The PHAsset is nil because you didn't obtain user authorization for Photo Library usage beforehand. Your lack of authorization means yes, you can present the picker, and yes, the user can choose a photo, and yes, you can receive the photo image, but you can't look into the Photo Library.
If you get permission beforehand, the PHAsset will be an actual PHAsset and you can use that to go back and get further info about the asset from the Photo Library. Use the asset's identifier if you want to store a persistent reference to it.
https://developer.apple.com/documentation/photokit/phobject/1622400-localidentifier
I am trying to get all the photos from cameraRoll using Photos framework but its taking a lot of time to fetch all the photos from cameraRoll.
Is their anyway to add pagination to it ?
so i can fetch while scrolling.
var images = [UIImage]()
var assets = [PHAsset]()
fileprivate func assetsFetchOptions() -> PHFetchOptions {
let fetchOptions = PHFetchOptions()
//fetchOptions.fetchLimit = 40 //uncomment to limit photo
let sortDescriptor = NSSortDescriptor(key: "creationDate", ascending: false)
fetchOptions.sortDescriptors = [sortDescriptor]
return fetchOptions
}
fileprivate func fetchPhotos() {
let allPhotos = PHAsset.fetchAssets(with: .image, options: assetsFetchOptions())
DispatchQueue.global(qos: .background).async {
allPhotos.enumerateObjects({ (asset, count, stop) in
//print(count)
let imageManager = PHImageManager.default()
let targetSize = CGSize(width: 200, height: 200)
let options = PHImageRequestOptions()
options.isSynchronous = true
imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFit, options: options, resultHandler: { (image, info) in
if let image = image {
self.images.append(image)
self.assets.append(asset)
}
if count == allPhotos.count - 1 {
DispatchQueue.main.async {
self.collectionView?.reloadData()
}
}
})
})
}
}
allPhotos is of type PHFetchResult< PHAsset > which is a lazy collection, ie it doesn't actually go out and get the photo until you ask it for one, which is what .enumerateObjects is doing. You can just grab the photos one at a time with the subscript operator or get a range of objects with objects(at:) to page through the collection as needed.
In what cases will I be sad? if I have set allowEditing as false.
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let selectedImage = info[UIImagePickerControllerOriginalImage] as? UIImage {
// I am happy :)
} else {
// I am sad :(
}
dismiss(animated: true, completion: nil)
}
(I got a crash in iOS 11.2.1 iPhone SE(as per Crashlytics), so confused if there are legit conditions where this can fail or it is just an iOS bug.)
changed in IOS 12
image.image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage
I've read it in a Apple Developer Forum thread that, if the image size is larger (Above 2048 X 2048) on iOS 11 UIImagePickerControllerOriginalImage returns nil. As a work-around the post suggests to use Photos framework to get the picked image. The solution offered in that post is as follows:
Objective C:
__block UIImage *image = (UIImage *) [info objectForKey:UIImagePickerControllerOriginalImage];
if (#available(iOS 11.0, *))
{
PHAsset * asset = (PHAsset*)[info objectForKey:UIImagePickerControllerPHAsset];
PHImageManager *manager = [PHImageManager defaultManager];
PHImageRequestOptions *requestOptions = [[PHImageRequestOptions alloc] init];
requestOptions.resizeMode = PHImageRequestOptionsResizeModeExact;
requestOptions.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
requestOptions.synchronous = true;
[manager requestImageForAsset:asset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeDefault options:requestOptions resultHandler:^void(UIImage *img, NSDictionary *info) {
if(img != nil)
{
image = img;
}
}];
}
Reference : iOS 11 does not return the original image for large images with UIImagePickerControllerOriginalImage
Swift:
var image = info[UIImagePickerControllerOriginalImage]
if #available(iOS 11.0, *)
{
let asset = info[UIImagePickerControllerPHAsset] as! PHAsset
let manager = PHImageManager.default()
let requestOptions = PHImageRequestOptions()
requestOptions.resizeMode = PHImageRequestOptionsResizeMode.exact
requestOptions.deliveryMode = PHImageRequestOptionsDeliveryMode.highQualityFormat
requestOptions.isSynchronous = true
manager.requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: PHImageContentMode.default, options: requestOptions, resultHandler: { (img, info) in
if img != nil
{
image = img
}
})
}
the method signature has changed like so:
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let selectedImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else {
fatalError("Expected a dictionary containing an image, but was provided the following: \(info)")
}
// Set photoImageView to display the selected image.
photoImageView.image = selectedImage
// Dismiss the picker.
dismiss(animated: true, completion: nil)
}
Note the second argument:
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]
It is not an array of strings like in the previous method signature.
I ended up using this:
import Photos
extension UIImage {
static func from(info: [String : Any]) -> UIImage? {
if let image = info[UIImagePickerControllerOriginalImage] as? UIImage {
return image
}
var imageToBeReturned: UIImage?
if let url = info[UIImagePickerControllerReferenceURL] as? URL,
let asset = PHAsset.fetchAssets(withALAssetURLs: [url], options: nil).firstObject {
let manager = PHImageManager.default()
let option = PHImageRequestOptions()
option.isSynchronous = true
manager.requestImage(for: asset, targetSize: CGSize(width: 1000, height: 1000), contentMode: .aspectFit, options: option, resultHandler: {(image: UIImage?, info: [AnyHashable : Any]?) in
imageToBeReturned = image
})
}
return imageToBeReturned
}
}
In this way-
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let selectedImage = UIImage.from(info: info) {
// I am happy :)
} else {
// I am sad :(
}
dismiss(animated: true, completion: nil)
}
This is working for me, please do suggest any improvements :)
I would like to fetch all photos that are saved in device and save them to my app and then eventually (if user allow this) delete originals.
This is my whole class I created for this task:
class ImageAssetsManager: NSObject {
let imageManager = PHCachingImageManager()
func fetchAllImages() {
let options = PHFetchOptions()
options.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.Image.rawValue)
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
if #available(iOS 9.0, *) {
options.fetchLimit = 5
} else {
// Fallback on earlier versions
}
let imageAssets = PHAsset.fetchAssetsWithOptions(options)
print(imageAssets.count)
self.getAssets(imageAssets)
}
func getAssets(assets: PHFetchResult) {
var assetsToDelete: [PHAsset] = []
assets.enumerateObjectsUsingBlock { (object, count, stop) in
if object is PHAsset {
let asset = object as! PHAsset
let imageSize = CGSize(width: asset.pixelWidth,height: asset.pixelHeight)
let options = PHImageRequestOptions()
options.deliveryMode = .FastFormat
options.synchronous = true
self.imageManager.requestImageForAsset(asset, targetSize: imageSize, contentMode: .AspectFill, options: options, resultHandler: { [weak self]
image, info in
self.addAssetToSync(image, info: info)
assetsToDelete.append(asset)
})
}
}
self.deleteAssets(assetsToDelete)
}
func addAssetToSync(image: UIImage?, info: [NSObject : AnyObject]?) {
guard let image = image else {
return
}
guard let info = info else {
return
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
let imageData = UIImageJPEGRepresentation(image, 0.95)!
let fileUrl = info["PHImageFileURLKey"] as! NSURL
dispatch_async(dispatch_get_main_queue(), {
let photoRootItem = DatabaseManager.sharedInstance.getPhotosRootItem()
let ssid = DatabaseManager.sharedInstance.getSsidInfoByName(ContentManager.sharedInstance.ssid)
let item = StorageManager.sharedInstance.createFile(imageData, name: fileUrl.absoluteString.fileNameWithoutPath(), parentFolder: photoRootItem!, ssid: ssid!)
})
})
}
func deleteAssets(assetsToDelete: [PHAsset]){
PHPhotoLibrary.sharedPhotoLibrary().performChanges({
PHAssetChangeRequest.deleteAssets(assetsToDelete)
}, completionHandler: { success, error in
guard let error = error else {return}
})
}
}
It's working but my problem is that it's working just for a limited number of photos. When I try it with all I get memory warnings and then app crashed. I know why it is. I know that my problem is that I get all photos to memory and it's too much. I could fetch images with that fetch limit and make it to loop but I am not sure if it is best solution.
I was hoping that with some solution process few photos then release memory and again and again until end. But this change would be somewhere in enumerateObjectsUsingBlock. I am not sure if it helps but I don't even need to get image. I just need to copy image file from device path to my app sandbox path.
What's best solution for this? How to avoid memory warnings and leaks? Thanks
Change your dispatch_async calls to dispatch_sync. Then you will process photos one at a time as you walk through enumerateObjectsUsingBlock, instead of trying to process them all at the same time.
I'm using UIImagePickerController to take photos with camera and also to get photos from SavedPhotosAlbum library. Once user takes a photo I save it in SavedPhotosAlbum and the following method is called:
override func image(image: UIImage, didFinishSavingWithError: NSErrorPointer, contextInfo:UnsafePointer<Void>) {
if (didFinishSavingWithError != nil) {
print("Error saving photo: \(didFinishSavingWithError)")
} else {
let photoToSend = CompressAndSendPhoto(image: image)
photoToSend.uploadImageRequest()
print("Successfully saved photo, will make request to update asset metadata")
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
let fetchResult = PHAsset.fetchAssetsWithMediaType(PHAssetMediaType.Image, options: fetchOptions)
let lastImageAsset = fetchResult.lastObject as! PHAsset
let coordinate = CLLocationCoordinate2DMake(self.coordinate1, self.coordinate2)
let nowDate = NSDate()
let myLocation = CLLocation(coordinate: coordinate, altitude: 0.0, horizontalAccuracy: 1.0, verticalAccuracy: 1.0, timestamp: nowDate)
PHPhotoLibrary.sharedPhotoLibrary().performChanges({
let assetChangeRequest = PHAssetChangeRequest(forAsset: lastImageAsset)
assetChangeRequest.location = myLocation
}, completionHandler: {
(success:Bool, error:NSError?) -> Void in
if (success) {
print("Succesfully saved metadata to asset")
print("location metadata = \(myLocation)")
} else {
print("Failed to save metadata to asset with error: \(error!)")
}
});
}
}
and it works fine, user current location is being added to the photo asset.
The problem is that I can not get this value while choosing a photo from SavedPhotosAlbum. I googled many options but none of them works. How can I do it in method below?
func imagePickerController(
picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [String : AnyObject])
{
let chosenImage = info[UIImagePickerControllerOriginalImage] as! UIImage
if picker.sourceType == UIImagePickerControllerSourceType.SavedPhotosAlbum {
}
dismissViewControllerAnimated(true, completion: nil)
}
Also I would like to add more "fields" to photo asset, not only location which is one of default ones, how can I add custom NSDictionary of values?