| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421 |
- //
- // ZLPhotoManager.swift
- // ZLPhotoBrowser
- //
- // Created by long on 2020/8/11.
- //
- // Copyright (c) 2020 Long Zhang <495181165@qq.com>
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to deal
- // in the Software without restriction, including without limitation the rights
- // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- // copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in
- // all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- // THE SOFTWARE.
- import UIKit
- import Photos
- public class ZLPhotoManager: NSObject {
- /// Save image to album.
- @objc public class func saveImageToAlbum(image: UIImage, completion: ( (Bool, PHAsset?) -> Void )? ) {
- let status = PHPhotoLibrary.authorizationStatus()
-
- if status == .denied || status == .restricted {
- completion?(false, nil)
- return
- }
-
- var placeholderAsset: PHObjectPlaceholder? = nil
- PHPhotoLibrary.shared().performChanges({
- let newAssetRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)
- placeholderAsset = newAssetRequest.placeholderForCreatedAsset
- }) { (suc, error) in
- DispatchQueue.main.async {
- if suc {
- let asset = self.getAsset(from: placeholderAsset?.localIdentifier)
- completion?(suc, asset)
- } else {
- completion?(false, nil)
- }
- }
- }
- }
-
- /// Save video to album.
- @objc public class func saveVideoToAlbum(url: URL, completion: ( (Bool, PHAsset?) -> Void )? ) {
- let status = PHPhotoLibrary.authorizationStatus()
-
- if status == .denied || status == .restricted {
- completion?(false, nil)
- return
- }
-
- var placeholderAsset: PHObjectPlaceholder? = nil
- PHPhotoLibrary.shared().performChanges({
- let newAssetRequest = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
- placeholderAsset = newAssetRequest?.placeholderForCreatedAsset
- }) { (suc, error) in
- DispatchQueue.main.async {
- if suc {
- let asset = self.getAsset(from: placeholderAsset?.localIdentifier)
- completion?(suc, asset)
- } else {
- completion?(false, nil)
- }
- }
- }
- }
-
- private class func getAsset(from localIdentifier: String?) -> PHAsset? {
- guard let id = localIdentifier else {
- return nil
- }
- let result = PHAsset.fetchAssets(withLocalIdentifiers: [id], options: nil)
- if result.count > 0{
- return result[0]
- }
- return nil
- }
-
- /// Fetch photos from result.
- class func fetchPhoto(in result: PHFetchResult<PHAsset>, ascending: Bool, allowSelectImage: Bool, allowSelectVideo: Bool, limitCount: Int = .max) -> [ZLPhotoModel] {
- var models: [ZLPhotoModel] = []
- let option: NSEnumerationOptions = ascending ? .init(rawValue: 0) : .reverse
- var count = 1
-
- result.enumerateObjects(options: option) { (asset, index, stop) in
- let m = ZLPhotoModel(asset: asset)
-
- if m.type == .image, !allowSelectImage {
- return
- }
- if m.type == .video, !allowSelectVideo {
- return
- }
- if count == limitCount {
- stop.pointee = true
- }
-
- models.append(m)
- count += 1
- }
-
- return models
- }
-
- /// Fetch all album list.
- class func getPhotoAlbumList(ascending: Bool, allowSelectImage: Bool, allowSelectVideo: Bool, completion: ( ([ZLAlbumListModel]) -> Void )) {
- let option = PHFetchOptions()
- if !allowSelectImage {
- option.predicate = NSPredicate(format: "mediaType == %ld", PHAssetMediaType.video.rawValue)
- }
- if !allowSelectVideo {
- option.predicate = NSPredicate(format: "mediaType == %ld", PHAssetMediaType.image.rawValue)
- }
-
- let smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .albumRegular, options: nil) as! PHFetchResult<PHCollection>
- let albums = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumRegular, options: nil) as! PHFetchResult<PHCollection>
- let streamAlbums = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumMyPhotoStream, options: nil) as! PHFetchResult<PHCollection>
- let syncedAlbums = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumSyncedAlbum, options: nil) as! PHFetchResult<PHCollection>
- let sharedAlbums = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumCloudShared, options: nil) as! PHFetchResult<PHCollection>
- let arr = [smartAlbums, albums, streamAlbums, syncedAlbums, sharedAlbums]
-
- var albumList: [ZLAlbumListModel] = []
- arr.forEach { (album) in
- album.enumerateObjects { (collection, _, _) in
- guard let collection = collection as? PHAssetCollection else { return }
- if collection.assetCollectionSubtype == .smartAlbumAllHidden {
- return
- }
- if #available(iOS 11.0, *), collection.assetCollectionSubtype.rawValue > PHAssetCollectionSubtype.smartAlbumLongExposures.rawValue {
- return
- }
- let result = PHAsset.fetchAssets(in: collection, options: option)
- if result.count == 0 {
- return
- }
- let title = self.getCollectionTitle(collection)
-
- if collection.assetCollectionSubtype == .smartAlbumUserLibrary {
- // Album of all photos.
- let m = ZLAlbumListModel(title: title, result: result, collection: collection, option: option, isCameraRoll: true)
- albumList.insert(m, at: 0)
- } else {
- let m = ZLAlbumListModel(title: title, result: result, collection: collection, option: option, isCameraRoll: false)
- albumList.append(m)
- }
- }
- }
-
- completion(albumList)
- }
-
- /// Fetch camera roll album.
- public class func getCameraRollAlbum(allowSelectImage: Bool, allowSelectVideo: Bool, completion: @escaping ( (ZLAlbumListModel) -> Void )) {
- let option = PHFetchOptions()
- if !allowSelectImage {
- option.predicate = NSPredicate(format: "mediaType == %ld", PHAssetMediaType.video.rawValue)
- }
- if !allowSelectVideo {
- option.predicate = NSPredicate(format: "mediaType == %ld", PHAssetMediaType.image.rawValue)
- }
-
- let smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .albumRegular, options: nil)
- smartAlbums.enumerateObjects { (collection, _, stop) in
- if collection.assetCollectionSubtype == .smartAlbumUserLibrary {
- let result = PHAsset.fetchAssets(in: collection, options: option)
- let albumModel = ZLAlbumListModel(title: self.getCollectionTitle(collection), result: result, collection: collection, option: option, isCameraRoll: true)
- completion(albumModel)
- stop.pointee = true
- }
- }
- }
-
- /// Conversion collection title.
- private class func getCollectionTitle(_ collection: PHAssetCollection) -> String {
- if collection.assetCollectionType == .album {
- // Albums created by user.
- var title: String? = nil
- if ZLCustomLanguageDeploy.language == .system {
- title = collection.localizedTitle
- } else {
- switch collection.assetCollectionSubtype {
- case .albumMyPhotoStream:
- title = localLanguageTextValue(.myPhotoStream)
- default:
- title = collection.localizedTitle
- }
- }
- return title ?? localLanguageTextValue(.noTitleAlbumListPlaceholder)
- }
-
- var title: String? = nil
- if ZLCustomLanguageDeploy.language == .system {
- title = collection.localizedTitle
- } else {
- switch collection.assetCollectionSubtype {
- case .smartAlbumUserLibrary:
- title = localLanguageTextValue(.cameraRoll)
- case .smartAlbumPanoramas:
- title = localLanguageTextValue(.panoramas)
- case .smartAlbumVideos:
- title = localLanguageTextValue(.videos)
- case .smartAlbumFavorites:
- title = localLanguageTextValue(.favorites)
- case .smartAlbumTimelapses:
- title = localLanguageTextValue(.timelapses)
- case .smartAlbumRecentlyAdded:
- title = localLanguageTextValue(.recentlyAdded)
- case .smartAlbumBursts:
- title = localLanguageTextValue(.bursts)
- case .smartAlbumSlomoVideos:
- title = localLanguageTextValue(.slomoVideos)
- case .smartAlbumSelfPortraits:
- title = localLanguageTextValue(.selfPortraits)
- case .smartAlbumScreenshots:
- title = localLanguageTextValue(.screenshots)
- case .smartAlbumDepthEffect:
- title = localLanguageTextValue(.depthEffect)
- case .smartAlbumLivePhotos:
- title = localLanguageTextValue(.livePhotos)
- default:
- title = collection.localizedTitle
- }
-
- if #available(iOS 11.0, *) {
- if collection.assetCollectionSubtype == PHAssetCollectionSubtype.smartAlbumAnimated {
- title = localLanguageTextValue(.animated)
- }
- }
- }
-
- return title ?? localLanguageTextValue(.noTitleAlbumListPlaceholder)
- }
-
- @discardableResult
- public class func fetchImage(for asset: PHAsset, size: CGSize, progress: ( (CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable : Any]?) -> Void )? = nil, completion: @escaping ( (UIImage?, Bool) -> Void )) -> PHImageRequestID {
- return self.fetchImage(for: asset, size: size, resizeMode: .fast, progress: progress, completion: completion)
- }
-
- @discardableResult
- class func fetchOriginalImage(for asset: PHAsset, progress: ( (CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable : Any]?) -> Void )? = nil, completion: @escaping ( (UIImage?, Bool) -> Void)) -> PHImageRequestID {
- return self.fetchImage(for: asset, size: PHImageManagerMaximumSize, resizeMode: .fast, progress: progress, completion: completion)
- }
-
- /// Fetch asset data.
- @discardableResult
- class func fetchOriginalImageData(for asset: PHAsset, progress: ( (CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable : Any]?) -> Void )? = nil, completion: @escaping ( (Data, [AnyHashable: Any]?, Bool) -> Void)) -> PHImageRequestID {
- let option = PHImageRequestOptions()
- if (asset.value(forKey: "filename") as? String)?.hasSuffix("GIF") == true {
- option.version = .original
- }
- option.isNetworkAccessAllowed = true
- option.resizeMode = .fast
- option.deliveryMode = .highQualityFormat
- option.progressHandler = { (pro, error, stop, info) in
- DispatchQueue.main.async {
- progress?(CGFloat(pro), error, stop, info)
- }
- }
-
- return PHImageManager.default().requestImageData(for: asset, options: option) { (data, _, _, info) in
- let cancel = info?[PHImageCancelledKey] as? Bool ?? false
- let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
- if !cancel, let data = data {
- completion(data, info, isDegraded)
- }
- }
- }
-
- /// Fetch image for asset.
- 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 {
- let option = PHImageRequestOptions()
- option.resizeMode = resizeMode
- option.isNetworkAccessAllowed = true
- option.progressHandler = { (pro, error, stop, info) in
- DispatchQueue.main.async {
- progress?(CGFloat(pro), error, stop, info)
- }
- }
-
- return PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: option) { (image, info) in
- var downloadFinished = false
- if let info = info {
- downloadFinished = !(info[PHImageCancelledKey] as? Bool ?? false) && (info[PHImageErrorKey] == nil)
- }
- let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
- if downloadFinished {
- completion(image, isDegraded)
- }
- }
- }
-
- class func fetchLivePhoto(for asset: PHAsset, completion: @escaping ( (PHLivePhoto?, [AnyHashable: Any]?, Bool) -> Void )) -> PHImageRequestID {
- let option = PHLivePhotoRequestOptions()
- option.version = .current
- option.deliveryMode = .opportunistic
- option.isNetworkAccessAllowed = true
-
- return PHImageManager.default().requestLivePhoto(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFit, options: option) { (livePhoto, info) in
- let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
- completion(livePhoto, info, isDegraded)
- }
- }
-
- class func fetchVideo(for asset: PHAsset, progress: ( (CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable : Any]?) -> Void )? = nil, completion: @escaping ( (AVPlayerItem?, [AnyHashable: Any]?, Bool) -> Void )) -> PHImageRequestID {
- let option = PHVideoRequestOptions()
- option.isNetworkAccessAllowed = true
- option.progressHandler = { (pro, error, stop, info) in
- DispatchQueue.main.async {
- progress?(CGFloat(pro), error, stop, info)
- }
- }
-
- // https://github.com/longitachi/ZLPhotoBrowser/issues/369#issuecomment-728679135
-
- if asset.isInCloud {
- return PHImageManager.default().requestExportSession(forVideo: asset, options: option, exportPreset: AVAssetExportPresetHighestQuality, resultHandler: { (session, info) in
- // iOS11 and earlier, callback is not on the main thread.
- DispatchQueue.main.async {
- let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
- if let avAsset = session?.asset {
- let item = AVPlayerItem(asset: avAsset)
- completion(item, info, isDegraded)
- }
- }
- })
- } else {
- return PHImageManager.default().requestPlayerItem(forVideo: asset, options: option) { (item, info) in
- // iOS11 and earlier, callback is not on the main thread.
- DispatchQueue.main.async {
- let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
- completion(item, info, isDegraded)
- }
- }
- }
- }
-
- class func isFetchImageError(_ error: Error?) -> Bool {
- guard let e = error as NSError? else {
- return false
- }
- if e.domain == "CKErrorDomain" || e.domain == "CloudPhotoLibraryErrorDomain" {
- return true
- }
- return false
- }
-
- @objc public class func fetchAVAsset(forVideo asset: PHAsset, completion: @escaping ( (AVAsset?, [AnyHashable: Any]?) -> Void )) -> PHImageRequestID {
- let options = PHVideoRequestOptions()
- options.version = .original
- options.deliveryMode = .automatic
- options.isNetworkAccessAllowed = true
-
- if asset.isInCloud {
- return PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { (session, info) in
- // iOS11 and earlier, callback is not on the main thread.
- DispatchQueue.main.async {
- if let avAsset = session?.asset {
- completion(avAsset, info)
- }
- }
- }
- } else {
- return PHImageManager.default().requestAVAsset(forVideo: asset, options: options) { (avAsset, _, info) in
- DispatchQueue.main.async {
- completion(avAsset, info)
- }
- }
- }
- }
-
- /// Fetch asset local file path.
- @objc public class func fetchAssetFilePath(asset: PHAsset, completion: @escaping (String?) -> Void ) {
- asset.requestContentEditingInput(with: nil) { (input, info) in
- var path = input?.fullSizeImageURL?.absoluteString
- if path == nil, let dir = asset.value(forKey: "directory") as? String, let name = asset.value(forKey: "filename") as? String {
- path = String(format: "file:///var/mobile/Media/%@/%@", dir, name)
- }
- completion(path)
- }
- }
- }
- /// Authority related.
- extension ZLPhotoManager {
-
- public class func hasPhotoLibratyAuthority() -> Bool {
- return PHPhotoLibrary.authorizationStatus() == .authorized
- }
-
- public class func hasCameraAuthority() -> Bool {
- let status = AVCaptureDevice.authorizationStatus(for: .video)
- if status == .restricted || status == .denied {
- return false
- }
- return true
- }
-
- public class func hasMicrophoneAuthority() -> Bool {
- let status = AVCaptureDevice.authorizationStatus(for: .audio)
- if status == .restricted || status == .denied {
- return false
- }
- return true
- }
-
- }
|