中文 | English
photo/video selector-supports LivePhoto, GIF selection, iCloud resource online download, photo/video editing
- UI Appearance supports light/dark/auto/custom
- Support multiple selection/mixed content selection
- Supported media types:
- Photo
- GIF
- Live Photo
- Video
- Supported local media types:
- Photo
- Video
- GIF
- Live Photo
- Supported network media types:
- Photo
- Video
- Support downloading assets on iCloud
- Support gesture back
- Support sliding selection
- Edit pictures (support animated pictures, network pictures)
- Graffiti
- Sticker
- Text
- Crop
- Mosaic
- Filter
- Edit video (support network video)
- Graffiti
- Stickers (support GIF)
- Text
- Soundtrack (support lyrics and subtitles)
- Crop duration
- Crop Size
- Filter
- Album display mode
- Separate list
- Pop-ups
- Multi-platform support
- iOS
- iPadOS
- Mac Catalyst
- Internationalization support
- 🇨🇳 Chinese, Simplified (zh-Hans)
- 🇬🇧 English (en)
- 🇨🇳 Chinese, traditional (zh-Hant)
- 🇯🇵 Japanese (ja)
- 🇰🇷 Korean (ko)
- 🇹🇭 Thai (th)
- 🇮🇳 Indonesian (id)
- 🇻🇳 Vietnamese (vi)
- 🇷🇺 russian (ru)
- 🇩🇪 german (de)
- 🇫🇷 french (fr)
- 🇸🇦 arabic (ar)
- ✍️ Custom language (custom)
- 🤝 More support... (Pull requests welcome)
- iOS 10.0+
- Xcode 12.5+
- Swift 5.4+
dependencies: [
.package(url: "https://github.com/SilenceLove/HXPhotoPicker.git", .upToNextMajor(from: "5.0.0"))
]
Add this to Podfile, and then update dependency:
/// iOS 10.0+ GIF and network images are not supported by default
pod 'HXPhotoPicker'
/// Use `SwiftyGif` to display GIF images
pod 'HXPhotoPicker/SwiftyGif'
/// Use `SDWebImage` to display network images
pod 'HXPhotoPicker/SDWebImage'
/// Displaying network images using `Kingfisher v6.0.0`
pod 'HXPhotoPicker/Kingfisher'
/// Only Picker
pod `HXPhotoPicker/Picker`
/// Only Editor
pod `HXPhotoPicker/Editor`
/// Only Camera
pod `HXPhotoPicker/Camera`
/// Does not include location functionality
pod `HXPhotoPicker/Camera/Lite`
Add these keys to your Info.plist when needed:
Key | Module | Info |
---|---|---|
NSPhotoLibraryUsageDescription | Picker | Allow access to album |
NSPhotoLibraryAddUsageDescription | Picker | Allow to save pictures to album |
PHPhotoLibraryPreventAutomaticLimitedAccessAlert | Picker | Set YES to prevent automatic limited access alert in iOS 14+ (Picker has been adapted with Limited features that can be triggered by the user to enhance the user experience) |
NSCameraUsageDescription | Camera | Allow camera |
NSMicrophoneUsageDescription | Camera | Allow microphone |
import HXPhotoPicker
class ViewController: UIViewController {
func presentPickerController() {
let config = PickerConfiguration()
// Method 1:async/await
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)
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()
// Method 2:
let pickerController = PhotoPickerController(picker: config)
pickerController.pickerDelegate = self
// The array of PhotoAsset objects corresponding to the currently selected asset
pickerController.selectedAssetArray = selectedAssets
// Whether to select the original image
pickerController.isOriginal = isOriginal
present(pickerController, animated: true, completion: nil)
// Method 3:
Photo.picker(
config
) { result, pickerController in
// Select completion callback
// result Select result
// .photoAssets Currently selected data
// .isOriginal Whether the original image is selected
// photoPickerController Corresponding photo selection controller
} cancel: { pickerController in
// Cancelled callback
// photoPickerController Corresponding photo selection controller
}
}
}
extension ViewController: PhotoPickerControllerDelegate {
/// Called after the selection is complete
/// - Parameters:
/// - pickerController: corresponding PhotoPickerController
/// - result: Selected result
/// result.photoAssets Selected asset array
/// result.isOriginal Whether to select the original image
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)
}
}
/// Called when cancel is clicked
/// - Parameter pickerController: Corresponding PhotoPickerController
func pickerController(didCancel pickerController: PhotoPickerController) {
}
}
How to support GIF/network images 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: [], context: nil) { (image, data, _) in
if let data = data {
completionHandler?(.success(.init(imageData: data)))
} else if let image = image as? SDAnimatedImage, let data = image.animatedImageData {
completionHandler?(.success(.init(imageData: data)))
} else if let image {
completionHandler?(.success(.init(image: image)))
} else {
completionHandler?(.failure(.error(nil)))
}
}
return nil
}
let operation = SDWebImageDownloader.shared.downloadImage(
with: resource.downloadURL,
options: sdOptions,
context: context,
progress: { receivedSize, totalSize, _ in
let progress = CGFloat(receivedSize) / CGFloat(totalSize)
DispatchQueue.main.async {
progressHandler?(progress)
}
},
completed: { image, data, error, finished in
guard let data = data, finished, error == nil else {
completionHandler?(.failure(.error(error)))
return
}
DispatchQueue.global().async {
let format = NSData.sd_imageFormat(forImageData: data)
if format == SDImageFormat.GIF, let gifImage = SDAnimatedImage(data: data) {
SDImageCache.shared.store(gifImage, imageData: data, forKey: key, options: [], context: nil, cacheType: .all) {
DispatchQueue.main.async {
completionHandler?(.success(.init(imageData: data)))
}
}
return
}
if let image = image {
SDImageCache.shared.store(image, imageData: data, forKey: key, options: [], context: nil, cacheType: .all) {
DispatchQueue.main.async {
completionHandler?(.success(.init(image: image)))
}
}
}
}
}
)
let downloadTask = ImageDownloadTask {
operation?.cancel()
}
return downloadTask
}
public func _startAnimating() {
startAnimating()
}
public func _stopAnimating() {
stopAnimating()
}
public static func getCacheKey(forURL url: URL) -> String {
SDWebImageManager.shared.cacheKey(for: url) ?? ""
}
public static func getCachePath(forKey key: String) -> String {
SDImageCache.shared.cachePath(forKey: key) ?? ""
}
public static func isCached(forKey key: String) -> Bool {
FileManager.default.fileExists(atPath: getCachePath(forKey: key))
}
public static func getInMemoryCacheImage(forKey key: String) -> UIImage? {
SDImageCache.shared.imageFromMemoryCache(forKey: key)
}
public static func getCacheImage(forKey key: String, completionHandler: ((UIImage?) -> Void)?) {
SDImageCache.shared.queryImage(forKey: key, context: nil, cacheType: .all) { image, data, _ in
if let data, let image = SDAnimatedImage(data: data) {
completionHandler?(image)
}else if let image {
completionHandler?(image)
}else {
completionHandler?(nil)
}
}
}
}
Kingfisher(v6.0.0)
PickerConfiguration.imageViewProtocol = KFImageView.self
public class KFImageView: AnimatedImageView, HXImageViewProtocol {
public func setImageData(_ imageData: Data?) {
guard let imageData else { return }
let image: KFCrossPlatformImage? = DefaultImageProcessor.default.process(item: .data(imageData), options: .init([]))
self.image = image
}
@discardableResult
public func setImage(with resource: ImageDownloadResource, placeholder: UIImage?, options: ImageDownloadOptionsInfo?, progressHandler: ((CGFloat) -> Void)?, completionHandler: ((Result<UIImage, ImageDownloadError>) -> Void)?) -> ImageDownloadTask? {
var kfOptions: KingfisherOptionsInfo = []
if let options {
for option in options {
switch option {
case .fade(let duration):
kfOptions += [.transition(.fade(duration))]
case .imageProcessor(let size):
let imageProcessor = DownsamplingImageProcessor(size: size)
kfOptions += [.processor(imageProcessor)]
case .onlyLoadFirstFrame:
kfOptions += [.onlyLoadFirstFrame]
case .cacheOriginalImage:
kfOptions += [.cacheOriginalImage]
case .memoryCacheExpirationExpired:
kfOptions += [.memoryCacheExpiration(.expired)]
case .scaleFactor(let scale):
kfOptions += [.scaleFactor(scale)]
}
}
}
let imageResource = Kingfisher.ImageResource(downloadURL: resource.downloadURL, cacheKey: resource.cacheKey)
if let indicatorColor = resource.indicatorColor {
kf.indicatorType = .activity
(kf.indicator?.view as? UIActivityIndicatorView)?.color = indicatorColor
}
let task = kf.setImage(with: imageResource, placeholder: placeholder, options: kfOptions) { receivedSize, totalSize in
progressHandler?(CGFloat(receivedSize) / CGFloat(totalSize))
} completionHandler: {
switch $0 {
case .success(let result):
completionHandler?(.success(result.image))
case .failure(let error):
completionHandler?(.failure(error.isTaskCancelled ? .cancel : .error(error)))
}
}
let downloadTask = ImageDownloadTask {
task?.cancel()
}
return downloadTask
}
public func setVideoCover(with url: URL, placeholder: UIImage?, completionHandler: ((Result<UIImage, ImageDownloadError>) -> Void)?) -> ImageDownloadTask? {
let provider = AVAssetImageDataProvider(assetURL: url, seconds: 0.1)
provider.assetImageGenerator.appliesPreferredTrackTransform = true
let task = KF.dataProvider(provider)
.placeholder(placeholder)
.onSuccess { result in
completionHandler?(.success(result.image))
}
.onFailure { error in
completionHandler?(.failure(error.isTaskCancelled ? .cancel : .error(error)))
}
.set(to: self)
let downloadTask = ImageDownloadTask {
task?.cancel()
}
return downloadTask
}
@discardableResult
public static func download(with resource: ImageDownloadResource, options: ImageDownloadOptionsInfo?, progressHandler: ((CGFloat) -> Void)?, completionHandler: ((Result<ImageDownloadResult, ImageDownloadError>) -> Void)?) -> ImageDownloadTask? {
let key = resource.cacheKey
var kfOptions: KingfisherOptionsInfo = []
if let options {
for option in options {
switch option {
case .fade(let duration):
kfOptions += [.transition(.fade(duration))]
case .imageProcessor(let size):
let imageProcessor = DownsamplingImageProcessor(size: size)
kfOptions += [.processor(imageProcessor)]
case .onlyLoadFirstFrame:
kfOptions += [.onlyLoadFirstFrame]
case .cacheOriginalImage:
kfOptions += [.cacheOriginalImage]
case .memoryCacheExpirationExpired:
kfOptions += [.memoryCacheExpiration(.expired)]
case .scaleFactor(let scale):
kfOptions += [.scaleFactor(scale)]
}
}
}
if ImageCache.default.isCached(forKey: key) {
ImageCache.default.retrieveImage(
forKey: key,
options: kfOptions
) { (result) in
switch result {
case .success(let value):
if let data = value.image?.kf.gifRepresentation() {
completionHandler?(.success(.init(imageData: data)))
}else if let image = value.image {
completionHandler?(.success(.init(image: image)))
}else {
completionHandler?(.failure(.error(nil)))
}
case .failure(let error):
completionHandler?(.failure(.error(error)))
}
}
return nil
}
let task = ImageDownloader.default.downloadImage(with: resource.downloadURL, options: kfOptions) { receivedSize, totalSize in
let progress = CGFloat(receivedSize) / CGFloat(totalSize)
progressHandler?(progress)
} completionHandler: {
switch $0 {
case .success(let value):
DispatchQueue.global().async {
if let gifImage = DefaultImageProcessor.default.process(
item: .data(value.originalData),
options: .init([])
) {
ImageCache.default.store(
gifImage,
original: value.originalData,
forKey: key
)
DispatchQueue.main.async {
completionHandler?(.success(.init( imageData: value.originalData)))
}
return
}
ImageCache.default.store(
value.image,
original: value.originalData,
forKey: key
)
DispatchQueue.main.async {
completionHandler?(.success(.init(image: value.image)))
}
}
case .failure(let error):
completionHandler?(.failure(.error(error)))
}
}
let downloadTask = ImageDownloadTask {
task?.cancel()
}
return downloadTask
}
public func _startAnimating() {
startAnimating()
}
public func _stopAnimating() {
stopAnimating()
}
public static func getCacheKey(forURL url: URL) -> String {
url.cacheKey
}
public static func getCachePath(forKey key: String) -> String {
ImageCache.default.cachePath(forKey: key)
}
public static func isCached(forKey key: String) -> Bool {
ImageCache.default.isCached(forKey: key)
}
public static func getInMemoryCacheImage(forKey key: String) -> UIImage? {
ImageCache.default.retrieveImageInMemoryCache(forKey: key)
}
public static func getCacheImage(forKey key: String, completionHandler: ((UIImage?) -> Void)?) {
ImageCache.default.retrieveImage(forKey: key, options: []) {
switch $0 {
case .success(let result):
completionHandler?(result.image)
case .failure:
completionHandler?(nil)
}
}
}
}
/// If it is a video, get the cover of the video
// async/await
// compression: if not passed, no compression
let image: UIImage = try await photoAsset.object(compression)
/// Get the `UIImage` of the specified `Size`
/// targetSize: specify imageSize
/// targetMode: crop mode
let image = try await photoAsset.image(targetSize: .init(width: 200, height: 200), targetMode: .fill)
// compressionQuality: Compress parameters, if not passed, no compression
photoAsset.getImage(compressionQuality: compressionQuality) { image in
print(image)
}
// async/await
// compression: if not passed, no compression
let url: URL = try await photoAsset.object(compression)
let urlResult: AssetURLResult = try await photoAsset.object(compression)
let assetResult: AssetResult = try await photoAsset.object(compression)
/// compression: Compress parameters, if not passed, no compression
photoAsset.getURL(compression: compression) { result in
switch result {
case .success(let urlResult):
switch urlResult.mediaType {
case .photo:
case .video:
}
switch urlResult.urlType {
case .local:
case .network:
}
print(urlResult.url)
// Image and video urls contained in LivePhoto
print(urlResult.livePhoto)
case .failure(let error):
print(error)
}
}
/// Get thumbnail
let thumImage = try await photoAsset.requesThumbnailImage()
/// Get preview
let previewImage = try await photoAsset.requestPreviewImage()
/// Get AVAsset
let avAsset = try await photoAsset.requestAVAsset()
/// Get AVPlayerItem
let playerItem = try await photoAsset.requestPlayerItem()
/// Get PHLivePhoto
let livePhoto = try await photoAsset.requestLivePhoto()
Latest updates
Version | Release Date | Xcode | Swift | iOS |
---|---|---|---|---|
v5.0.0 | 2025-03-03 | 16.0.0 | 6.0.0 | 10.0+ |
History record
Version | Release Date | Xcode | Swift | iOS |
---|---|---|---|---|
v4.2.5 | 2025-02-12 | 16.0.0 | 6.0.0 | 13.0+ |
v4.2.4 | 2024-12-14 | 16.0.0 | 6.0.0 | 13.0+ |
v4.2.3 | 2024-08-05 | 15.0.0 | 5.9.0 | 12.0+ |
v4.2.2 | 2024-07-08 | 15.0.0 | 5.9.0 | 12.0+ |
v4.2.1 | 2024-05-18 | 15.0.0 | 5.9.0 | 12.0+ |
v4.2.0 | 2024-04-23 | 15.0.0 | 5.9.0 | 12.0+ |
v4.1.9 | 2024-04-09 | 15.0.0 | 5.9.0 | 12.0+ |
v4.1.8 | 2024-03-24 | 15.0.0 | 5.9.0 | 12.0+ |
v4.1.7 | 2024-03-09 | 15.0.0 | 5.9.0 | 12.0+ |
v4.1.6 | 2024-02-16 | 15.0.0 | 5.9.0 | 12.0+ |
v4.1.5 | 2024-01-10 | 15.0.0 | 5.9.0 | 12.0+ |
v4.1.4 | 2023-12-24 | 15.0.0 | 5.9.0 | 12.0+ |
v4.1.3 | 2023-12-16 | 15.0.0 | 5.9.0 | 12.0+ |
v4.1.2 | 2023-12-02 | 15.0.0 | 5.9.0 | 12.0+ |
v4.1.1 | 2023-11-14 | 15.0.0 | 5.9.0 | 12.0+ |
v4.1.0 | 2023-11-07 | 15.0.0 | 5.9.0 | 12.0+ |
v4.0.9 | 2023-10-22 | 15.0.0 | 5.9.0 | 12.0+ |
v4.0.8 | 2023-10-13 | 15.0.0 | 5.9.0 | 12.0+ |
v4.0.7 | 2023-09-23 | 14.3.0 | 5.7.0 | 12.0+ |
v4.0.6 | 2023-09-09 | 14.3.0 | 5.7.0 | 12.0+ |
v4.0.5 | 2023-08-12 | 14.3.0 | 5.7.0 | 12.0+ |
v4.0.4 | 2023-07-30 | 14.3.0 | 5.7.0 | 12.0+ |
v4.0.3 | 2023-07-06 | 14.3.0 | 5.7.0 | 12.0+ |
v4.0.2 | 2023-06-24 | 14.3.0 | 5.7.0 | 12.0+ |
v4.0.1 | 2023-06-17 | 14.3.0 | 5.7.0 | 12.0+ |
v4.0.0 | 2023-06-15 | 14.3.0 | 5.7.0 | 12.0+ |
Choose a photo | Picture editing | Video editing |
---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
---|
HXPhotoPicker is released under the MIT license. See LICENSE for details.
- ★ Star this repo.
- Support with
![]() |
![]() |
![]() |
---|