PHPickerViewController allows access to copies of photo library assets as well as returning PHAssets in the results. To get PHAssets instead of file copies, I do:
let photolibrary = PHPhotoLibrary.shared()
var configuration = PHPickerConfiguration(photoLibrary: photolibrary)
configuration.filter = .videos
configuration.selectionLimit = 0
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = self
self.present(picker, animated: true, completion: nil)
And then,
//MARK:- PHPickerViewController delegate
#available(iOS 14, *)
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true) {
let identifiers:[String] = results.compactMap(\.assetIdentifier)
let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: identifiers, options: nil)
NSLog("\(identifiers), \(fetchResult)")
}
}
But the problem is once the photo picker is dismissed, it prompts for Photo Library access which is confusing and since the user anyways implicitly gave access to the selected assets in PHPickerViewController, PHPhotoLibrary should load those assets directly. Is there anyway to avoid the Photo library permission?
Is there anyway to avoid the Photo library permission?
Yes, sort of:
Change PHPickerConfiguration(photoLibrary: photolibrary) to PHPickerConfiguration()
Do not use the assetIdentifier to return to the photo library (it will be nil anyway)
In that case, the user can give you image data but that's all. The picker is out-of-process and no data from the library itself is really coming across, just a mere image that the user has explicitly selected.
However, if your goal really is to return to the photo library and obtain the PHAsset, then you must have permission, as you now are indeed attempting to probe the photo library behind the scenes within your app.
What I do, when my app depends on PHAsset information, is to ask for photo library permission (if needed) before presenting the picker, and I don't present the picker if I can't get permission. So, the user taps a button, I discover we have no permission, I ask for permission, we get it (let's say), and I present the picker (doing the asynchronous dance, because the permission arrives asynchronously). Looks great.
By the way, this is also just as true with UIImagePickerController.
Related
Im attempting to select assets using PHPickerViewController as a replacement for my UIImagePickerController, however when setting the filter and config options Im not seeing a way to set a limit on the length of video that is returned. i.e. the way one may have done using a UIImagePickerController through videoMaximumDuration.
Has anyone found any sort of workout?
private func presentPicker(filter: PHPickerFilter?)-> PHPickerViewController {
var configuration = PHPickerConfiguration(photoLibrary: .shared())
// Set the filter type according to the user’s selection.
configuration.filter = filter
// Set the mode to avoid transcoding, if possible, if your app supports arbitrary image/video encodings.
configuration.preferredAssetRepresentationMode = .current
// Set the selection behavior to respect the user’s selection order.
configuration.selection = .ordered
// Set the selection limit to enable singular selection.
configuration.selectionLimit = 1
// Set the preselected asset identifiers with the identifiers that the app tracks.
configuration.preselectedAssetIdentifiers = []
let picker = PHPickerViewController(configuration: configuration)
//picker.delegate = self
//present(picker, animated: true)
return picker
}
Having a look at the documentation for PHPickerFilter and the PHPickerViewController I can't see any native way to filter by video length.
The best solution that I can think of to conform to the delegate PHPickerViewControllerDelegate and check the video length after the user has selected it.
You haven't provided the rest of your code but you should conform to the PHPickerViewControllerDelegate and implement the required method:
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
guard let provider = results.first?.itemProvider else { return }
provider.loadItem(forTypeIdentifier: UTType.movie.identifier, options: [:]) { (videoURL, error) in
// Check the video length here
}
}
}
See the above code for an example on how to load the video. From here you can follow some of the existing answers to see how to extract the video duration. If the video duration is within the bounds you want to filter for, copy it to an appropriate location using FileManager and process it how you see fit, if it does not (IE it is too long, or too short for your liking), then discard it and tell the user using an alert / message.
Here is an example on how to get the duration of a video. Note: You just need to replace the references to urls to the videoURL you have access to in the completion handler (See the comment).
Get the accurate duration of a video
One other solution is to use a more powerful photo picker from a 3rd party.
This has the downside of requiring the user to provide permission to their whole photo library (where as photo picker does not need to prompt for permission, as it is an iOS provided & sandboxed view controller). It also introduces another dependency into your app.
If these downside are okay for your use case, then this might be just what you're looking for.
Here is a library that provides some more powerful filters: https://github.com/Yummypets/YPImagePicker
See the "Video" section on the README. It allows you to specify minimum and maximum video duration.
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 need to get the metaData from an Image I'm picking via UIImagePickerController.
This is my code:
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let image = info[UIImagePickerController.InfoKey.originalImage] as! UIImage
let metaData = info[UIImagePickerController.InfoKey.mediaMetadata] as? [AnyHashable: Any]
print(metaData)
picker.dismiss(animated: true, completion: nil)
}
It works fine, when im picking the Image having .camera as source. But when I use .photoLibrary as source, then metaData is nil. I already read through other questions and tried stuff like
let asset = info[.phAsset] as? PHAsset
print(asset?.creationDate ?? "None")
print(asset?.location ?? "None")
But this also returns nil. I guess the source of the problem is, that the info-Dictionary only returns 4 Keys when picking from .photoLibrary:
UIImagePickerControllerOriginalImage
UIImagePickerControllerMediaType
UIImagePickerControllerImageURL
UIImagePickerControllerReferenceURL
It would be awesome if someone could tell me where my mistake is.
Thanks in advance !
I had the same problem. If the user does not grant access to the photo library first, info[.phAsset] will return nil.
Quote from Apple: "Accessing the photo library always requires explicit permission from the user. The first time your app uses PHAsset, PHCollection, PHAssetCollection, or PHCollectionList methods to fetch content from the library ..."
Thus, you have to call PHPhotoLibrary.requestAuthorization{ ... } before presenting the image picker.
If the user denies the request, info[.phAsset] will also be nil!
This key is valid only when using an image picker whose source type is set to UIImagePickerController.SourceType.camera, and applies only to still images.
The value for this key is an NSDictionary object that contains the metadata of the photo that was just captured. To store the metadata along with the image in the Camera Roll, use the PHAssetChangeRequest class from the Photos framework.
https://developer.apple.com/documentation/uikit/uiimagepickercontroller/infokey/1619147-mediametadata
My application is having trouble locating the files that are in the music folder on my iOS device.
If the files are held below as application data (in my playpen), no problem. But when I go looking for the "Music" folder, using the following lines:
dest_dir = try! FileManager.default.url(for: .musicDirectory, in: .allDomainsMask, appropriateFor: nil, create: false)
musicDir = dest_dir
if UIApplication.shared.canOpenURL(musicDir) {
print("found music directory")
}else {
print("did not find music directory")
}
When the code executes the canOpenURL, I get a permissions complaint.
I also tried accessing the user directory (thinking I could then navigate into Music).
Any clues on how an application can access the files and playlists held under the music system folder on the iOS device?
There is no such thing as "the Music folder" in iOS. To access the user's music library, use the Media Player framework.
Example (assuming you have obtained the necessary authorization from the user):
let query = MPMediaQuery()
let result = query.items
Here you go! (Swift 4.2)
Inside a button method or #IBAction button
let mediaItems = MPMediaQuery.songs().items
let mediaCollection = MPMediaItemCollection(items: mediaItems ?? [])
let player = MPMusicPlayerController.systemMusicPlayer
player.setQueue(with: mediaCollection)
player.play()
let picker = MPMediaPickerController(mediaTypes: .anyAudio)
picker.delegate = self
picker.allowsPickingMultipleItems = false
picker.prompt = "Choose a song"
present(picker, animated: true, completion: nil)
iOS doesn’t give you direct access to the Music folder, for security and privacy reasons; you need to use the APIs in the MediaPlayer framework. Assuming you’re trying to play content from the user’s library, take a look at MPMusicPlayerController; you’ll need to provide it some MPMediaItem instances retrieved with an MPMediaQuery, then call methods from the MPMediaPlayback protocol on the controller to make it play the content.
Currently I am using this code:
#IBAction func selectPicture(sender: UIButton) {
if UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.PhotoLibrary){
var imag = UIImagePickerController()
imag.delegate = self
imag.sourceType = .Camera;
imag.mediaTypes = [kUTTypeImage as String]
imag.allowsEditing = false
imag.modalPresentationStyle = .Popover
self.presentViewController(imag, animated: true, completion: nil)
}
}
But when I run it on the iPad of iPhone device it asks me only for the camera permission and I don't have the possibility to choose a photo from any album/camera roll.
What can I do so I can access the photos from the device ?
You are setting the wrong sourceType:
imag.sourceType = .Camera;
Change that to:
imag.sourceType = .SavedPhotosAlbum;
or
imag.sourceType = .PhotoLibrary
The constants are defined in UIImagePickerControllerSourceType Enumeration and the definition is like:
UIImagePickerControllerSourceType
The source to use when picking an image or when determining available
media types. Declaration
Swift
enum UIImagePickerControllerSourceType : Int
{
case PhotoLibrary
case Camera
case SavedPhotosAlbum
}
Constants
PhotoLibrary
Specifies the device’s photo library as the source for the image picker controller.
Camera
Specifies the device’s built-in camera as the source for the image picker controller. Indicate the specific camera you want (front or
rear, as available) by using the cameraDevice property.
SavedPhotosAlbum
Specifies the device’s Camera Roll album as the source for the image picker controller. If the device does not have a camera,
specifies the Saved Photos album as the source.
Discussion
A given source may not be available on a given device because the
source is not physically present or because it cannot currently be
accessed.