import Foundation import MobileCoreServices import Flutter import UIKit import Photos public class SwiftPhotoGalleryPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "photo_gallery", binaryMessenger: registrar.messenger()) let instance = SwiftPhotoGalleryPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { if(call.method == "listAlbums") { let arguments = call.arguments as! Dictionary let mediumType = arguments["mediumType"] as! String let hideIfEmpty = arguments["hideIfEmpty"] as? Bool result(listAlbums(mediumType: mediumType, hideIfEmpty: hideIfEmpty)) } else if(call.method == "listMedia") { let arguments = call.arguments as! Dictionary let albumId = arguments["albumId"] as! String let mediumType = arguments["mediumType"] as! String let newest = arguments["newest"] as! Bool let skip = arguments["skip"] as? NSNumber let take = arguments["take"] as? NSNumber result(listMedia(albumId: albumId, mediumType: mediumType, newest: newest, skip: skip, take: take)) } else if(call.method == "getMedium") { let arguments = call.arguments as! Dictionary let mediumId = arguments["mediumId"] as! String getMedium( mediumId: mediumId, completion: { (data: [String: Any?]?, error: Error?) -> Void in result(data) }) } else if(call.method == "getThumbnail") { let arguments = call.arguments as! Dictionary let mediumId = arguments["mediumId"] as! String let width = arguments["width"] as? NSNumber let height = arguments["height"] as? NSNumber let highQuality = arguments["highQuality"] as? Bool getThumbnail( mediumId: mediumId, width: width, height: height, highQuality: highQuality, completion: { (data: Data?, error: Error?) -> Void in result(data) }) } else if(call.method == "getAlbumThumbnail") { let arguments = call.arguments as! Dictionary let albumId = arguments["albumId"] as! String let mediumType = arguments["mediumType"] as? String let width = arguments["width"] as? Int let height = arguments["height"] as? Int let highQuality = arguments["highQuality"] as? Bool getAlbumThumbnail( albumId: albumId, mediumType: mediumType, width: width, height: height, highQuality: highQuality, completion: { (data: Data?, error: Error?) -> Void in result(data) }) } else if(call.method == "getFile") { let arguments = call.arguments as! Dictionary let mediumId = arguments["mediumId"] as! String let mimeType = arguments["mimeType"] as? String getFile( mediumId: mediumId, mimeType: mimeType, completion: { (filepath: String?, error: Error?) -> Void in result(filepath?.replacingOccurrences(of: "file://", with: "")) }) } else if(call.method == "cleanCache") { cleanCache() result(nil) } else { result(FlutterMethodNotImplemented) } } private var assetCollections : [PHAssetCollection] = [] private func listAlbums(mediumType: String, hideIfEmpty: Bool? = true) -> [NSDictionary] { self.assetCollections = [] let fetchOptions = PHFetchOptions() var total = 0 var albums = [NSDictionary]() var albumIds = Set() func addCollection (collection: PHAssetCollection, hideIfEmpty: Bool) -> Void { let kRecentlyDeletedCollectionSubtype = PHAssetCollectionSubtype(rawValue: 1000000201) guard collection.assetCollectionSubtype != kRecentlyDeletedCollectionSubtype else { return } // De-duplicate by id. let albumId = collection.localIdentifier guard !albumIds.contains(albumId) else { return } albumIds.insert(albumId) let options = PHFetchOptions() options.predicate = self.predicateFromMediumType(mediumType: mediumType) if #available(iOS 9, *) { fetchOptions.fetchLimit = 1 } let count = PHAsset.fetchAssets(in: collection, options: options).count if(count > 0 || !hideIfEmpty) { total+=count self.assetCollections.append(collection) albums.append([ "id": collection.localIdentifier, "mediumType": mediumType, "name": collection.localizedTitle ?? "Unknown", "count": count, ]) } } func processPHAssetCollections(fetchResult: PHFetchResult, hideIfEmpty: Bool) -> Void { fetchResult.enumerateObjects { (assetCollection, _, _) in addCollection(collection: assetCollection, hideIfEmpty: hideIfEmpty) } } func processPHCollections (fetchResult: PHFetchResult, hideIfEmpty: Bool) -> Void { fetchResult.enumerateObjects { (collection, _, _) in if let assetCollection = collection as? PHAssetCollection { addCollection(collection: assetCollection, hideIfEmpty: hideIfEmpty) } else if let collectionList = collection as? PHCollectionList { processPHCollections(fetchResult: PHCollectionList.fetchCollections(in: collectionList, options: nil), hideIfEmpty: hideIfEmpty) } } } // Smart Albums. processPHAssetCollections( fetchResult: PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .albumRegular, options: fetchOptions), hideIfEmpty: hideIfEmpty ?? true ) // User-created collections. processPHCollections( fetchResult: PHAssetCollection.fetchTopLevelUserCollections(with: fetchOptions), hideIfEmpty: hideIfEmpty ?? true ) albums.insert([ "id": "__ALL__", "mediumType": mediumType, "name": "All", "count" : countMedia(collection: nil, mediumTypes: [mediumType]), ], at: 0) return albums } private func countMedia(collection: PHAssetCollection?, mediumTypes: [String]) -> Int { let options = PHFetchOptions() options.predicate = self.predicateFromMediumTypes(mediumTypes: mediumTypes) if(collection == nil) { return PHAsset.fetchAssets(with: options).count } return PHAsset.fetchAssets(in: collection ?? PHAssetCollection.init(), options: options).count } private func listMedia(albumId: String, mediumType: String, newest: Bool, skip: NSNumber?, take: NSNumber?) -> NSDictionary { let fetchOptions = PHFetchOptions() fetchOptions.sortDescriptors = [ NSSortDescriptor(key: "creationDate", ascending: newest ? false : true), NSSortDescriptor(key: "modificationDate", ascending: newest ? false : true) ] fetchOptions.predicate = predicateFromMediumType(mediumType: mediumType) let collection = self.assetCollections.first(where: { (collection) -> Bool in collection.localIdentifier == albumId }) let fetchResult = albumId == "__ALL__" ? PHAsset.fetchAssets(with: fetchOptions) : PHAsset.fetchAssets(in: collection ?? PHAssetCollection.init(), options: fetchOptions) let start = skip?.intValue ?? 0 let total = fetchResult.count let end = take == nil ? total : min(start + take!.intValue, total) var items = [[String: Any?]]() for index in start.. Void) { let fetchOptions = PHFetchOptions() if #available(iOS 9, *) { fetchOptions.fetchLimit = 1 } let assets: PHFetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [mediumId], options: fetchOptions) if (assets.count <= 0) { completion(nil, NSError(domain: "photo_gallery", code: 404, userInfo: nil)) } else { let asset: PHAsset = assets[0] getMediumFromAssetAsync( asset: asset, completion: { (data: [String: Any?]?, error: Error?) -> Void in completion(data, nil) } ) } } private func getThumbnail( mediumId: String, width: NSNumber?, height: NSNumber?, highQuality: Bool?, completion: @escaping (Data?, Error?) -> Void ) { let manager = PHImageManager.default() let fetchOptions = PHFetchOptions() if #available(iOS 9, *) { fetchOptions.fetchLimit = 1 } let assets: PHFetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [mediumId], options: fetchOptions) if (assets.count > 0) { let asset: PHAsset = assets[0] let options = PHImageRequestOptions() options.isSynchronous = false options.version = .current options.deliveryMode = (highQuality ?? false) ? .highQualityFormat : .fastFormat options.isNetworkAccessAllowed = true let imageSize = CGSize(width: width?.intValue ?? 128, height: height?.intValue ?? 128) manager.requestImage( for: asset, targetSize: CGSize(width: imageSize.width * UIScreen.main.scale, height: imageSize.height * UIScreen.main.scale), contentMode: PHImageContentMode.aspectFill, options: options, resultHandler: {(uiImage: UIImage?, info) in guard let image = uiImage else { completion(nil , NSError(domain: "photo_gallery", code: 404, userInfo: nil)) return } let bytes = image.jpegData(compressionQuality: CGFloat(70)) completion(bytes, nil) }) return } completion(nil , NSError(domain: "photo_gallery", code: 404, userInfo: nil)) } private func getAlbumThumbnail( albumId: String, mediumType: String?, width: Int?, height: Int?, highQuality: Bool?, completion: @escaping (Data?, Error?) -> Void ) { let manager = PHImageManager.default() let fetchOptions = PHFetchOptions() if (mediumType != nil) { fetchOptions.predicate = self.predicateFromMediumType(mediumType: mediumType!) } fetchOptions.sortDescriptors = [ NSSortDescriptor(key: "creationDate", ascending: false), NSSortDescriptor(key: "modificationDate", ascending: false) ] if #available(iOS 9, *) { fetchOptions.fetchLimit = 1 } let assets = albumId == "__ALL__" ? PHAsset.fetchAssets(with: fetchOptions) : PHAsset.fetchAssets(in: self.assetCollections.first(where: { (collection) -> Bool in collection.localIdentifier == albumId })!, options: fetchOptions) if (assets.count > 0) { let asset: PHAsset = assets[0] let options = PHImageRequestOptions() options.isSynchronous = false options.version = .current options.deliveryMode = (highQuality ?? false) ? .highQualityFormat : .fastFormat options.isNetworkAccessAllowed = true let imageSize = CGSize(width: width ?? 128, height: height ?? 128) manager.requestImage( for: asset, targetSize: CGSize( width: imageSize.width * UIScreen.main.scale, height: imageSize.height * UIScreen.main.scale ), contentMode: PHImageContentMode.aspectFill, options: options, resultHandler: {(uiImage: UIImage?, info) in guard let image = uiImage else { completion(nil , NSError(domain: "photo_gallery", code: 404, userInfo: nil)) return } let bytes = image.jpegData(compressionQuality: CGFloat(80)) completion(bytes, nil) }) return } completion(nil , NSError(domain: "photo_gallery", code: 404, userInfo: nil)) } private func getFile(mediumId: String, mimeType: String?, completion: @escaping (String?, Error?) -> Void) { let manager = PHImageManager.default() let fetchOptions = PHFetchOptions() if #available(iOS 9, *) { fetchOptions.fetchLimit = 1 } let assets: PHFetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [mediumId], options: fetchOptions) if (assets.count > 0) { let asset: PHAsset = assets[0] if(asset.mediaType == PHAssetMediaType.image) { let options = PHImageRequestOptions() options.isSynchronous = false options.version = .current options.deliveryMode = .highQualityFormat options.isNetworkAccessAllowed = true manager.requestImageData( for: asset, options: options, resultHandler: { (data: Data?, uti: String?, orientation, info) in DispatchQueue.main.async(execute: { guard let imageData = data else { completion(nil, NSError(domain: "photo_gallery", code: 404, userInfo: nil)) return } guard let assetUTI = uti else { completion(nil, NSError(domain: "photo_gallery", code: 404, userInfo: nil)) return } if mimeType != nil { let type = self.extractMimeTypeFromUTI(uti: assetUTI) if type != mimeType { let path = self.cacheImage(asset: asset, data: imageData, mimeType: mimeType!) completion(path, NSError(domain: "photo_gallery", code: 404, userInfo: nil)) return } } let fileExt = self.extractFileExtensionFromUTI(uti: assetUTI) let filepath = self.exportPathForAsset(asset: asset, ext: fileExt) try! imageData.write(to: filepath, options: .atomic) completion(filepath.absoluteString, nil) }) } ) } else if(asset.mediaType == PHAssetMediaType.video || asset.mediaType == PHAssetMediaType.audio) { let options = PHVideoRequestOptions() options.version = .current options.deliveryMode = .highQualityFormat options.isNetworkAccessAllowed = true manager.requestAVAsset(forVideo: asset, options: options, resultHandler: { (avAsset, avAudioMix, info) in DispatchQueue.main.async(execute: { do { let avAsset = avAsset as? AVURLAsset let data = try Data(contentsOf: avAsset!.url) let fileExt = self.extractFileExtensionFromAsset(asset: asset) let filepath = self.exportPathForAsset(asset: asset, ext: fileExt) try! data.write(to: filepath, options: .atomic) completion(filepath.absoluteString, nil) } catch { completion(nil, NSError(domain: "photo_gallery", code: 500, userInfo: nil)) } }) } ) } } } private func cacheImage(asset: PHAsset, data: Data, mimeType: String) -> String? { if mimeType == "image/jpeg" { let filepath = self.exportPathForAsset(asset: asset, ext: ".jpeg") let uiImage = UIImage(data: data) try! uiImage?.jpegData(compressionQuality: 100)?.write(to: filepath, options: .atomic) return filepath.absoluteString } else if mimeType == "image/png" { let filepath = self.exportPathForAsset(asset: asset, ext: ".png") let uiImage = UIImage(data: data) try! uiImage?.pngData()?.write(to: filepath, options: .atomic) return filepath.absoluteString } else { return nil } } private func getMediumFromAsset(asset: PHAsset) -> [String: Any?] { let mimeType = self.extractMimeTypeFromAsset(asset: asset) return [ "id": asset.localIdentifier, "mediumType": toDartMediumType(value: asset.mediaType), "mimeType": mimeType, "height": asset.pixelHeight, "width": asset.pixelWidth, "duration": NSInteger(asset.duration * 1000), "creationDate": (asset.creationDate != nil) ? NSInteger(asset.creationDate!.timeIntervalSince1970 * 1000) : nil, "modifiedDate": (asset.modificationDate != nil) ? NSInteger(asset.modificationDate!.timeIntervalSince1970 * 1000) : nil ] } private func getMediumFromAssetAsync(asset: PHAsset, completion: @escaping ([String : Any?]?, Error?) -> Void) -> Void { let mimeType = self.extractMimeTypeFromAsset(asset: asset) let manager = PHImageManager.default() manager.requestImageData( for: asset, options: nil, resultHandler: { (data: Data?, uti: String?, orientation: UIImage.Orientation, info: ([AnyHashable: Any]?)) -> Void in completion([ "id": asset.localIdentifier, "mediumType": self.toDartMediumType(value: asset.mediaType), "mimeType": mimeType, "height": asset.pixelHeight, "width": asset.pixelWidth, "orientation": self.toOrientationValue(orientation: orientation), "duration": NSInteger(asset.duration * 1000), "creationDate": (asset.creationDate != nil) ? NSInteger(asset.creationDate!.timeIntervalSince1970 * 1000) : nil, "modifiedDate": (asset.modificationDate != nil) ? NSInteger(asset.modificationDate!.timeIntervalSince1970 * 1000) : nil ], nil) } ) } private func exportPathForAsset(asset: PHAsset, ext: String) -> URL { let mediumId = asset.localIdentifier .replacingOccurrences(of: "/", with: "__") .replacingOccurrences(of: "\\", with: "__") let cachePath = self.cachePath() return cachePath.appendingPathComponent(mediumId + ext) } private func toSwiftMediumType(value: String) -> PHAssetMediaType? { switch value { case "image": return PHAssetMediaType.image case "video": return PHAssetMediaType.video case "audio": return PHAssetMediaType.audio default: return nil } } private func toDartMediumType(value: PHAssetMediaType) -> String? { switch value { case PHAssetMediaType.image: return "image" case PHAssetMediaType.video: return "video" case PHAssetMediaType.audio: return "audio" default: return nil } } private func toOrientationValue(orientation: UIImage.Orientation) -> Int { switch orientation { case UIImage.Orientation.up: return 1 case UIImage.Orientation.down: return 3 case UIImage.Orientation.left: return 6 case UIImage.Orientation.right: return 8 case UIImage.Orientation.upMirrored: return 2 case UIImage.Orientation.downMirrored: return 4 case UIImage.Orientation.leftMirrored: return 5 case UIImage.Orientation.rightMirrored: return 7 @unknown default: return 0 } } private func predicateFromMediumTypes(mediumTypes: [String]) -> NSPredicate { let predicates = mediumTypes.map { (dartValue) -> NSPredicate in return predicateFromMediumType(mediumType: dartValue) } return NSCompoundPredicate(type: NSCompoundPredicate.LogicalType.or, subpredicates: predicates) } private func predicateFromMediumType(mediumType: String) -> NSPredicate { let swiftType = toSwiftMediumType(value: mediumType) return NSPredicate(format: "mediaType = %d", swiftType!.rawValue) } private func extractFileExtensionFromUTI(uti: String?) -> String { guard let assetUTI = uti else { return "" } guard let ext = UTTypeCopyPreferredTagWithClass(assetUTI as CFString, kUTTagClassFilenameExtension as CFString)?.takeRetainedValue() as String? else { return "" } return "." + ext } private func extractMimeTypeFromUTI(uti: String?) -> String? { guard let assetUTI = uti else { return nil } guard let mimeType = UTTypeCopyPreferredTagWithClass(assetUTI as CFString, kUTTagClassMIMEType as CFString)?.takeRetainedValue() as String? else { return nil } return mimeType } private func extractUTIFromAsset(asset: PHAsset) -> String? { if #available(iOS 9, *) { let resourceList = PHAssetResource.assetResources(for: asset) if let resource = resourceList.first { return resource.uniformTypeIdentifier } } return asset.value(forKey: "uniformTypeIdentifier") as? String } private func extractFileExtensionFromAsset(asset: PHAsset) -> String { let uti = self.extractUTIFromAsset(asset: asset) return self.extractFileExtensionFromUTI(uti: uti) } private func extractMimeTypeFromAsset(asset: PHAsset) -> String? { let uti = self.extractUTIFromAsset(asset: asset) return self.extractMimeTypeFromUTI(uti: uti) } private func cachePath() -> URL { let paths = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask) let cacheFolder = paths[0].appendingPathComponent("photo_gallery") try! FileManager.default.createDirectory(at: cacheFolder, withIntermediateDirectories: true, attributes: nil) return cacheFolder } private func cleanCache() { try? FileManager.default.removeItem(at: self.cachePath()) } }