I have an app that takes pictures that are stored in the photos library. I would like to be able to load just the images taken with the app into an app library .I have two functions that load the photos library and then request an individual image. The images are requested in a foreach loop. That works fine with full access. However, with limited access I get nothing. If I use the photo picker I get the pictures selected.
My retrieval code is:
func loadLibrary() {
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate",ascending: false)]
fetchOptions.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.image.rawValue)
assets = PHAsset.fetchAssets(with: fetchOptions)
}
func loadImage(_ asset: PHAsset) -> UIImage? {
var image: UIImage? = nil
let option = PHImageRequestOptions()
option.isSynchronous = true
option.isNetworkAccessAllowed = true
option.resizeMode = .fast
manager.requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFill, options: option) { img, err in
guard let img = img else { return }
image = img
}
return image
}
My saving code:
final class ImageSaver: NSObject, ObservableObject {
public static let shared = ImageSaver()
let objectDidChange = PassthroughSubject<Void, Never>()
#Published var saved = false {
didSet {
if saved {
objectDidChange.send()
}
}
}
func writeToPhotoAlbum(image: UIImage) {
UIImageWriteToSavedPhotosAlbum(image, self, #selector(saveError), nil)
}
#objc func saveError(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
if let error = error {
print("Image not saved. Error: \(error)")
self.saved = false
} else {
print("Save finished!")
self.saved = true
}
}
}
In the WWDC2020 Session Video Handle the Limited Photos Library in your app the video states that "When your app creates new assets they will automatically be included as part of the user's selection for the application." This is exactly the behavior I want, but it is not the behavior I am getting. Changing the privacy settings shows the fetch and load are working as expected.
Related
I am trying to upload a video to firebase Storage, to be able to do that I have to get the following:
let localFile = URL(string: "path/to/video")!
and then upload it with:
let uploadTask = riversRef.putFile(from: localFile
My problem is that I dont know how to get that path. So far I have a have a custom image / video picker, and then a function that can upload images. I just dont know how to upload Videos, for that I believe i need to get the path.
Any ideas on how to get localFile?
UPDATE:
This is my custom Picker
import SwiftUI
import PhotosUI
class ImagePickerViewModel: ObservableObject {
// MARK: Properties
#Published var fetchedImages: [ImageAsset] = []
#Published var selectedImages: [ImageAsset] = []
init(){
fetchImages()
}
// MARK: Fetching Images
func fetchImages(){
let options = PHFetchOptions()
// MARK: Modify As Per Your Wish
options.includeHiddenAssets = false
options.includeAssetSourceTypes = [.typeUserLibrary]
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
PHAsset.fetchAssets(with: .image, options: options).enumerateObjects { asset, _, _ in
let imageAsset: ImageAsset = .init(asset: asset)
self.fetchedImages.append(imageAsset)
}
PHAsset.fetchAssets(with: .video, options: options).enumerateObjects { asset, _, _ in
let imageAsset: ImageAsset = .init(asset: asset)
self.fetchedImages.append(imageAsset)
}
}
}
On my view I have this:
.popupImagePicker(show: $showPicker) { assets in
// MARK: Do Your Operation With PHAsset
// I'm Simply Extracting Image
// .init() Means Exact Size of the Image
let manager = PHCachingImageManager.default()
let options = PHImageRequestOptions()
options.isSynchronous = true
DispatchQueue.global(qos: .userInteractive).async {
assets.forEach { asset in
manager.requestImage(for: asset, targetSize: .init(), contentMode: .default, options: options) { image, _ in
guard let image = image else {return}
DispatchQueue.main.async {
self.pickedImages.append(image)
}
}
}
}
}
In one of my project there is a functionality to pick image from Photos. So for that I've used one third party lib named "YangMingShan". The functionality works perfect as per my requirement.
But the problem is that, I want to get the image name picked from the Photos. Whenever I picked single image from photos, it called below method. And it gives me image.
func photoPickerViewController(_ picker: YMSPhotoPickerViewController!, didFinishPicking image: UIImage!) {
}
Can anyone please help me to get the image name from image ?
func photoPickerViewController(_ picker: YMSPhotoPickerViewController!, didFinishPickingImages photoAssets: [PHAsset]!) {
// Remember images you get here is PHAsset array, you need to implement PHImageManager to get UIImage data by yourself
picker.dismiss(animated: true) {
let options = PHImageRequestOptions.init()
options.deliveryMode = .highQualityFormat
options.resizeMode = .exact
options.isSynchronous = true
var imagesWithName:[ImageModel] = []
for asset: PHAsset in photoAssets {
print(asset.originalFilename)
let image = asset.getImage()
let fileName = asset.originalFilename
imagesWithName.append(ImageModel(name: fileName, image: image))
}
print(imagesWithName)
// Assign to Array with images
}
}
the extension of PHAsset to get file name as follows
extension PHAsset {
var primaryResource: PHAssetResource? {
let types: Set<PHAssetResourceType>
switch mediaType {
case .video:
types = [.video, .fullSizeVideo]
case .image:
types = [.photo, .fullSizePhoto]
case .audio:
types = [.audio]
case .unknown:
types = []
#unknown default:
types = []
}
let resources = PHAssetResource.assetResources(for: self)
let resource = resources.first { types.contains($0.type)}
return resource ?? resources.first
}
var originalFilename: String {
guard let result = primaryResource else {
return "file"
}
return result.originalFilename
}
func getImage() -> UIImage {
let manager = PHImageManager.default()
let option = PHImageRequestOptions()
var thumbnail = UIImage()
option.isSynchronous = true
manager.requestImage(for: self,
targetSize: CGSize(width: self.pixelWidth, height: self.pixelHeight),
contentMode: .aspectFit,
options: option,
resultHandler: {(result, info) -> Void in
thumbnail = result!
})
return thumbnail
}
}
and the model
struct ImageModel {
let name:String
let image:UIImage
}
replace the line 77 in Pod -> YangMingSha -> YMSPhotoPicker -> YMSPhotoPickerViewController.h with
- (void)photoPickerViewController:(YMSPhotoPickerViewController *)picker didFinishPickingImage:(UIImage *)image didFinishPickingAssets:(PHAsset *)photoAssets;
then after build it will raise two error for that photoAsset parameter missing in delete function
in YMSPhotoPickerViewController.m replace the error part line 256 with
[self.delegate photoPickerViewController:self
didFinishPickingImage:[self yms_orientationNormalizedImage:image] didFinishPickingAssets:asset];
and in other error also replace the code with
PHAsset *asset = self.currentCollectionItem[#"assets"];
[self.delegate photoPickerViewController:self
didFinishPickingImage:[self yms_orientationNormalizedImage:image] didFinishPickingAssets:asset];
and the singleImage delegate method now should be like this
func photoPickerViewController(_ picker: YMSPhotoPickerViewController!, didFinishPicking image: UIImage!, didFinishPickingAssets photoAssets: PHAsset!) {
let asset = photoAssets.originalFilename
let image = photoAssets.getImage()
print(asset)
picker.dismiss(animated: true)
}
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 have an album of images that is managed by a remote server. I would like to give the user an option to download the album and store it to a custom album in Photos. But since the album is dynamic (photos get added to it) the user can download it multiple times. I don't want to download the same pictures multiple times, only the new ones.
Is it possible to associate some metadata (unique id) when I store the image in the Photo app? And then check if that image already exists?
I am using the Photos Framework to create the custom album and save the photos.
Edit: Here is my code for creating the custom album and saving photos
/** Returns the first album from the photos app with the specified name. */
static func getAlbumWithName(name: String, completion: (album: PHAssetCollection?) -> Void) {
let fetchOptions = PHFetchOptions()
fetchOptions.predicate = NSPredicate(format: "localizedTitle = %#", name)
let fetchResult = PHAssetCollection.fetchAssetCollectionsWithType(PHAssetCollectionType.Album, subtype: PHAssetCollectionSubtype.Any, options: fetchOptions)
if fetchResult.count > 0 {
guard let album = fetchResult.firstObject as? PHAssetCollection else {return}
completion(album: album)
} else {
PHPhotoLibrary.sharedPhotoLibrary().performChanges({
PHAssetCollectionChangeRequest.creationRequestForAssetCollectionWithTitle(name)
}, completionHandler: { (result, error) in
if result {
FileUtils.getAlbumWithName(name, completion: completion)
} else {
completion(album: nil)
}
})
}
}
/** Adds an image to the specified photos app album */
private static func addImage(image: UIImage, toAlbum album: PHAssetCollection, completion: ((status: Bool) -> Void)?) {
PHPhotoLibrary.sharedPhotoLibrary().performChanges({
let assetRequest = PHAssetChangeRequest.creationRequestForAssetFromImage(image)
let assetPlaceholder = assetRequest.placeholderForCreatedAsset
let albumChangeRequest = PHAssetCollectionChangeRequest(forAssetCollection: album)
albumChangeRequest?.addAssets([assetPlaceholder!])
}) { (status, error) in
completion?(status: status)
}
}
All you need to do is read "localIdentifier" from the asset placeholder. I've augmented your code to return the identifier in the completion handler. You may like to deal with those optionals.
private static func addImage(image: UIImage, toAlbum album: PHAssetCollection, completion: ((status: Bool, identifier: String?) -> Void)?) {
var localIdentifier: String?
PHPhotoLibrary.sharedPhotoLibrary().performChanges({
let assetRequest = PHAssetChangeRequest.creationRequestForAssetFromImage(image)
let assetPlaceholder = assetRequest.placeholderForCreatedAsset
let albumChangeRequest = PHAssetCollectionChangeRequest(forAssetCollection: album)
albumChangeRequest?.addAssets([assetPlaceholder!])
localIdentifier = assetPlaceholder?.localIdentifier
}) { (status, error) in
completion?(status: status, identifier: localIdentifier)
}
}
When you want to read that asset again your load image method might look something like this (I haven't used your conventions or variable names). This will read the asset synchronously but I'm sure you can spot the async option.
internal func loadPhoto(identifier: String) -> UIImage? {
if assetCollection == nil {
return nil
}
let fetchOptions = PHFetchOptions()
fetchOptions.predicate = NSPredicate(format: "localIdentifier = %#", identifier)
let fetchResult = PHAsset.fetchAssetsInAssetCollection(assetCollection, options: fetchOptions)
if fetchResult.count > 0 {
if let asset = fetchResult.firstObject as? PHAsset {
let options = PHImageRequestOptions()
options.deliveryMode = .HighQualityFormat
options.synchronous = true
var result: UIImage?
PHImageManager.defaultManager().requestImageForAsset(asset, targetSize: CGSize(width: asset.pixelWidth, height: asset.pixelHeight), contentMode: .AspectFit, options: options, resultHandler: {(image: UIImage?, _: [NSObject: AnyObject]?) -> Void in
result = image
})
return result
}
}
return nil
}
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?