2017年5月6日土曜日

PhotoLibrayにある画像、ビデオのファイルの名前、作成日時(アップロード日時)を取得する方法(iCloud共有含む)

PhotoLibrayにある画像、ビデオのファイルの名前、作成日時(アップロード日時)を取得する方法(iCloud共有含む)


iCloud共有にアップロードされたファイルをダウンロードすると、jpegの場合はExifデータから撮影日がわかるが、ビデオは撮影日時を調べるのに写真アプリで情報をチェックする必要があり、面倒だった。そこでプログラムでファイル名、CreateionDateを取得しようとすてみると、これもまた意外と面倒でした。

次のサンプルコードはPhotoLibray内のiCould、ローカル、画像、ビデオ、ライブフォトを含めてファイル名を取得するようにしたものです。

iPhoneで撮影したvideoをそのデバイスからiCloud共有にアップロードした場合は作成日時=撮影日時と考えて良いと思いますが、それ以外で撮影し、Macからアップロードした場合は作成日時=アップロード日時になるでしょう。また、その場合に拡張子がMP4以外の場合、iCloud共有からイクスポートしたファイルはもとの拡張子(M4Vなど)ですが、以下のプログラムで取得するファイル名の拡張子は全てMP4になります。

iPhoneとMacでiCloud共有の同じ写真やビデオのファイル名を比べてみると、同じものが多いものの、異なるものが結構あります。デバイス毎にファイル名が異なりますね。

MyAssetListViewControllerを呼び出す遷移元で、次の要領でPHAssetをフェッチする。


let sharedAlbums = PHAssetCollection.fetchAssetCollections(
      with: PHAssetCollectionType.album,
      subtype: PHAssetCollectionSubtype.albumCloudShared,

      options: fetchOptions)

let assetsFetchResults = PHAsset.fetchAssets(in: sharedAlbums, options: nil)


画面遷移時にMyAssetListViewControllerにassetsFetchResultsをセットする。



    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        let assetGridViewController = segue.destination as! MyAssetListViewController                      assetGridViewController.assetsFetchResults = assetsFetchResults
    }


//---- MyAssetListViewController ----


import UIKit
import Photos

/// UITableViewCellにプロパティーを追加。IBでコネクトする。
class MyTableViewCell: UITableViewCell {
    @IBOutlet var thumbnail: UIImageView?
    @IBOutlet var fileName: UILabel?
    @IBOutlet var creationDate: UILabel?
    @IBOutlet var resolution: UILabel?
    @IBOutlet var duration: UILabel?
}

class MyAssetListViewController: UITableViewController {

    // MyRootListViewControllerから画面遷移する時にセットするインスタンス変数
    var assetsFetchResults: PHFetchResult<PHAsset>?
    var albumTitle: String?
    
    var assetToFileName = [PHAsset: String]()
    let reuseIdentifier = "Cell"
    let imageManager = PHCachingImageManager()
    let dateFormatter = DateFormatter()
    let timeFormatter = DateComponentsFormatter()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.navigationItem.title = albumTitle
        // Formatterの設定
        dateFormatter.timeZone = TimeZone.current
        dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss"
        timeFormatter.allowedUnits = [NSCalendar.Unit.minute, NSCalendar.Unit.second]
        timeFormatter.zeroFormattingBehavior = .pad
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        guard let assets = self.assetsFetchResults else { return 0 }
        return assets.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MyTableViewCell
        guard let assets = self.assetsFetchResults else { return cell }
        
        let asset = assets[indexPath.item]
        cell.tag = asset.localIdentifier.hash
        //iCloudへアクセスするようにOptionを設定
        let options = PHImageRequestOptions()
        options.isNetworkAccessAllowed = true
        self.imageManager.requestImage(for: asset,
                         targetSize: CGSize(width:80, height:80),
                         contentMode: PHImageContentMode.aspectFit,
                         options: options,
                         resultHandler: {(result, info)->Void in
                         //非同期処理ためcellの再利用により異なるassetの内容を表示している場合がある。
                         //localIdentifierが異なる場合は何もしない。
                         guard cell.tag == asset.localIdentifier.hash,
                               let result = result else { return }
                         cell.thumbnail!.image = result
        })
        // assetから取得できるデータをセット
        cell.fileName!.text = nil
        cell.creationDate!.text = asset.creationDate == nil ? "(unknown)" : dateFormatter.string(from:asset.creationDate!)
        cell.resolution!.text = "\(asset.pixelWidth) x \(asset.pixelHeight)"
        cell.duration!.text = timeFormatter.string(from: asset.duration)
        
