TLAssetsCollection.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. //
  2. // TLAssetsCollection.swift
  3. // TLPhotosPicker
  4. //
  5. // Created by wade.hawk on 2017. 4. 18..
  6. // Copyright © 2017년 wade.hawk. All rights reserved.
  7. //
  8. import Foundation
  9. import Photos
  10. import PhotosUI
  11. import MobileCoreServices
  12. public struct TLPHAsset {
  13. enum CloudDownloadState {
  14. case ready, progress, complete, failed
  15. }
  16. public enum AssetType {
  17. case photo, video, livePhoto
  18. }
  19. public enum ImageExtType: String {
  20. case png, jpg, gif, heic
  21. }
  22. var state = CloudDownloadState.ready
  23. public var phAsset: PHAsset? = nil
  24. public var selectedOrder: Int = 0
  25. public var type: AssetType {
  26. get {
  27. guard let phAsset = self.phAsset else { return .photo }
  28. if phAsset.mediaSubtypes.contains(.photoLive) {
  29. return .livePhoto
  30. }else if phAsset.mediaType == .video {
  31. return .video
  32. }else {
  33. return .photo
  34. }
  35. }
  36. }
  37. public var fullResolutionImage: UIImage? {
  38. get {
  39. guard let phAsset = self.phAsset else { return nil }
  40. return TLPhotoLibrary.fullResolutionImageData(asset: phAsset)
  41. }
  42. }
  43. public func extType() -> ImageExtType {
  44. var ext = ImageExtType.png
  45. if let fileName = self.originalFileName, let extention = URL(string: fileName)?.pathExtension.lowercased() {
  46. ext = ImageExtType(rawValue: extention) ?? .png
  47. }
  48. return ext
  49. }
  50. @discardableResult
  51. public func cloudImageDownload(progressBlock: @escaping (Double) -> Void, completionBlock:@escaping (UIImage?)-> Void ) -> PHImageRequestID? {
  52. guard let phAsset = self.phAsset else { return nil }
  53. return TLPhotoLibrary.cloudImageDownload(asset: phAsset, progressBlock: progressBlock, completionBlock: completionBlock)
  54. }
  55. public var originalFileName: String? {
  56. get {
  57. guard let phAsset = self.phAsset,let resource = PHAssetResource.assetResources(for: phAsset).first else { return nil }
  58. return resource.originalFilename
  59. }
  60. }
  61. public func photoSize(options: PHImageRequestOptions? = nil ,completion: @escaping ((Int)->Void), livePhotoVideoSize: Bool = false) {
  62. guard let phAsset = self.phAsset, self.type == .photo else { completion(-1); return }
  63. var resource: PHAssetResource? = nil
  64. if phAsset.mediaSubtypes.contains(.photoLive) == true, livePhotoVideoSize {
  65. resource = PHAssetResource.assetResources(for: phAsset).filter { $0.type == .pairedVideo }.first
  66. }else {
  67. resource = PHAssetResource.assetResources(for: phAsset).filter { $0.type == .photo }.first
  68. }
  69. if let fileSize = resource?.value(forKey: "fileSize") as? Int {
  70. completion(fileSize)
  71. }else {
  72. PHImageManager.default().requestImageData(for: phAsset, options: nil) { (data, uti, orientation, info) in
  73. var fileSize = -1
  74. if let data = data {
  75. let bcf = ByteCountFormatter()
  76. bcf.countStyle = .file
  77. fileSize = data.count
  78. }
  79. DispatchQueue.main.async {
  80. completion(fileSize)
  81. }
  82. }
  83. }
  84. }
  85. public func videoSize(options: PHVideoRequestOptions? = nil, completion: @escaping ((Int)->Void)) {
  86. guard let phAsset = self.phAsset, self.type == .video else { completion(-1); return }
  87. let resource = PHAssetResource.assetResources(for: phAsset).filter { $0.type == .video }.first
  88. if let fileSize = resource?.value(forKey: "fileSize") as? Int {
  89. completion(fileSize)
  90. }else {
  91. PHImageManager.default().requestAVAsset(forVideo: phAsset, options: options) { (avasset, audioMix, info) in
  92. func fileSize(_ url: URL?) -> Int? {
  93. do {
  94. guard let fileSize = try url?.resourceValues(forKeys: [.fileSizeKey]).fileSize else { return nil }
  95. return fileSize
  96. }catch { return nil }
  97. }
  98. var url: URL? = nil
  99. if let urlAsset = avasset as? AVURLAsset {
  100. url = urlAsset.url
  101. }else if let sandboxKeys = info?["PHImageFileSandboxExtensionTokenKey"] as? String, let path = sandboxKeys.components(separatedBy: ";").last {
  102. url = URL(fileURLWithPath: path)
  103. }
  104. let size = fileSize(url) ?? -1
  105. DispatchQueue.main.async {
  106. completion(size)
  107. }
  108. }
  109. }
  110. }
  111. func MIMEType(_ url: URL?) -> String? {
  112. guard let ext = url?.pathExtension else { return nil }
  113. if !ext.isEmpty {
  114. let UTIRef = UTTypeCreatePreferredIdentifierForTag("public.filename-extension" as CFString, ext as CFString, nil)
  115. let UTI = UTIRef?.takeUnretainedValue()
  116. UTIRef?.release()
  117. if let UTI = UTI {
  118. guard let MIMETypeRef = UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType) else { return nil }
  119. let MIMEType = MIMETypeRef.takeUnretainedValue()
  120. MIMETypeRef.release()
  121. return MIMEType as String
  122. }
  123. }
  124. return nil
  125. }
  126. @discardableResult
  127. //convertLivePhotosToPNG
  128. // false : If you want mov file at live photos
  129. // true : If you want png file at live photos ( HEIC )
  130. public func tempCopyMediaFile(videoRequestOptions: PHVideoRequestOptions? = nil, imageRequestOptions: PHImageRequestOptions? = nil, exportPreset: String = AVAssetExportPresetHighestQuality, convertLivePhotosToJPG: Bool = false, progressBlock:((Double) -> Void)? = nil, completionBlock:@escaping ((URL,String) -> Void)) -> PHImageRequestID? {
  131. guard let phAsset = self.phAsset else { return nil }
  132. var type: PHAssetResourceType? = nil
  133. if phAsset.mediaSubtypes.contains(.photoLive) == true, convertLivePhotosToJPG == false {
  134. type = .pairedVideo
  135. }else {
  136. type = phAsset.mediaType == .video ? .video : .photo
  137. }
  138. guard let resource = (PHAssetResource.assetResources(for: phAsset).filter{ $0.type == type }).first else { return nil }
  139. let fileName = resource.originalFilename
  140. var writeURL: URL? = nil
  141. if #available(iOS 10.0, *) {
  142. writeURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(fileName)")
  143. } else {
  144. writeURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent("\(fileName)")
  145. }
  146. if (writeURL?.pathExtension.uppercased() == "HEIC" || writeURL?.pathExtension.uppercased() == "HEIF") && convertLivePhotosToJPG {
  147. if let fileName2 = writeURL?.deletingPathExtension().lastPathComponent {
  148. writeURL?.deleteLastPathComponent()
  149. writeURL?.appendPathComponent("\(fileName2).jpg")
  150. }
  151. }
  152. guard let localURL = writeURL,let mimetype = MIMEType(writeURL) else { return nil }
  153. switch phAsset.mediaType {
  154. case .video:
  155. var requestOptions = PHVideoRequestOptions()
  156. if let options = videoRequestOptions {
  157. requestOptions = options
  158. }else {
  159. requestOptions.isNetworkAccessAllowed = true
  160. }
  161. //iCloud download progress
  162. requestOptions.progressHandler = { (progress, error, stop, info) in
  163. DispatchQueue.main.async {
  164. progressBlock?(progress)
  165. }
  166. }
  167. return PHImageManager.default().requestExportSession(forVideo: phAsset, options: requestOptions, exportPreset: exportPreset) { (session, infoDict) in
  168. session?.outputURL = localURL
  169. session?.outputFileType = AVFileType.mov
  170. session?.exportAsynchronously(completionHandler: {
  171. DispatchQueue.main.async {
  172. completionBlock(localURL, mimetype)
  173. }
  174. })
  175. }
  176. case .image:
  177. var requestOptions = PHImageRequestOptions()
  178. if let options = imageRequestOptions {
  179. requestOptions = options
  180. }else {
  181. requestOptions.isNetworkAccessAllowed = true
  182. }
  183. //iCloud download progress
  184. requestOptions.progressHandler = { (progress, error, stop, info) in
  185. DispatchQueue.main.async {
  186. progressBlock?(progress)
  187. }
  188. }
  189. return PHImageManager.default().requestImageData(for: phAsset, options: requestOptions, resultHandler: { (data, uti, orientation, info) in
  190. do {
  191. var data = data
  192. if convertLivePhotosToJPG == true, let imgData = data, let rawImage = UIImage(data: imgData)?.upOrientationImage() {
  193. data = rawImage.jpegData(compressionQuality: 1)
  194. }
  195. try data?.write(to: localURL)
  196. DispatchQueue.main.async {
  197. completionBlock(localURL, mimetype)
  198. }
  199. }catch { }
  200. })
  201. default:
  202. return nil
  203. }
  204. }
  205. //Apparently, this method is not be safety to export a video.
  206. //There is many way that export a video.
  207. //This method was one of them.
  208. public func exportVideoFile(options: PHVideoRequestOptions? = nil, progressBlock:((Float) -> Void)? = nil, completionBlock:@escaping ((URL,String) -> Void)) {
  209. guard let phAsset = self.phAsset, phAsset.mediaType == .video else { return }
  210. var type = PHAssetResourceType.video
  211. guard let resource = (PHAssetResource.assetResources(for: phAsset).filter{ $0.type == type }).first else { return }
  212. let fileName = resource.originalFilename
  213. var writeURL: URL? = nil
  214. if #available(iOS 10.0, *) {
  215. writeURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(fileName)")
  216. } else {
  217. writeURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent("\(fileName)")
  218. }
  219. guard let localURL = writeURL,let mimetype = MIMEType(writeURL) else { return }
  220. var requestOptions = PHVideoRequestOptions()
  221. if let options = options {
  222. requestOptions = options
  223. }else {
  224. requestOptions.isNetworkAccessAllowed = true
  225. }
  226. //iCloud download progress
  227. //options.progressHandler = { (progress, error, stop, info) in
  228. //}
  229. PHImageManager.default().requestAVAsset(forVideo: phAsset, options: options) { (avasset, avaudioMix, infoDict) in
  230. guard let avasset = avasset else { return }
  231. let exportSession = AVAssetExportSession.init(asset: avasset, presetName: AVAssetExportPresetHighestQuality)
  232. exportSession?.outputURL = localURL
  233. exportSession?.outputFileType = AVFileType.mov
  234. exportSession?.exportAsynchronously(completionHandler: {
  235. completionBlock(localURL,mimetype)
  236. })
  237. func checkExportSession() {
  238. DispatchQueue.global().async { [weak exportSession] in
  239. guard let exportSession = exportSession else { return }
  240. switch exportSession.status {
  241. case .waiting,.exporting:
  242. DispatchQueue.main.async {
  243. progressBlock?(exportSession.progress)
  244. }
  245. Thread.sleep(forTimeInterval: 1)
  246. checkExportSession()
  247. default:
  248. break
  249. }
  250. }
  251. }
  252. checkExportSession()
  253. }
  254. }
  255. init(asset: PHAsset?) {
  256. self.phAsset = asset
  257. }
  258. }
  259. extension TLPHAsset: Equatable {
  260. public static func ==(lhs: TLPHAsset, rhs: TLPHAsset) -> Bool {
  261. guard let lphAsset = lhs.phAsset, let rphAsset = rhs.phAsset else { return false }
  262. return lphAsset.localIdentifier == rphAsset.localIdentifier
  263. }
  264. }
  265. struct TLAssetsCollection {
  266. var phAssetCollection: PHAssetCollection? = nil
  267. var fetchResult: PHFetchResult<PHAsset>? = nil
  268. var useCameraButton: Bool = false
  269. var recentPosition: CGPoint = CGPoint.zero
  270. var title: String
  271. var localIdentifier: String
  272. var count: Int {
  273. get {
  274. guard let count = self.fetchResult?.count, count > 0 else { return self.useCameraButton ? 1 : 0 }
  275. return count + (self.useCameraButton ? 1 : 0)
  276. }
  277. }
  278. init(collection: PHAssetCollection) {
  279. self.phAssetCollection = collection
  280. self.title = collection.localizedTitle ?? ""
  281. self.localIdentifier = collection.localIdentifier
  282. }
  283. func getAsset(at index: Int) -> PHAsset? {
  284. if self.useCameraButton && index == 0 { return nil }
  285. let index = index - (self.useCameraButton ? 1 : 0)
  286. guard let result = self.fetchResult, index < result.count else { return nil }
  287. return result.object(at: max(index,0))
  288. }
  289. func getTLAsset(at index: Int) -> TLPHAsset? {
  290. if self.useCameraButton && index == 0 { return nil }
  291. let index = index - (self.useCameraButton ? 1 : 0)
  292. guard let result = self.fetchResult, index < result.count else { return nil }
  293. return TLPHAsset(asset: result.object(at: max(index,0)))
  294. }
  295. func getAssets(at range: CountableClosedRange<Int>) -> [PHAsset]? {
  296. let lowerBound = range.lowerBound - (self.useCameraButton ? 1 : 0)
  297. let upperBound = range.upperBound - (self.useCameraButton ? 1 : 0)
  298. return self.fetchResult?.objects(at: IndexSet(integersIn: max(lowerBound,0)...min(upperBound,count)))
  299. }
  300. static func ==(lhs: TLAssetsCollection, rhs: TLAssetsCollection) -> Bool {
  301. return lhs.localIdentifier == rhs.localIdentifier
  302. }
  303. }
  304. extension UIImage {
  305. func upOrientationImage() -> UIImage? {
  306. switch imageOrientation {
  307. case .up:
  308. return self
  309. default:
  310. UIGraphicsBeginImageContextWithOptions(size, false, scale)
  311. draw(in: CGRect(origin: .zero, size: size))
  312. let result = UIGraphicsGetImageFromCurrentImageContext()
  313. UIGraphicsEndImageContext()
  314. return result
  315. }
  316. }
  317. }