How to use PHAsset to get [PHAssetCollection] - ios

I can get all the photos, but I do not know how to find the albums in which these photos are located.
Another option is to go through all the albums and get photos from them.
I think this is not a good option.
The goal is to get the name of the albums for each photo.
private func getPhotosAll() -> [String:Photo] {
var photos = [String:Photo]()
let assets = PHAsset.fetchAssets(with: .image, options: nil)
assets.enumerateObjects({ (asset, index, stop) in
let id = asset.localIdentifier
guard (id.isEmpty == false) else {
return
}
guard let name = asset.originalFilename else {
return
}
let photo = Photo()
photo.name = name
// TODO: Read albums?
photo.albums = [String]()
photos[id] = photo
})
return photos
}
Update. I get only user albums. And how to get the rest? (Camera Roll, People, Places, Recently Deleted)
var albums = Set<String>()
let typesAlbum: [PHAssetCollectionType] = [.album, .smartAlbum, .moment]
for type in typesAlbum {
let collectionsBox = PHAssetCollection.fetchAssetCollectionsContaining(asset,
with: type, options: nil)
collectionsBox.enumerateObjects { (collection, _, _) in
guard let albumName = collection.localizedTitle else {
return
}
albums.insert(albumName)
}
}

For each PHAsset, call fetchAssetCollectionsContaining(_:with:options:).

Related

iOS Photos library - how to get the REAL filename?

I am looking for either a Swift or Objective C solution for this. Here, I am showing my example using swift.
I am using the Photos framework on iOS and having a hard time finding the REAL filename instead of the IMG_4227.JPG type. The name of my image is actually myawesomefilename.jpg and I see this file name if I airdrop or transfer to dropbox the image from my iPhone. But when I use the Photos framework, I get the names as IMG_4227.JPG.
There is an app on the app store which is able to get the real file names of all images, so it is definitely possible. I just don't know how.
Here's how I am currently getting the IMG_4227.JPG type names:
func exifTapped(asset: PHAsset) {
print("name: \(asset.value(forKey: "filename"))")
getURL(ofPhotoWith: asset) { (url) in
print("URL: \(url)")
let data = NSData.init(contentsOf: url!)!
if let imageSource = CGImageSourceCreateWithData(data, nil) {
let imageProperties2 = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil)! as NSDictionary
print("imageProperties2: ", imageProperties2)
}
}
}
func getURL(ofPhotoWith mPhasset: PHAsset, completionHandler : #escaping ((_ responseURL : URL?) -> Void)) {
if mPhasset.mediaType == .image {
let options: PHContentEditingInputRequestOptions = PHContentEditingInputRequestOptions()
options.isNetworkAccessAllowed = true
options.canHandleAdjustmentData = {(adjustmeta: PHAdjustmentData) -> Bool in
return true
}
mPhasset.requestContentEditingInput(with: options, completionHandler: { (contentEditingInput, info) in
completionHandler(contentEditingInput!.fullSizeImageURL)
})
} else if mPhasset.mediaType == .video {
let options: PHVideoRequestOptions = PHVideoRequestOptions()
options.version = .original
options.isNetworkAccessAllowed = true
PHImageManager.default().requestAVAsset(forVideo: mPhasset, options: options, resultHandler: { (asset, audioMix, info) in
if let urlAsset = asset as? AVURLAsset {
let localVideoUrl = urlAsset.url
completionHandler(localVideoUrl)
} else {
completionHandler(nil)
}
})
}
}
EDIT: None of the duplicate solutions are any different than what I already have and they all give the IMG_4227.JPG type names instead of the real name. So this is not a duplicate.
I figured it out.
let resources = PHAssetResource.assetResources(for: asset)
print("Filename: \((resources.first as! PHAssetResource).originalFilename)")

Modifing metadata from existing phAsset seems not working