        // ファイル名セット
        // 既にassetResourcesがキャッシュされている場合はそれからファイル名を取得する。
        // iOS10時点では、LivePhotoの場合にassetResourcesJPGMOVの対がセットされている。
        let assetResources = PHAssetResource.assetResources(for: asset)
        if setFileName(from: assetResources, to: cell) == false {
            // assetResourcesが空の場合はfalseが返される。
            // LivePhoto以外のimagevideoの場合は空。LivePhotoでもキャッシュされていない場合は空。
            // assetResourcesがない場合はrequestContentEditingInputを取得する。
            setFileNameFromContentEditingInput(with: asset, to: cell)

            // AVURLAssetを使う場合はこちらに変更。
            //setFileNameFromAVAsset(with: asset, to: cell)
        }
        return cell
    }
    
    /// assetResource配列の各要素のoriginalFilenameを結合してlabelにセットする。
    /// assetResourcesが空の場合は"(searching)"をセットする。
    /// - parameter from: PHAssetResource.assetResources(asset)
    /// - parameter cell: MyTableViewCell
    func setFileName(from assetResources:[PHAssetResource], to cell: MyTableViewCell) -> Bool {
        let label = cell.fileName!
        if assetResources.count > 0 {
            label.text = nil
            for assetResource in assetResources {
                let fileName = assetResource.originalFilename
                if label.text == nil {
                    label.text = fileName
                } else {

                    //assetResourcesが複数あるので同じfileNameが重複する可能性がある。
                    label.text = label.text! + ", " + fileName
                }
            }
            return true
        } else {
            //assetResourcesが空の場合は"(searching)"をセット。
            label.text = "(searching)"
            return false
        }
    }
    
    /// assetからContentEditingInputを得て、livePhotoimagevideoの種類に応じてfileNameをセットする。
    /// - parameter with: PHAsset
    /// - parameter to: MyTableViewCell
    func setFileNameFromContentEditingInput(with asset: PHAsset, to cell: MyTableViewCell) {
        let options = PHContentEditingInputRequestOptions()
        options.isNetworkAccessAllowed = true
        asset.requestContentEditingInput(with: options,
           completionHandler: { (contentEditingInput, info) -> Void in
           guard cell.tag == asset.localIdentifier.hash else { return }
           
           if let livePhoto = contentEditingInput?.livePhoto {
               // live photoの場合はassetResourcesを取得してファイル名をセットする。
               let _ = self.setFileName(from: PHAssetResource.assetResources(for: livePhoto), to: cell)
           } else if let url = contentEditingInput?.fullSizeImageURL {
               // imageの場合
               cell.fileName!.text = url.path.components(separatedBy: "/").last
           } else if let urlAsset = contentEditingInput?.audiovisualAsset as? AVURLAsset {
               // videoの場合
               cell.fileName!.text = urlAsset.url.path.components(separatedBy: "/").last
               cell.duration!.text = self.timeFormatter.string(from: asset.duration)
           } else {
               cell.fileName!.text = "(not found)"
           }
        })
    }
}

-----------------------------

Live Photoの場合にJPG、MOVの対を取得するにはContentEditingInputを使わないとダメそうですが、MOVだけでよければ次のようなコードでAVURLAssetからファイル名を取得することができます。
上のコードのsetFileNameFromContentEditingInput呼び出しをこれの呼び出しに変更します。

func setFileNameFromAVAsset(with asset: PHAsset, to cell: MyTableViewCell) {
    if asset.mediaType == PHAssetMediaType.video {
        //iCloudへアクセスするようにOptionを設定
        let options = PHVideoRequestOptions()
        options.isNetworkAccessAllowed = true
        //PHAssetに対応するAVPlayerItemを取得
        let _ = self.imageManager.requestAVAsset(forVideo: asset,
                       options: options,
                       resultHandler: { (avAsset, avAudioMix, info) -> Void in
                           guard let urlAsset = avAsset asAVURLAsset,
                                 let fileName =   urlAsset.url.path.components(separatedBy: "/").last
                           else { return; }
                       cell.fileName!.text = fileName
                    })
        }
    }

-----------------------------

PHAssetからvalue(for: "filename")でファイル名を取得する方法もありました。
ビデオの場合も拡張子JPGのファイル名が返されますが、用途によってはこれで十分な場合もあるでしょう。
なお、PHAssetの公開仕様ではないので、今後のバージョンで同じ方法が使い続けれらるかはわかりません。


cell.fileName!.text = asset.value(forKey: "filename") as? String ?? "<no filename>"