I'm trying to retrieve over 1,000 images from the user's camera roll with PHAsset but it ends up crashing or taking a long time if it's just thumbnails. Here is my function where I retrieve the images...
func retrieveImages(thumbnail: Bool) {
/* Retrieve the items in order of modification date, ascending */
let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "modificationDate",
ascending: false)]
/* Then get an object of type PHFetchResult that will contain
all our image assets */
let assetResults = PHAsset.fetchAssetsWithMediaType(.Image,
options: options)
let imageManager = PHCachingImageManager()
assetResults.enumerateObjectsUsingBlock{(object: AnyObject!,
count: Int,
stop: UnsafeMutablePointer<ObjCBool>) in
if object is PHAsset{
let asset = object as! PHAsset
print("Inside If object is PHAsset, This is number 1")
var imageSize: CGSize!
if thumbnail == true {
imageSize = CGSize(width: 100, height: 100)
} else {
imageSize = CGSize(width: self.cameraView.bounds.width, height: self.cameraView.bounds.height)
}
/* For faster performance, and maybe degraded image */
let options = PHImageRequestOptions()
options.deliveryMode = .FastFormat
options.synchronous = true
imageManager.requestImageForAsset(asset,
targetSize: imageSize,
contentMode: .AspectFill,
options: options,
resultHandler: { (image, _: [NSObject : AnyObject]?) -> Void in
if thumbnail == true {
self.libraryImageThumbnails.append(image!)
self.collectionTable.reloadData()
} else {
self.libraryImages.append(image!)
self.collectionTable.reloadData()
}
})
/* The image is now available to us */
print("enum for image, This is number 2")
print("Inside If object is PHAsset, This is number 3")
}
print("Outside If object is PHAsset, This is number 4")
}
}
Please tell me if any more information is needed. Thank you!
Generally you don't want to be loading tons of full-size or screen-sized images and keeping them in memory yourself. For that size, you're only going to be presenting one (or two or three, if you want to preload for screen-transition animations) at a time, so you don't need to fetch more than that at once.
Similarly, loading hundreds or thousands of thumbnails and caching them yourself will cause you memory issues. (Telling your collection view to reloadData over and over again causes a lot of churn, too.) Instead, let the Photos framework manage them — it has features for making sure that thumbnails are generated only when needed and cached between uses, and even for helping you with tasks like grabbing only the thumbnails you need as the user scrolls in a collection view.
Apple's Using Photos Framework sample code project shows several best practices for fetching images. Here's a summary of some of the major points:
AssetGridViewController, which manages a collection view of thumbnails, caches a PHFetchResult<PHAsset> that's given to it upon initialization. Info about the fetch result (like its count) is all that's needed for the basic management of the collection view.
AssetGridViewController requests thumbnail images from PHImageManager individually, only when the collection view asks for a cell to display. This means that we only create a UIImage when we know a cell is going to be seen, and we don't have to tell the collection view to reload everything over and over again.
On its own, step 2 would lead to slow scrolling, or rather, to slow catch-up of images loading into cells after you scroll. So, we also do:
Instead of using PHImageManager.default(), we keep an instance of PHCachingImageManager, and tell it what set of images we want to "preheat" the cache for as the user scrolls.
AssetGridViewController has some extra logic for keeping track of the visible rect of the scroll view so that the image manager knows to prepare thumbnails for the currently visible items (plus a little bit of scroll-ahead), and can free resources for items no longer visible if it needs to. Note that prefetching doesn't create UIImages, just starts the necessary loading from disk or downloading from iCloud, so your memory usage stays small.
On selecting an item in the collection view (to segue to a full-screen presentation of that photo), AssetViewController uses the default PHImageManager to request a screen-sized (not full-sized) image.
Opportunistic delivery means that we'll get a lower-quality version (basically the same thumbnail that Photos has already cached for us from its previous use in the collection view) right away, and a higher quality version soon after (asynchronously, after which we automatically update the view).
If you need to support zooming (that sample code app doesn't, but it's likely a real app would), you could either request a full-size image right away after going to the full-screen view (at the cost of taking longer to transition from blown-up-thumbnail to fullscreen image), or start out by requesting a screen-sized image and then also requesting a full-sized image for later delivery.
In CellForRowAtIndexPath use the PHImageManager's RequestImageForAsset method to lazy load the image property on your UIImageView.
Related
I am learning how to use the PHFetchRequest to get images from the user's photo library and then display them in a scroll view for a custom image picker but I am getting mostly nil returned data and some returned as optional data that can be un-wrapped.
I back up all my photos on ICloud, could this be the reason I am getting nil??
Below is the function within my struct that fetches and appends the data to an empty array variable...
What am I doing wrong? Thanks guys!
XCode log showing nil and optional returns
func getAllImages() {
let request = PHAsset.fetchAssets(with: .image, options: .none)
DispatchQueue.global(qos: .background).async {
let options = PHImageRequestOptions()
options.isSynchronous = true
request.enumerateObjects { (asset, _, _ ) in
PHCachingImageManager.default().requestImage(for: asset, targetSize: .init(), contentMode: .default, options: options) { (image, _) in
print(image?.pngData())
// I had to coalesce with an empty UIImage I made as an extension
let data1 = Images(image: (image ?? UIImage.emptyImage(with: CGSize(width: 10, height: 10)))!, selected: false)
self.data.append(data1)
}
}
if request.count == self.data.count {
self.getGrid()
}
}
}
You are seeing nil data because the image resource is in cloud and not on your phone. Optional is expected because it is not guaranteed to exist on your phone (like it's in cloud in your case).
You can instruct fetch request to fetch the photo from cloud (if needed) using PHImageRequestOptions.isNetworkAccessAllowed option. This is false by default.
A Boolean value that specifies whether Photos can download the requested image from iCloud.
Discussion
If true, and the requested image is not stored on the local device, Photos downloads the image from iCloud. To be notified of the download’s progress, use the progressHandler property to provide a block that Photos calls periodically while downloading the image.
If false (the default), and the image is not on the local device, the PHImageResultIsInCloudKey value in the result handler’s info dictionary indicates that the image is not available unless you enable network access.
progressHandler Notes
You don't need to implement this to allow iCloud photo downloads. The iCloud photo downloads will work without this as well.
In case you plan to implement this to show progress on UI, you should keep in mind that these calls are fired multiple times for one download. So you can't consider your photo download to be complete upon first call in progressHandler callback.
Discussion
If you request an image whose data is not on the local device, and you have enabled downloading with the isNetworkAccessAllowed property, Photos calls your block periodically to report progress and to allow you to cancel the download.
I want to get all images from the phone.for create multiple selections in photos library.I am using below code to get all images from the phone. it's working perfectly but when on the phone have 1000 images its take more time to get all images and show on view.How to reduce the time of getting all images from the phone.
// get all images from photos
func getAllImagesFromPhotos() -> [Photos]?
{
let imgManager = PHImageManager.default()
let requestOptions = PHImageRequestOptions()
requestOptions.isSynchronous = true
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key:"creationDate", ascending: true)]
let fetchResult = PHAsset.fetchAssets(with: PHAssetMediaType.image, options: fetchOptions)
var allImages = [Photos]()
for index in 0..<fetchResult.count
{
let asset = fetchResult.object(at: index) as PHAsset
imgManager.requestImage(for: asset, targetSize: CGSize(width:200,height:200), contentMode: .aspectFill, options: requestOptions, resultHandler: { (uiimage, info) in
if let image = uiimage
{
allImages.append(Photos(image: image, selected: false))
}
})
}
return allImages
}
To solve your problem you want to actually batch the image loading, load 10 images at a time, and display them, when the user scrolls a bit more, load the next batch, etc..
if you load all images, then of course there will be an unavoidable performance hit.
what you want to use instead is the UIImagePickerController class from UIKit
Here is the documentation from apple on UIImagePickerController.
If you don't want to use UIImagePickerController, then you have to load a smaller set of images at a time and allow the users to "load more" by scrolling further on the selection view.
EDIT: Multi image selection
After reading your comment about wanting to select multiple images, it's clear that UIImagePickerController is made to select single images, however, you can just not dismiss it upon selection, and have a toast message saying "Image selected", when the user select an image, and not dismiss the view, allowing the user to select another image, etc.. if the user select the same image again, you may just show the toast again saying "Image removed" since you can verify if the image has already been selected from before.
if you don't want to implement that workaround, your only choice is to batch load images in smaller chunks of around 10 images or so as I recommended in the original answer.
however, If you really don't want to implement that yourself, i recommend using a 3rd party library, something like ELCImagePickerController for example, there are hundreds of other libraries that do just that.
Hope this helps, good luck
I am building an app where you can pick images from your phone or make new ones with the camera. The question is about picking internal images. First I was using ImagepickerController where a new Controller pops up and I can select the image I want. Later I decided to change that design approach, but to embed all the pictures from my phone inside the main screen (the one that shows when you enter the app) where you can select each image by just tapping on it (so no new controller pops up). And there is the problem. Reading pictures and loading them inside my collectionview takes just too much time (reading pictures is the main issue). I have over 1000 pictures on my phone, and it takes like 7 seconds for the app to read 100 pictures (than it crashes, not sure why, maybe some memory issue, didnt look further into it bcz my main problem was the reading performance).
For reading I am using PHAssetCollection, this is the code:
let allPhotosResult = PHAsset.fetchAssets(with: PHAssetMediaType.image, options: nil)
let imageManager = PHCachingImageManager()
allPhotosResult.enumerateObjects({(object: AnyObject!,
count: Int,
stop: UnsafeMutablePointer<ObjCBool>) in
if object is PHAsset{
let imgAsset = object as! PHAsset
countI += 1
if countI > 100 {
return
}
let imageSize = CGSize(width: imgAsset.pixelWidth,
height: imgAsset.pixelHeight)
let options = PHImageRequestOptions()
options.deliveryMode = .fastFormat
options.isSynchronous = true
imageManager.requestImage(for: imgAsset,
targetSize: imageSize,
contentMode: .aspectFill,
options: options,
resultHandler: {
(image, info) -> Void in
self._images.append(image!)
}
})
}
})
I quit at 100 images bcz then they are being displayed, but again thats not the issue I think I will be able to solve that.
So, is there a faster way to read all the images and load them as thumbnails into my collectionview ? I didnt have any of the popular photos apps, but I downloaded some of them to see how their performance is, and Instagram is pretty impressive. All the images are there without any delay, which means there must be a way to get them all pretty fast. Maybe they fetch them in the background even when the app is not running ?
Regards
P.S. - ah yes, I know I can execute the reading async, but that doesnt solve my problem bcz the delay will still be there
I'm trying to develop an iOS app where users can pick their images from Camera Roll, Facebook, Instagram, etc. and edit them later (crop, filters, rotate).
Now, I'm having problems loading multiple images using Photos Framework to get PHAssets and converting them to full quality images and show them in next view.
When loading full quality images in SelectedImagesView I get memory warning and the app just blows because I'm storing images in memory.
How can I store a list of selected images (original and edited) and show them to the user according to the image "SelectedImagesView"?
1) PickImagesView
2) SelectedImagesView
So far, this is how I get the images using the Photo Framework:
Load the PHAssets from selected album in CollectionView
When user select an image, store the PHAsset identifier in NSUserDefault
When user finish selecting images, load all selected identifiers and send them to next View
Load images from Photos Library in SelectedImagesView with following code:
func processImages() {
for identifier in self.selectedAssets {
let options = PHImageRequestOptions()
options.deliveryMode = .FastFormat
// request images no bigger than 1/3 the screen width
let maxDimension = UIScreen.mainScreen().bounds.width / 3 * UIScreen.mainScreen().scale
let size = CGSize(width: maxDimension, height: maxDimension)
let assets = PHAsset.fetchAssetsWithLocalIdentifiers([identifier], options: nil)
guard let asset = assets.firstObject as? PHAsset
else { fatalError("no asset") }
PHImageManager.defaultManager().requestImageForAsset(asset, targetSize: PHImageManagerMaximumSize, contentMode: .AspectFill, options: options)
{ result, info in
// probably some of this code is unnecessary, too,
// but I'm not sure what you're doing here so leaving it alone
let photo: Photo = Photo()
photo.originalPhoto = result
self.selectedImages.append(photo)
}
}
self.tableView!.reloadData()
self.tableView!.reloadInputViews()
}
But the memory cost is too high.
Thanks in advance.
My ViewController looks like: Camera View on top and Photo Gallery on the bottom, like in Instagram.
When I try to load user's image library and display it on UICollectionView, app crashes with the Memory Warning. How can I prevent it and improve my function?
My function:
func getImagesFromLibrary(from: Int = 0, to: Int = 5, completion: (loaded: Bool) -> Void) {
if images.count > 0 {
for i in from..<to {
let asset: PHAsset = self.images[i] as! PHAsset
let imageFetchOptions = PHImageRequestOptions()
self.imageManager.requestImageForAsset(asset, targetSize: CGSize(width: 75, height: 75), contentMode: .AspectFit, options: imageFetchOptions, resultHandler: { (image: UIImage?, info: [NSObject : AnyObject]?) in
if (image != nil) {
if let img = image {
self.croppedImagesArray[i] = img
self.imagesCollectionView.reloadData()
}
}
})
}
completion(loaded: true)
} else {
print("Images Empty")
}
}
and I call it via:
if AVCaptureDevice.authorizationStatusForMediaType(AVMediaTypeVideo) == AVAuthorizationStatus.Authorized {
print("GRANTED")
self.getImagesFromLibrary { (loaded) in
if loaded {
print("Images Count: \(self.images.count)")
if self.images.count > 5 {
print("Second part")
self.getImagesFromLibrary(5, to: self.images.count / 2, completion: { (loaded) in
if loaded {
self.getImagesFromLibrary(self.images.count / 2, to: self.images.count, completion: { (loaded) in })
}
})
}
}
}
}
So, now I have in my gallery 300 images. It's a lot and that's why my app crashes on loading process.
How can I solve it? Any solutions?
Apparently, you simply hold too much images in memory, and thus get a memory warning.
When you get a memory warning, iOS gives you the opportunity to release data that are easily be recreated. If you don’t do so, your app can be terminated anytime.
So the question is if you really need to hold all those images in memory.
If so, you have a real problem, and iOS might not be the right platform for your app.
If not, e.g. if you want to show only part of the loaded images at a time, you should use placeholder images that are shown when a image that should be shown is not in memory, download the image, and replace the placeholder by the real image as soon as it has been downloaded.
One way to store images so that you won’t get a memory warning is to use a NSCache <Docs>, which is something like a NSMutableDictionary, in which you can store images, but images which have not been used long will be cleared automatically when memory becomes tight. So if you have to display an image, you could first display the placeholder, and then try to load it from the cache, and if this fails, to download it.
Of course, there are also frameworks that do the caching automatically for you, e.g. AFNetworking <Github>.
By default requestImageForAsset: is asynchronous. The code you have written will immediately fire off requests for all 300 images, and as you see run out of memory. So the code where you load 5 images, and then the first half, and then the second half is all just being run at once.
I would recommend in UICollection cellForRowAtIndexPath: you call self.imageManager.requestImageForAsset(self.images[indexPath.item] .... and set the UImageView for that cell in the result handler block. No need to preload all 300 images, the user may never even scroll to see them all. And someone else may have 3000 images or more.
Also in your result handler your calling collectionView reloadData every time an image is fetched, even reloading when the image that was fetched isn't even currently displayed. Use reloadItemsAtIndexPath: to just load the cell associated with the image.