In my App I want to make it possible, that the user sets an StarRating from 0 to 5 for any Image he has in his PhotoLibrary. My research shows, that there are a couple of ways to get this done:
Save the exif metadata using the new PHPhotoLibrary
Swift: Custom camera save modified metadata with image
Writing a Photo with Metadata using Photokit
Most of these Answers were creating a new Photo. My snippet now looks like this:
let options = PHContentEditingInputRequestOptions()
options.isNetworkAccessAllowed = true
self.requestContentEditingInput(with: options, completionHandler: {
(contentEditingInput, _) -> Void in
if contentEditingInput != nil {
if let url = contentEditingInput!.fullSizeImageURL {
if let nsurl = url as? NSURL {
if let imageSource = CGImageSourceCreateWithURL(nsurl, nil) {
var imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as Dictionary?
if imageProperties != nil {
imageProperties![kCGImagePropertyIPTCStarRating] = rating as AnyObject
let imageData = NSMutableData(contentsOf: url)
let image = UIImage(contentsOfFile: url.path)
let destination = CGImageDestinationCreateWithData(imageData!, CGImageSourceGetType(imageSource)!, 1, nil)
CGImageDestinationAddImage(destination!, image!.cgImage!, imageProperties! as CFDictionary)
var contentEditingOutput : PHContentEditingOutput? = nil
if CGImageDestinationFinalize(destination!) {
let archievedData = NSKeyedArchiver.archivedData(withRootObject: rating)
let identifier = "com.example.starrating"
let adjustmentData = PHAdjustmentData(formatIdentifier: identifier, formatVersion: "1.0", data: archievedData)
contentEditingOutput = PHContentEditingOutput(contentEditingInput: contentEditingInput!)
contentEditingOutput!.adjustmentData = adjustmentData
if imageData!.write(to: contentEditingOutput!.renderedContentURL, atomically: true) {
PHPhotoLibrary.shared().performChanges({
let request = PHAssetChangeRequest(for: self)
request.contentEditingOutput = contentEditingOutput
}, completionHandler: {
success, error in
if success && error == nil {
completion(true)
} else {
completion(false)
}
})
}
} else {
completion(false)
}
}
}
}
}
}
})
Now when I want to read the metadata from the PHAsset I request the ContentEditingInput again and do the following:
if let url = contentEditingInput!.fullSizeImageURL {
if let nsurl = url as? NSURL {
if let imageSource = CGImageSourceCreateWithURL(nsurl, nil) {
if let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as Dictionary? {
if let starRating = imageProperties[kCGImagePropertyIPTCStarRating] as? Int {
rating = starRating
}
}
}
}
}
But I never get my rating because it says that the value of imageProperties[kCGImagePropertyIPTCStarRating] is nil.
I also tried the examples from the Answers I posted above, but I always get the same result.
I hope anybody knows, what I can do to change the Metadata.
Also, how can I change the Metadata from an PHAsset with the MediaType .video? I tried to achieve that through the AVAssetWriter and AVExportSession Objects, but in both cases it does not work. Here what I tried for Videos:
var exportSession = AVAssetExportSession(asset: asset!, presetName: AVAssetExportPresetPassthrough)
exportSession!.outputURL = outputURL
exportSession!.outputFileType = AVFileTypeQuickTimeMovie
exportSession!.timeRange = CMTimeRange(start: start, duration: duration)
var modifiedMetadata = asset!.metadata
let metadataItem = AVMutableMetadataItem()
metadataItem.keySpace = AVMetadataKeySpaceQuickTimeMetadata
metadataItem.key = AVMetadataQuickTimeMetadataKeyRatingUser as NSCopying & NSObjectProtocol
metadataItem.value = rating as NSCopying & NSObjectProtocol
modifiedMetadata.append(metadataItem)
exportSession!.metadata = modifiedMetadata
exportSession!.exportAsynchronously(completionHandler: {
let status = exportSession?.status
let success = status == AVAssetExportSessionStatus.completed
if success {
do {
let sourceURL = urlAsset.url
let manager = FileManager.default
_ = try manager.removeItem(at: sourceURL)
_ = try manager.moveItem(at: outputURL, to: sourceURL)
} catch {
LogError("\(error)")
completion(false)
}
} else {
LogError("\(exportSession!.error!)")
completion(false)
}
})
Sorry this isn't a full answer but it covers one part of your question. I noticed you are placing the StarRating in the wrong place. You need to place it in a IPTC dictionary. Also the properties data is stored as strings. Given you have the imageProperties you can add the star rating as follows and read it back using the following two functions
func setIPTCStarRating(imageProperties : NSMutableDictionary, rating : Int) {
if let iptc = imageProperties[kCGImagePropertyIPTCDictionary] as? NSMutableDictionary {
iptc[kCGImagePropertyIPTCStarRating] = String(rating)
} else {
let iptc = NSMutableDictionary()
iptc[kCGImagePropertyIPTCStarRating] = String(rating)
imageProperties[kCGImagePropertyIPTCDictionary] = iptc
}
}
func getIPTCStarRating(imageProperties : NSMutableDictionary) -> Int? {
if let iptc = imageProperties[kCGImagePropertyIPTCDictionary] as? NSDictionary {
if let starRating = iptc[kCGImagePropertyIPTCStarRating] as? String {
return Int(starRating)
}
}
return nil
}
As the imageProperties you get from the image are not mutable you need to create a mutable copy of these properties first before you can call the functions above. When you create your image to save use the mutable properties in your call to CGImageDestinationAddImage()
if let mutableProperties = imageProperties.mutableCopy() as? NSMutableDictionary {
setIPTCStarRating(imageProperties:mutableProperties, rating:rating)
}
One other point you are creating an unnecessary UIImage. If you use CGImageDestinationAddImageFromSource() instead of CGImageDestinationAddImage() you can use the imageSource you created earlier instead of loading the image data into a UIImage.

How to get the date of a photo using PHPhotoLibrary

I'm trying to convert from ALAssetsLibrary to PHPhotoLibrary since it is deprecated. I need to grab the date from a chosen photo from the photo library but can't figure out the right way. Here is how I'm currently doing it.
mediaUrl = info[UIImagePickerControllerReferenceURL] as? NSURL
...
let assetsLibrary = ALAssetsLibrary()
assetsLibrary.assetForURL(mediaUrl, resultBlock: { (asset) -> Void in
guard let asset = asset else { return }
guard let date = asset.valueForProperty(ALAssetPropertyDate) as? NSDate else { return }
let dateString = dateFormatter.stringFromDate(date)
//---- use date string here
}) { (error) -> Void in
print(error)
}
Here is where I've gotten to:
let photoLibrary = PHPhotoLibrary.sharedPhotoLibrary()
var photoAssetPlaceholder: PHObjectPlaceholder!
photoLibrary.performChanges({
let request = PHAssetChangeRequest.creationRequestForAssetFromImage(image)
photoAssetPlaceholder = request.placeholderForCreatedAsset
}, completionHandler: { success, error in
if success {
print (photoAssetPlaceholder)
//How do I get date from PhotoAssetPlaceholder???
} else {
print(error?.localizedDescription)
}
})
I feel like I'm close. Any help would be appreciated!
The model objects that represents the photos and videos themselves are of type PHAsset.
A PHAsset contains metadata such as the asset’s media type and its creation date.

Check if image exists in Photos

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
}

Creating an array of tracks from multiple playlists in Spotify iOS SDK 3.0

I was able to do this with CocoaLibSpotify, but I can't figure out how it should be done in the new Spotify iOS SDK.
I'm trying to create an array of tracks from multiple playlists that I load in via their URIs. The goal here is to be able to play, at random, tracks from across a set of playlists.
I've been able to load in a playlist from its URI, it's represented as an SPPlaylistSnapshot. There doesn't seem to be a way to get the individual tracks from this SPPlaylistSnapshot so I can create a pool that I can add to and draw from.
Does anyone know a way to do this?
SPTPlaylistSnapshot has firstTrackPage property, which contains the first x tracks, where x is a number I can't remember. With that first page, you can request additional pages until you have all the tracks.
See the documentation for SPTPlaylistSnapshot and SPTListPage for details.
After authentication, define a playlist request , like this:
let playListRequest = try! SPTPlaylistList.createRequestForGettingPlaylists(forUser: userName, withAccessToken: token)
I use alamofire to post this request:
Alamofire.request(playListRequest)
.response { response in
let list = try! SPTPlaylistList(from: response.data, with: response.response)
for playList in list.items {
if let playlist = playList as? SPTPartialPlaylist {
print( playlist.name ) // playlist name
print( playlist.uri) // playlist uri
let stringFromUrl = playlist.uri.absoluteString
let uri = URL(string: stringFromUrl)
// use SPTPlaylistSnapshot to get all the playlists
SPTPlaylistSnapshot.playlist(withURI: uri, accessToken: token!) { (error, snap) in
if let s = snap as? SPTPlaylistSnapshot {
// get the tracks for each playlist
print(s.name)
for track in s.firstTrackPage.items {
if let thistrack = track as? SPTPlaylistTrack {
print(thistrack.name)
}
}
}
}
}
}
}
Basically what everyone's saying is correct, use SPTPlaylistSnapshot to get the tracks for the playlist, here's my code for getting all the items in a playlist in Swift 3.
func getTracksFrom(page:SPTListPage, allItems:[SPTPlaylistTrack]) -> [SPTPlaylistTrack] {
guard let items = page.items as? [SPTPlaylistTrack] else {print("empty page");return allItems}
var allTracks = allItems
allTracks.append(contentsOf: items)
var nextPage = SPTListPage()
if page.hasNextPage {
page.requestNextPage(withAccessToken: (SPTAuth.defaultInstance().session.accessToken)!) { (error, data) in
guard let p = data as? SPTListPage else {return}
nextPage = p
}
return getTracksFrom(page:nextPage,allItems:allTracks)
}
return allTracks
}
func getAllTracksFrom(_ playlist:SPTPlaylistSnapshot) -> [SPTPlaylistTrack] {
var allTracks:[SPTPlaylistTrack] = []
allTracks = getTracksFrom(page: playlist.firstTrackPage, allItems: allTracks)
return allTracks
}
override func viewDidLoad() {
super.viewDidLoad()
guard let p = playlist else {return}
SPTPlaylistSnapshot.playlist(withURI: p.uri, accessToken: (SPTAuth.defaultInstance().session.accessToken)!, callback: { (error, dataOrNil) in
guard error == nil else {print(error!);return}
guard let playlistSnapshot = dataOrNil as? SPTPlaylistSnapshot else {print("couldn't cast as SPTPlaylistSnapshot");return}
self.tracks = self.getAllTracksFrom(playlistSnapshot)
self.tableView.reloadData()
})
}

Resources