19

iOS PhotoKit - 浏览照片和视频

 3 years ago
source link: http://blog.danthought.com/programming/2020/06/28/ios-photokit-browse-photo-video/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

iOS 中的通过 PhotoKit 提供访问 “照片 App” 中的照片和视频,本文主要讲解使用 PhotoKit 浏览相册中的照片和视频、导出相册中的照片、导出相册中的视频、修改相册中的照片和新增相册中的照片。

Camera Sea

完整代码:

浏览相册中的照片和视频

第一步,获取相册使用权限:

PHPhotoLibrary.requestAuthorization { [weak self] status in
    guard let self = self else { return }
    switch status {
    case .authorized:
        self.imageManager = PHCachingImageManager()
        self.fetchAlbums()
        self.album = self.albums.first
        DispatchQueue.main.async { [weak self] in
            self?.updateAlbum()
            self?.tableView.reloadData()
        }
    default:
        DispatchQueue.main.async {
            DTMessageBar.error(message: "相册未授权", position: .bottom)
        }
    }
}

第二步,获取 Albums 和 Assets,先介绍一些核心类:

  • PHImageManager 是操作相册的中心类,为了缓存和提升访问性能,这里使用子类 PHCachingImageManager。
  • PHAssetCollection 是一组 Assets 的抽象,可以理解为相册。
  • PHAsset 是一个 Asset 的抽象,可以是照片、视频或 Live Photo。

获取 PHAssetCollection:

var collections = PHAssetCollection.fetchAssetCollections(with: .smartAlbum,
                                                          subtype: .albumRegular,
                                                          options: nil)
var index = 0
while index < collections.count {
    let collection = collections.object(at: index)
    var subtypes: [PHAssetCollectionSubtype] = [.smartAlbumRecentlyAdded]
    if #available(iOS 9.0, *) {
        if mode.type != .video {
            subtypes.append(.smartAlbumScreenshots)
        }
    }
    if mode.type != .photo {
        subtypes.append(.smartAlbumVideos)
    }
    if subtypes.contains(collection.assetCollectionSubtype) {
        assets = fetchAssets(in: collection)
        album = Album(collection: collection, assets: assets)
        albums.append(album)
    }
    index += 1
}

collections = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumRegular, options: nil)
index = 0
while index < collections.count {
    let collection = collections.object(at: index)
    assets = fetchAssets(in: collection)
    album = Album(collection: collection, assets: assets)
    albums.append(album)
    index += 1
}

获取 PHAsset,通过 NSPredicate 设置一些查询限定条件:

private func fetchAssets(in collection: PHAssetCollection? = nil) -> PHFetchResult<PHAsset> {
    if let collection = collection {
        let fetchOptions = PHFetchOptions()
        switch mode.type {
        case .photo:
            fetchOptions.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.image.rawValue)
        case .video:
            fetchOptions.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.video.rawValue)
        case .all:
            break
        }
        fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
        return PHAsset.fetchAssets(in: collection, options: fetchOptions)
    } else {
        let fetchOptions = PHFetchOptions()
        fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
        switch mode.type {
        case .photo:
            return PHAsset.fetchAssets(with: .image, options: fetchOptions)
        case .video:
            return PHAsset.fetchAssets(with: .video, options: fetchOptions)
        case .all:
            return PHAsset.fetchAssets(with: fetchOptions)
        }
    }
}

第三步,显示照片和视频的缩略图,PHAsset 包含的只是照片和视频的信息,缩略图要通过 PHImageManager 的 requestImage 方法异步获取:

func requestImage(for asset: PHAsset, targetSize: CGSize, contentMode: PHImageContentMode, options: PHImageRequestOptions?, resultHandler: @escaping (UIImage?, [AnyHashable : Any]?) -> Void) -> PHImageRequestID

PHImageRequestOptions 中的 PHImageRequestOptionsDeliveryMode 决定提供的照片质量和次数。

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    guard let assets = album?.assets else { return UICollectionViewCell() }
    let asset = assets.object(at: indexPath.row)
    if asset.mediaType == .image {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: photoCellIdentifier, for: indexPath) as! LibraryPhotoCell
        cell.photoImageView.image = nil
        cell.representedAssetIdentifier = asset.localIdentifier
        if let imageManager = imageManager {
            let requestOptions = PHImageRequestOptions()
            requestOptions.isNetworkAccessAllowed = true
            imageManager.requestImage(for: asset,
                                      targetSize: thumbnailSize,
                                      contentMode: .aspectFill,
                                      options: requestOptions,
                                      resultHandler: { photo, _ in
                                        DispatchQueue.main.async {
                                            if cell.representedAssetIdentifier == asset.localIdentifier {
                                                cell.photoImageView.image = photo
                                            }
                                        }
            })
        }
        let index = selectedAssets.firstIndex { $0.localIdentifier == asset.localIdentifier }
        if let index = index {
            cell.setCheckNumber(index + 1)
            cell.toggleMask(isShow: false)
        } else {
            cell.setCheckNumber(nil)
            cell.toggleMask(isShow: selectedAssets.count >= mode.config.limitOfPhotos)
        }
        return cell
    } else {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: videoCellIdentifier, for: indexPath) as! LibraryVideoCell
        cell.photoImageView.image = nil
        cell.representedAssetIdentifier = asset.localIdentifier
        if let imageManager = imageManager {
            let requestOptions = PHImageRequestOptions()
            requestOptions.isNetworkAccessAllowed = true
            imageManager.requestImage(for: asset,
                                      targetSize: thumbnailSize,
                                      contentMode: .aspectFill,
                                      options: requestOptions,
                                      resultHandler: { photo, _ in
                                        DispatchQueue.main.async {
                                            if cell.representedAssetIdentifier == asset.localIdentifier {
                                                cell.photoImageView.image = photo
                                            }
                                        }
            })
        }
        cell.setDuration(asset.duration)
        if asset.duration >= TimeInterval(mode.config.minDuration),
            asset.duration <= TimeInterval(mode.config.maxDuration) {
            cell.toggleMask(isShow: !selectedAssets.isEmpty)
        } else {
            cell.toggleMask(isShow: true)
        }
        return cell
    }
}

