中文 | English
一款图片/视频选择器-支持LivePhoto、GIF选择、iCloud/网络资源在线下载、图片/视频编辑
目录
- 功能
- 要求
- 安装
- 示例
- 快速使用
- 如何支持GIF/网络图片
- 如何获取
- 更新记录
- 演示效果
- 界面展示
- 支持❤️
功能
- UI 外观支持浅色/深色/自动/自定义
- 支持多选/混合内容选择
-
支持的媒体类型:
- Photo
- GIF
- Live Photo
- Video
-
支持的本地资源类型:
- Photo
- Video
- GIF
- Live Photo
-
支持的网络资源类型:
- Photo
- Video
- 支持下载iCloud上的资源
- 支持手势返回
- 支持滑动选择
-
编辑图片(支持动图、网络资源)
- 涂鸦
- 贴纸
- 文字
- 裁剪
- 旋转任意角度
- 自定义蒙版
- 马赛克
- 画面调整
- 滤镜
-
编辑视频(支持网络资源)
- 涂鸦
- 贴纸(支持GIF)
- 文字
- 配乐(支持歌词字幕)
- 裁剪时长
- 裁剪尺寸
- 旋转任意角度
- 自定义蒙版
- 画面调整
- 滤镜
-
相册展现方式
- 单独列表
- 弹窗
-
多平台支持
- iOS
- iPadOS
- Mac Catalyst
-
国际化支持
- ?? 简体中文 (zh-Hans)
- ?? 繁体中文 (zh-Hant)
- ?? 英文 (en)
- ?? 日语 (ja)
- ?? 韩语 (ko)
- ?? 泰语 (th)
- ?? 印尼语 (id)
- ?? 越南语 (vi)
- ?? 俄罗斯 (ru)
- ?? 德国 (de)
- ?? 法国 (fr)
- ?? 阿拉伯 (ar)
- ✍️ 自定义语言 (custom)
- ? 更多支持… (欢迎PR)
要求
- iOS 10.0+
- Xcode 12.5+
- Swift 5.4+
安装
Swift Package Manager
️ 需要 Xcode 13.0 及以上版本来支持资源文件/本地化文件的添加。
dependencies: [ .package(url: \"https://gith*ub.*c*om/SilenceLove/HXPhotoPicker.git\", .upToNextMajor(from: \"5.0.3\")) ]
CocoaPods
将下面内容添加到 Podfile,并执行依赖更新。
/// iOS 10.0+ 默认不支持GIF和网络图片 pod \'HXPhotoPicker\' /// 使用`SwiftyGif`加载GIF图片 pod \'HXPhotoPicker/SwiftyGif\' /// 使用`SDWebImage`加载GIF/网络图片 pod \'HXPhotoPicker/SDWebImage\' /// 使用`Kingfisher v6.0.0`加载GIF/网络图片 pod \'HXPhotoPicker/Kingfisher\' /// 相机不包含定位功能 pod `HXPhotoPicker/NoLocation` /// 只有选择器 pod `HXPhotoPicker/Picker` /// 只有编辑器 pod `HXPhotoPicker/Editor` /// 只有相机 pod `HXPhotoPicker/Camera` /// 不包含定位功能 pod `HXPhotoPicker/Camera/Lite` v4.0以下的ObjC版本 pod \'HXPhotoPickerObjC\'
准备工作
按需在你的 Info.plist 中添加以下键值:
| Key | 模块 | 备注 |
|---|---|---|
| NSPhotoLibraryUsageDescription | Picker | 允许访问相册 |
| NSPhotoLibraryAddUsageDescription | Picker | 允许保存图片至相册 |
| PHPhotoLibraryPreventAutomaticLimitedAccessAlert | Picker | 设置为 YES iOS 14+ 以禁用自动弹出添加更多照片的弹框(Picker 已适配 Limited 功能,可由用户主动触发,提升用户体验) |
| NSCameraUsageDescription | Camera | 允许使用相机 |
| NSMicrophoneUsageDescription | Camera | 允许使用麦克风 |
快速上手
import HXPhotoPicker class ViewController: UIViewController { func presentPickerController() { // 设置与微信主题一致的配置 let config = PickerConfiguration.default // 方法一:async/await // 使用`Photo` let images: [UIImage] = try await Photo.picker(config) let urls: [URL] = try await Photo.picker(config) let urlResult: [AssetURLResult] = try await Photo.picker(config) let assetResult: [AssetResult] = try await Photo.picker(config) // 使用`PhotoPickerController` let images: [UIImage] = try await PhotoPickerController.picker(config) let urls: [URL] = try await PhotoPickerController.picker(config) let urlResult: [AssetURLResult] = try await PhotoPickerController.picker(config) let assetResult: [AssetResult] = try await PhotoPickerController.picker(config) let pickerResult = try await Photo.picker(config) let images: [UIImage] = try await pickerResult.objects() let urls: [URL] = try await pickerResult.objects() let urlResults: [AssetURLResult] = try await pickerResult.objects() let assetResults: [AssetResult] = try await pickerResult.objects() // 方法二: let pickerController = PhotoPickerController(picker: config) pickerController.pickerDelegate = self // 当前被选择的资源对应的 PhotoAsset 对象数组 pickerController.selectedAssetArray = selectedAssets // 是否选中原图 pickerController.isOriginal = isOriginal present(pickerController, animated: true, completion: nil) // 方法三: Photo.picker( config ) { result, pickerController in // 选择完成的回调 // result 选择结果 // .photoAssets 当前选择的数据 // .isOriginal 是否选中了原图 // photoPickerController 对应的照片选择控制器 } cancel: { pickerController in // 取消的回调 // photoPickerController 对应的照片选择控制器 } } } extension ViewController: PhotoPickerControllerDelegate { /// 选择完成之后调用 /// - Parameters: /// - pickerController: 对应的 PhotoPickerController /// - result: 选择的结果 /// result.photoAssets 选择的资源数组 /// result.isOriginal 是否选中原图 func pickerController( _ pickerController: PhotoPickerController, didFinishSelection result: PickerResult ) { // async/await let images: [UIImage] = try await result.objects() let urls: [URL] = try await result.objects() let urlResults: [AssetURLResult] = try await result.objects() let assetResults: [AssetResult] = try await result.objects() result.getImage { (image, photoAsset, index) in if let image = image { print(\"success\", image) }else { print(\"failed\") } } completionHandler: { (images) in print(images) } } /// 点击取消时调用 /// - Parameter pickerController: 对应的 PhotoPickerController func pickerController(didCancel pickerController: PhotoPickerController) { } }
如何支持GIF/网络图片 HXImageViewProtocol
SwiftyGif
PickerConfiguration.imageViewProtocol = GIFImageView.self public class GIFImageView: UIImageView, HXImageViewProtocol { public func setImageData(_ imageData: Data?) { guard let imageData else { clear() SwiftyGifManager.defaultManager.deleteImageView(self) image = nil return } if let image = try? UIImage(gifData: imageData) { setGifImage(image) }else { image = .init(data: imageData) } } public func _startAnimating() { startAnimatingGif() } public func _stopAnimating() { stopAnimatingGif() } }
SDWebImage
PickerConfiguration.imageViewProtocol = SDImageView.self public class SDImageView: SDAnimatedImageView, HXImageViewProtocol { public func setImageData(_ imageData: Data?) { guard let imageData else { return } let image = SDAnimatedImage(data: imageData) self.image = image } @discardableResult public func setImage(with resource: ImageDownloadResource, placeholder: UIImage?, options: ImageDownloadOptionsInfo?, progressHandler: ((CGFloat) -> Void)?, completionHandler: ((Result<UIImage, ImageDownloadError>) -> Void)?) -> ImageDownloadTask? { var sdOptions: SDWebImageOptions = [] var context: [SDWebImageContextOption: Any] = [:] if let options { for option in options { switch option { case .imageProcessor(let size): let imageProcessor = SDImageResizingTransformer(size: size, scaleMode: .aspectFill) context[.imageTransformer] = imageProcessor case .onlyLoadFirstFrame: sdOptions.insert(.decodeFirstFrameOnly) case .memoryCacheExpirationExpired: sdOptions.insert(.refreshCached) case .cacheOriginalImage, .fade, .scaleFactor: break } } } sd_setImage(with: resource.downloadURL, placeholderImage: placeholder, options: sdOptions, context: context) { receivedSize, totalSize, _ in let progress = CGFloat(receivedSize) / CGFloat(totalSize) DispatchQueue.main.async { progressHandler?(progress) } } completed: { image, error, cacheType, sourceURL in if let image { completionHandler?(.success(image)) }else { if let error = error as? NSError, error.code == NSURLErrorCancelled { completionHandler?(.failure(.cancel)) return } completionHandler?(.failure(.error(error))) } } let downloadTask = ImageDownloadTask { [weak self] in self?.sd_cancelCurrentImageLoad() } return downloadTask } @discardableResult public func setVideoCover(with url: URL, placeholder: UIImage?, completionHandler: ((Result<UIImage, ImageDownloadError>) -> Void)?) -> ImageDownloadTask? { let cacheKey = url.absoluteString if SDImageView.isCached(forKey: cacheKey) { SDImageCache.shared.queryImage(forKey: cacheKey, options: [], context: nil) { (image, data, _) in if let image { completionHandler?(.success(image)) }else { completionHandler?(.failure(.error(nil))) } } return nil } var imageGenerator: AVAssetImageGenerator? let avAsset = PhotoTools.getVideoThumbnailImage(url: url, atTime: 0.1) { imageGenerator = $0 } completion: { _, image, _ in guard let image else { completionHandler?(.failure(.error(nil))) return } SDImageCache.shared.store(image, imageData: nil, forKey: cacheKey, cacheType: .all) { DispatchQueue.main.async { completionHandler?(.success(image)) } } } let task = ImageDownloadTask { avAsset.cancelLoading() imageGenerator?.cancelAllCGImageGeneration() } return task } @discardableResult public static func download(with resource: ImageDownloadResource, options: ImageDownloadOptionsInfo?, progressHandler: ((CGFloat) -> Void)?, completionHandler: ((Result<ImageDownloadResult, ImageDownloadError>) -> Void)?) -> ImageDownloadTask? { var sdOptions: SDWebImageDownloaderOptions = [] var context: [SDWebImageContextOption: Any] = [:] if let options { for option in options { switch option { case .imageProcessor(let size): let imageProcessor = SDImageResizingTransformer(size: size, scaleMode: .aspectFill) context[.imageTransformer] = imageProcessor case .onlyLoadFirstFrame: sdOptions.insert(.decodeFirstFrameOnly) default: break } } } let key = resource.cacheKey if SDImageView.isCached(forKey: key) { SDImageCache.shared.queryImage(forKey: key, options
