Files
OrderScheduling/Pods/ZLPhotoBrowser/Sources/General/ZLPhotoManager.swift
DDIsFriend f0e8a1709d initial
2023-08-18 17:28:57 +08:00

463 lines
20 KiB
Swift

//
// 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
@objcMembers
public class ZLPhotoManager: NSObject {
/// Save image to album.
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?
let completionHandler: ((Bool, Error?) -> Void) = { suc, _ in
ZLMainAsync {
if suc {
let asset = self.getAsset(from: placeholderAsset?.localIdentifier)
completion?(suc, asset)
} else {
completion?(false, nil)
}
}
}
if image.zl.hasAlphaChannel(), let data = image.pngData() {
PHPhotoLibrary.shared().performChanges({
let newAssetRequest = PHAssetCreationRequest.forAsset()
newAssetRequest.addResource(with: .photo, data: data, options: nil)
placeholderAsset = newAssetRequest.placeholderForCreatedAsset
}, completionHandler: completionHandler)
} else {
PHPhotoLibrary.shared().performChanges({
let newAssetRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)
placeholderAsset = newAssetRequest.placeholderForCreatedAsset
}, completionHandler: completionHandler)
}
}
/// Save video to album.
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?
PHPhotoLibrary.shared().performChanges({
let newAssetRequest = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
placeholderAsset = newAssetRequest?.placeholderForCreatedAsset
}) { suc, _ in
ZLMainAsync {
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)
return result.firstObject
}
/// Fetch photos from result.
public 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, _, 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.
public 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) {
DispatchQueue.global().async {
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 {
stop.pointee = true
let result = PHAsset.fetchAssets(in: collection, options: option)
let albumModel = ZLAlbumListModel(title: self.getCollectionTitle(collection), result: result, collection: collection, option: option, isCameraRoll: true)
ZLMainAsync {
completion(albumModel)
}
}
}
}
}
/// Conversion collection title.
private class func getCollectionTitle(_ collection: PHAssetCollection) -> String {
if collection.assetCollectionType == .album {
// Albums created by user.
var title: String?
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?
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 fetchImage(for: asset, size: size, resizeMode: .fast, progress: progress, completion: completion)
}
@discardableResult
public class func fetchOriginalImage(for asset: PHAsset, progress: ((CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable: Any]?) -> Void)? = nil, completion: @escaping (UIImage?, Bool) -> Void) -> PHImageRequestID {
return fetchImage(for: asset, size: PHImageManagerMaximumSize, resizeMode: .fast, progress: progress, completion: completion)
}
/// Fetch asset data.
@discardableResult
public 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.zl.isGif {
option.version = .original
}
option.isNetworkAccessAllowed = true
option.resizeMode = .fast
option.deliveryMode = .highQualityFormat
option.progressHandler = { pro, error, stop, info in
ZLMainAsync {
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
ZLMainAsync {
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 {
ZLMainAsync {
completion(image, isDegraded)
}
}
}
}
public 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)
}
}
public 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
ZLMainAsync {
progress?(CGFloat(pro), error, stop, info)
}
}
// https://github.com/longitachi/ZLPhotoBrowser/issues/369#issuecomment-728679135
if asset.zl.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.
ZLMainAsync {
let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
if let avAsset = session?.asset {
let item = AVPlayerItem(asset: avAsset)
completion(item, info, isDegraded)
} else {
completion(nil, nil, true)
}
}
})
} else {
return PHImageManager.default().requestPlayerItem(forVideo: asset, options: option) { item, info in
// iOS11 and earlier, callback is not on the main thread.
ZLMainAsync {
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
}
public class func fetchAVAsset(forVideo asset: PHAsset, completion: @escaping (AVAsset?, [AnyHashable: Any]?) -> Void) -> PHImageRequestID {
let options = PHVideoRequestOptions()
options.deliveryMode = .automatic
options.isNetworkAccessAllowed = true
if asset.zl.isInCloud {
return PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { session, info in
// iOS11 and earlier, callback is not on the main thread.
ZLMainAsync {
if let avAsset = session?.asset {
completion(avAsset, info)
} else {
completion(nil, info)
}
}
}
} else {
return PHImageManager.default().requestAVAsset(forVideo: asset, options: options) { avAsset, _, info in
ZLMainAsync {
completion(avAsset, info)
}
}
}
}
/// Fetch the size of asset. Unit is KB.
public class func fetchAssetSize(for asset: PHAsset) -> ZLPhotoConfiguration.KBUnit? {
guard let resource = PHAssetResource.assetResources(for: asset).first,
let size = resource.value(forKey: "fileSize") as? CGFloat else {
return nil
}
return size / 1024
}
/// Fetch asset local file path.
/// - Note: Asynchronously to fetch the file path. calls completionHandler block on the main queue.
public class func fetchAssetFilePath(for asset: PHAsset, completion: @escaping (String?) -> Void) {
asset.requestContentEditingInput(with: nil) { input, _ in
var path = input?.fullSizeImageURL?.absoluteString
if path == nil,
let dir = asset.value(forKey: "directory") as? String,
let name = asset.zl.filename {
path = String(format: "file:///var/mobile/Media/%@/%@", dir, name)
}
completion(path)
}
}
/// Save asset original data to file url. Support save image and video.
/// - Note: Asynchronously write to a local file. Calls completionHandler block on the main queue.
public class func saveAsset(_ asset: PHAsset, toFile fileUrl: URL, completion: @escaping ((Error?) -> Void)) {
guard let resource = asset.zl.resource else {
completion(NSError.assetSaveError)
return
}
PHAssetResourceManager.default().writeData(for: resource, toFile: fileUrl, options: nil) { error in
ZLMainAsync {
completion(error)
}
}
}
}
/// Authority related.
public extension ZLPhotoManager {
class func hasPhotoLibratyAuthority() -> Bool {
return PHPhotoLibrary.authorizationStatus() == .authorized
}
class func hasCameraAuthority() -> Bool {
let status = AVCaptureDevice.authorizationStatus(for: .video)
if status == .restricted || status == .denied {
return false
}
return true
}
class func hasMicrophoneAuthority() -> Bool {
let status = AVCaptureDevice.authorizationStatus(for: .audio)
if status == .restricted || status == .denied {
return false
}
return true
}
}