导出相册中的照片

和前面获取照片缩略图的代码一样,只是设置的照片质量不同:

private func fetchPhotos(completionBlock: @escaping (_ photos: [UIImage]?) -> Void) {
    guard let imageManager = imageManager else { return }
    let total = selectedAssets.count
    var current = 0
    var success = 0
    var photos = [UIImage](repeating: UIImage(), count: selectedAssets.count)
    let requestOptions = PHImageRequestOptions()
    requestOptions.deliveryMode = .highQualityFormat
    requestOptions.isNetworkAccessAllowed = true
    for (index, asset) in selectedAssets.enumerated() {
        imageManager.requestImage(for: asset,
                                  targetSize: PHImageManagerMaximumSize,
                                  contentMode: .aspectFit,
                                  options: requestOptions,
                                  resultHandler: { photo, _ in
                                    DispatchQueue.main.async {
                                        current += 1
                                        if let photo = photo {
                                            success += 1
                                            photos[index] = photo
                                        }
                                        if current == total {
                                            completionBlock(success == total ? photos : nil)
                                        }
                                    }
        })
    }
}

导出相册中的视频

PHImageManager 获取相册中的视频资源有如下几个方法:

  • requestPlayerItem 获取可以直接播放的 AVPlayerItem。
  • requestAVAsset 获取可以直接播放和编辑的 AVAsset。
  • requestExportSession 获取可以导出到目录文件的 AVAssetExportSession。

这里使用 requestExportSession 获取 AVAssetExportSession,PHImageRequestOptions 和 PHVideoRequestOptions 都有关于从 iCloud 获取照片和视频的功能,isNetworkAccessAllowed 允许获取从 iCloud 获取照片和视频,progressHandler 获取从 iCloud 下载照片和视频的进度,PHImageManager cancelImageRequest 可以取消整个获取照片和视频的异步任务:

asset.duration >= TimeInterval(mode.config.minDuration),
asset.duration <= TimeInterval(mode.config.maxDuration) {
DTMessageHUD.hud()
guard let videoFile = MediaViewController.getMediaFileURL(name: "video", ext: "mp4"),
    let imageManager = imageManager else {
        DTMessageHUD.dismiss()
        DTMessageBar.error(message: "创建视频文件失败", position: .bottom)
        return
}
let requestOptions = PHVideoRequestOptions()
requestOptions.isNetworkAccessAllowed = true
let presets = AVAssetExportSession.allExportPresets()
var preset = presets.first ?? ""
if presets.contains(AVAssetExportPreset1280x720) {
    preset = AVAssetExportPreset1280x720
} else if presets.contains(AVAssetExportPresetMediumQuality) {
    preset = AVAssetExportPresetMediumQuality
}
imageManager.requestExportSession(forVideo: asset, options: requestOptions,
                                  exportPreset: preset) { [weak self] sess, _ in
                                    self?.exportVideo(videoFile, with: sess)
}

接着,AVAssetExportSession 通过 exportAsynchronously 异步导出相册中视频到沙盒中,cancelExport 取消导出,status 和 progress 获取状态和进度:

private func exportVideo(_ video: URL, with session: AVAssetExportSession?) {
    guard let session = session else {
        DispatchQueue.main.async {
            DTMessageHUD.dismiss()
            DTMessageBar.error(message: "获取视频导出会话失败", position: .bottom)
        }
        return
    }
    session.outputURL = video
    session.outputFileType = .mp4
    session.shouldOptimizeForNetworkUse = true
    session.exportAsynchronously { [weak self] in
        DispatchQueue.main.async { [weak self] in
            switch session.status {
            case .completed:
                DTMessageHUD.dismiss()
                self?.previewVideo(video)
            default:
                DTMessageHUD.dismiss()
                DTMessageBar.error(message: "视频文件导出失败", position: .bottom)
            }
        }
    }
}

修改相册中的照片

if asset.canPerform(.content) {
    asset.requestContentEditingInput(with: nil) { (contentEditingInput, _) in
        do {
            guard let contentEditingInput = contentEditingInput else { return }
            let contentEditingOutput = PHContentEditingOutput(contentEditingInput: contentEditingInput)
            let formatIdentifier = Bundle.main.bundleIdentifier ?? ""
            let cropping = "cropping".data(using: .utf8, allowLossyConversion: false)!
            contentEditingOutput.adjustmentData = PHAdjustmentData(formatIdentifier: formatIdentifier,
                                                                   formatVersion: "0.1",
                                                                   data: cropping)
            try croppedPhotoJPEG.write(to: contentEditingOutput.renderedContentURL,
                                       options: .atomicWrite)
            PHPhotoLibrary.shared().performChanges({
                let request = PHAssetChangeRequest(for: self.asset)
                request.contentEditingOutput = contentEditingOutput
            }, completionHandler: { (success, error) in
                if success {
                    print("modify success")
                } else {
                    print("modify fail \(error?.localizedDescription ?? "")")
                }
            })
        } catch {
            print("modify error \(error)")
        }
    }
}

新增相册中的照片

PHPhotoLibrary.shared().performChanges({
    let request = PHAssetChangeRequest.creationRequestForAsset(from: croppedPhoto)
}) { (success, _) in
    if success {
        print("save success")
    } else {
        print("save fail")
    }
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK