ZLPhotoManager.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. //
  2. // ZLPhotoManager.swift
  3. // ZLPhotoBrowser
  4. //
  5. // Created by long on 2020/8/11.
  6. //
  7. // Copyright (c) 2020 Long Zhang <495181165@qq.com>
  8. //
  9. // Permission is hereby granted, free of charge, to any person obtaining a copy
  10. // of this software and associated documentation files (the "Software"), to deal
  11. // in the Software without restriction, including without limitation the rights
  12. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. // copies of the Software, and to permit persons to whom the Software is
  14. // furnished to do so, subject to the following conditions:
  15. //
  16. // The above copyright notice and this permission notice shall be included in
  17. // all copies or substantial portions of the Software.
  18. //
  19. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. // THE SOFTWARE.
  26. import UIKit
  27. import Photos
  28. public class ZLPhotoManager: NSObject {
  29. /// Save image to album.
  30. @objc public class func saveImageToAlbum(image: UIImage, completion: ( (Bool, PHAsset?) -> Void )? ) {
  31. let status = PHPhotoLibrary.authorizationStatus()
  32. if status == .denied || status == .restricted {
  33. completion?(false, nil)
  34. return
  35. }
  36. var placeholderAsset: PHObjectPlaceholder? = nil
  37. PHPhotoLibrary.shared().performChanges({
  38. let newAssetRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)
  39. placeholderAsset = newAssetRequest.placeholderForCreatedAsset
  40. }) { (suc, error) in
  41. DispatchQueue.main.async {
  42. if suc {
  43. let asset = self.getAsset(from: placeholderAsset?.localIdentifier)
  44. completion?(suc, asset)
  45. } else {
  46. completion?(false, nil)
  47. }
  48. }
  49. }
  50. }
  51. /// Save video to album.
  52. @objc public class func saveVideoToAlbum(url: URL, completion: ( (Bool, PHAsset?) -> Void )? ) {
  53. let status = PHPhotoLibrary.authorizationStatus()
  54. if status == .denied || status == .restricted {
  55. completion?(false, nil)
  56. return
  57. }
  58. var placeholderAsset: PHObjectPlaceholder? = nil
  59. PHPhotoLibrary.shared().performChanges({
  60. let newAssetRequest = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
  61. placeholderAsset = newAssetRequest?.placeholderForCreatedAsset
  62. }) { (suc, error) in
  63. DispatchQueue.main.async {
  64. if suc {
  65. let asset = self.getAsset(from: placeholderAsset?.localIdentifier)
  66. completion?(suc, asset)
  67. } else {
  68. completion?(false, nil)
  69. }
  70. }
  71. }
  72. }
  73. private class func getAsset(from localIdentifier: String?) -> PHAsset? {
  74. guard let id = localIdentifier else {
  75. return nil
  76. }
  77. let result = PHAsset.fetchAssets(withLocalIdentifiers: [id], options: nil)
  78. if result.count > 0{
  79. return result[0]
  80. }
  81. return nil
  82. }
  83. /// Fetch photos from result.
  84. class func fetchPhoto(in result: PHFetchResult<PHAsset>, ascending: Bool, allowSelectImage: Bool, allowSelectVideo: Bool, limitCount: Int = .max) -> [ZLPhotoModel] {
  85. var models: [ZLPhotoModel] = []
  86. let option: NSEnumerationOptions = ascending ? .init(rawValue: 0) : .reverse
  87. var count = 1
  88. result.enumerateObjects(options: option) { (asset, index, stop) in
  89. let m = ZLPhotoModel(asset: asset)
  90. if m.type == .image, !allowSelectImage {
  91. return
  92. }
  93. if m.type == .video, !allowSelectVideo {
  94. return
  95. }
  96. if count == limitCount {
  97. stop.pointee = true
  98. }
  99. models.append(m)
  100. count += 1
  101. }
  102. return models
  103. }
  104. /// Fetch all album list.
  105. class func getPhotoAlbumList(ascending: Bool, allowSelectImage: Bool, allowSelectVideo: Bool, completion: ( ([ZLAlbumListModel]) -> Void )) {
  106. let option = PHFetchOptions()
  107. if !allowSelectImage {
  108. option.predicate = NSPredicate(format: "mediaType == %ld", PHAssetMediaType.video.rawValue)
  109. }
  110. if !allowSelectVideo {
  111. option.predicate = NSPredicate(format: "mediaType == %ld", PHAssetMediaType.image.rawValue)
  112. }
  113. let smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .albumRegular, options: nil) as! PHFetchResult<PHCollection>
  114. let albums = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumRegular, options: nil) as! PHFetchResult<PHCollection>
  115. let streamAlbums = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumMyPhotoStream, options: nil) as! PHFetchResult<PHCollection>
  116. let syncedAlbums = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumSyncedAlbum, options: nil) as! PHFetchResult<PHCollection>
  117. let sharedAlbums = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumCloudShared, options: nil) as! PHFetchResult<PHCollection>
  118. let arr = [smartAlbums, albums, streamAlbums, syncedAlbums, sharedAlbums]
  119. var albumList: [ZLAlbumListModel] = []
  120. arr.forEach { (album) in
  121. album.enumerateObjects { (collection, _, _) in
  122. guard let collection = collection as? PHAssetCollection else { return }
  123. if collection.assetCollectionSubtype == .smartAlbumAllHidden {
  124. return
  125. }
  126. if #available(iOS 11.0, *), collection.assetCollectionSubtype.rawValue > PHAssetCollectionSubtype.smartAlbumLongExposures.rawValue {
  127. return
  128. }
  129. let result = PHAsset.fetchAssets(in: collection, options: option)
  130. if result.count == 0 {
  131. return
  132. }
  133. let title = self.getCollectionTitle(collection)
  134. if collection.assetCollectionSubtype == .smartAlbumUserLibrary {
  135. // Album of all photos.
  136. let m = ZLAlbumListModel(title: title, result: result, collection: collection, option: option, isCameraRoll: true)
  137. albumList.insert(m, at: 0)
  138. } else {
  139. let m = ZLAlbumListModel(title: title, result: result, collection: collection, option: option, isCameraRoll: false)
  140. albumList.append(m)
  141. }
  142. }
  143. }
  144. completion(albumList)
  145. }
  146. /// Fetch camera roll album.
  147. public class func getCameraRollAlbum(allowSelectImage: Bool, allowSelectVideo: Bool, completion: @escaping ( (ZLAlbumListModel) -> Void )) {
  148. let option = PHFetchOptions()
  149. if !allowSelectImage {
  150. option.predicate = NSPredicate(format: "mediaType == %ld", PHAssetMediaType.video.rawValue)
  151. }
  152. if !allowSelectVideo {
  153. option.predicate = NSPredicate(format: "mediaType == %ld", PHAssetMediaType.image.rawValue)
  154. }
  155. let smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .albumRegular, options: nil)
  156. smartAlbums.enumerateObjects { (collection, _, stop) in
  157. if collection.assetCollectionSubtype == .smartAlbumUserLibrary {
  158. let result = PHAsset.fetchAssets(in: collection, options: option)
  159. let albumModel = ZLAlbumListModel(title: self.getCollectionTitle(collection), result: result, collection: collection, option: option, isCameraRoll: true)
  160. completion(albumModel)
  161. stop.pointee = true
  162. }
  163. }
  164. }
  165. /// Conversion collection title.
  166. private class func getCollectionTitle(_ collection: PHAssetCollection) -> String {
  167. if collection.assetCollectionType == .album {
  168. // Albums created by user.
  169. var title: String? = nil
  170. if ZLCustomLanguageDeploy.language == .system {
  171. title = collection.localizedTitle
  172. } else {
  173. switch collection.assetCollectionSubtype {
  174. case .albumMyPhotoStream:
  175. title = localLanguageTextValue(.myPhotoStream)
  176. default:
  177. title = collection.localizedTitle
  178. }
  179. }
  180. return title ?? localLanguageTextValue(.noTitleAlbumListPlaceholder)
  181. }
  182. var title: String? = nil
  183. if ZLCustomLanguageDeploy.language == .system {
  184. title = collection.localizedTitle
  185. } else {
  186. switch collection.assetCollectionSubtype {
  187. case .smartAlbumUserLibrary:
  188. title = localLanguageTextValue(.cameraRoll)
  189. case .smartAlbumPanoramas:
  190. title = localLanguageTextValue(.panoramas)
  191. case .smartAlbumVideos:
  192. title = localLanguageTextValue(.videos)
  193. case .smartAlbumFavorites:
  194. title = localLanguageTextValue(.favorites)
  195. case .smartAlbumTimelapses:
  196. title = localLanguageTextValue(.timelapses)
  197. case .smartAlbumRecentlyAdded:
  198. title = localLanguageTextValue(.recentlyAdded)
  199. case .smartAlbumBursts:
  200. title = localLanguageTextValue(.bursts)
  201. case .smartAlbumSlomoVideos:
  202. title = localLanguageTextValue(.slomoVideos)
  203. case .smartAlbumSelfPortraits:
  204. title = localLanguageTextValue(.selfPortraits)
  205. case .smartAlbumScreenshots:
  206. title = localLanguageTextValue(.screenshots)
  207. case .smartAlbumDepthEffect:
  208. title = localLanguageTextValue(.depthEffect)
  209. case .smartAlbumLivePhotos:
  210. title = localLanguageTextValue(.livePhotos)
  211. default:
  212. title = collection.localizedTitle
  213. }
  214. if #available(iOS 11.0, *) {
  215. if collection.assetCollectionSubtype == PHAssetCollectionSubtype.smartAlbumAnimated {
  216. title = localLanguageTextValue(.animated)
  217. }
  218. }
  219. }
  220. return title ?? localLanguageTextValue(.noTitleAlbumListPlaceholder)
  221. }
  222. @discardableResult
  223. public class func fetchImage(for asset: PHAsset, size: CGSize, progress: ( (CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable : Any]?) -> Void )? = nil, completion: @escaping ( (UIImage?, Bool) -> Void )) -> PHImageRequestID {
  224. return self.fetchImage(for: asset, size: size, resizeMode: .fast, progress: progress, completion: completion)
  225. }
  226. @discardableResult
  227. class func fetchOriginalImage(for asset: PHAsset, progress: ( (CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable : Any]?) -> Void )? = nil, completion: @escaping ( (UIImage?, Bool) -> Void)) -> PHImageRequestID {
  228. return self.fetchImage(for: asset, size: PHImageManagerMaximumSize, resizeMode: .fast, progress: progress, completion: completion)
  229. }
  230. /// Fetch asset data.
  231. @discardableResult
  232. class func fetchOriginalImageData(for asset: PHAsset, progress: ( (CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable : Any]?) -> Void )? = nil, completion: @escaping ( (Data, [AnyHashable: Any]?, Bool) -> Void)) -> PHImageRequestID {
  233. let option = PHImageRequestOptions()
  234. if (asset.value(forKey: "filename") as? String)?.hasSuffix("GIF") == true {
  235. option.version = .original
  236. }
  237. option.isNetworkAccessAllowed = true
  238. option.resizeMode = .fast
  239. option.deliveryMode = .highQualityFormat
  240. option.progressHandler = { (pro, error, stop, info) in
  241. DispatchQueue.main.async {
  242. progress?(CGFloat(pro), error, stop, info)
  243. }
  244. }
  245. return PHImageManager.default().requestImageData(for: asset, options: option) { (data, _, _, info) in
  246. let cancel = info?[PHImageCancelledKey] as? Bool ?? false
  247. let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
  248. if !cancel, let data = data {
  249. completion(data, info, isDegraded)
  250. }
  251. }
  252. }
  253. /// Fetch image for asset.
  254. private class func fetchImage(for asset: PHAsset, size: CGSize, resizeMode: PHImageRequestOptionsResizeMode, progress: ( (CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable : Any]?) -> Void )? = nil, completion: @escaping ( (UIImage?, Bool) -> Void )) -> PHImageRequestID {
  255. let option = PHImageRequestOptions()
  256. option.resizeMode = resizeMode
  257. option.isNetworkAccessAllowed = true
  258. option.progressHandler = { (pro, error, stop, info) in
  259. DispatchQueue.main.async {
  260. progress?(CGFloat(pro), error, stop, info)
  261. }
  262. }
  263. return PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: option) { (image, info) in
  264. var downloadFinished = false
  265. if let info = info {
  266. downloadFinished = !(info[PHImageCancelledKey] as? Bool ?? false) && (info[PHImageErrorKey] == nil)
  267. }
  268. let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
  269. if downloadFinished {
  270. completion(image, isDegraded)
  271. }
  272. }
  273. }
  274. class func fetchLivePhoto(for asset: PHAsset, completion: @escaping ( (PHLivePhoto?, [AnyHashable: Any]?, Bool) -> Void )) -> PHImageRequestID {
  275. let option = PHLivePhotoRequestOptions()
  276. option.version = .current
  277. option.deliveryMode = .opportunistic
  278. option.isNetworkAccessAllowed = true
  279. return PHImageManager.default().requestLivePhoto(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFit, options: option) { (livePhoto, info) in
  280. let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
  281. completion(livePhoto, info, isDegraded)
  282. }
  283. }
  284. class func fetchVideo(for asset: PHAsset, progress: ( (CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable : Any]?) -> Void )? = nil, completion: @escaping ( (AVPlayerItem?, [AnyHashable: Any]?, Bool) -> Void )) -> PHImageRequestID {
  285. let option = PHVideoRequestOptions()
  286. option.isNetworkAccessAllowed = true
  287. option.progressHandler = { (pro, error, stop, info) in
  288. DispatchQueue.main.async {
  289. progress?(CGFloat(pro), error, stop, info)
  290. }
  291. }
  292. // https://github.com/longitachi/ZLPhotoBrowser/issues/369#issuecomment-728679135
  293. if asset.isInCloud {
  294. return PHImageManager.default().requestExportSession(forVideo: asset, options: option, exportPreset: AVAssetExportPresetHighestQuality, resultHandler: { (session, info) in
  295. // iOS11 and earlier, callback is not on the main thread.
  296. DispatchQueue.main.async {
  297. let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
  298. if let avAsset = session?.asset {
  299. let item = AVPlayerItem(asset: avAsset)
  300. completion(item, info, isDegraded)
  301. }
  302. }
  303. })
  304. } else {
  305. return PHImageManager.default().requestPlayerItem(forVideo: asset, options: option) { (item, info) in
  306. // iOS11 and earlier, callback is not on the main thread.
  307. DispatchQueue.main.async {
  308. let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
  309. completion(item, info, isDegraded)
  310. }
  311. }
  312. }
  313. }
  314. class func isFetchImageError(_ error: Error?) -> Bool {
  315. guard let e = error as NSError? else {
  316. return false
  317. }
  318. if e.domain == "CKErrorDomain" || e.domain == "CloudPhotoLibraryErrorDomain" {
  319. return true
  320. }
  321. return false
  322. }
  323. @objc public class func fetchAVAsset(forVideo asset: PHAsset, completion: @escaping ( (AVAsset?, [AnyHashable: Any]?) -> Void )) -> PHImageRequestID {
  324. let options = PHVideoRequestOptions()
  325. options.version = .original
  326. options.deliveryMode = .automatic
  327. options.isNetworkAccessAllowed = true
  328. if asset.isInCloud {
  329. return PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { (session, info) in
  330. // iOS11 and earlier, callback is not on the main thread.
  331. DispatchQueue.main.async {
  332. if let avAsset = session?.asset {
  333. completion(avAsset, info)
  334. }
  335. }
  336. }
  337. } else {
  338. return PHImageManager.default().requestAVAsset(forVideo: asset, options: options) { (avAsset, _, info) in
  339. DispatchQueue.main.async {
  340. completion(avAsset, info)
  341. }
  342. }
  343. }
  344. }
  345. /// Fetch asset local file path.
  346. @objc public class func fetchAssetFilePath(asset: PHAsset, completion: @escaping (String?) -> Void ) {
  347. asset.requestContentEditingInput(with: nil) { (input, info) in
  348. var path = input?.fullSizeImageURL?.absoluteString
  349. if path == nil, let dir = asset.value(forKey: "directory") as? String, let name = asset.value(forKey: "filename") as? String {
  350. path = String(format: "file:///var/mobile/Media/%@/%@", dir, name)
  351. }
  352. completion(path)
  353. }
  354. }
  355. }
  356. /// Authority related.
  357. extension ZLPhotoManager {
  358. public class func hasPhotoLibratyAuthority() -> Bool {
  359. return PHPhotoLibrary.authorizationStatus() == .authorized
  360. }
  361. public class func hasCameraAuthority() -> Bool {
  362. let status = AVCaptureDevice.authorizationStatus(for: .video)
  363. if status == .restricted || status == .denied {
  364. return false
  365. }
  366. return true
  367. }
  368. public class func hasMicrophoneAuthority() -> Bool {
  369. let status = AVCaptureDevice.authorizationStatus(for: .audio)
  370. if status == .restricted || status == .denied {
  371. return false
  372. }
  373. return true
  374. }
  375. }