463 lines
20 KiB
Swift
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
|
|
}
|
|
}
|