A modern, flexible photo and video picker for iOS applications. TLPhotoPicker enables selecting media from multiple smart albums with an interface similar to Facebook's photo picker.
| Facebook Picker | TLPhotoPicker |
|---|---|
![]() |
![]() |
- β Smart Album Support - Camera roll, selfies, panoramas, favorites, videos, and custom albums
- π± Selection Order - Visual order indicators for selected media
βΆοΈ Media Playback - Preview videos and Live Photos directly in the picker- β±οΈ Video Duration - Display video length on thumbnails
- β‘ High Performance - Async asset loading with excellent scrolling performance
- π¨ Customizable - Custom cells, selection rules, and UI elements
- π Live Updates - Automatic reload when Photos library changes
- βοΈ iCloud Support - Seamless iCloud Photo Library integration
| Smart Albums | Live Photo | Video | Photo | Custom Cell |
|---|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
| Live Camera Cell |
|---|
![]() |
- iOS 13.0+
- Swift 5.0+
- Xcode 14.0+
platform :ios, '13.0'
pod "TLPhotoPicker"Add TLPhotoPicker as a dependency in your Package.swift:
dependencies: [
.package(url: "https://github.com/tilltue/TLPhotoPicker.git", .upToNextMajor(from: "2.1.0"))
]Add the following keys to your Info.plist:
<key>NSPhotoLibraryUsageDescription</key>
<string>Access to photos is required to select images</string>
<key>NSCameraUsageDescription</key>
<string>Camera access is required to take photos</string>iOS 14+ Limited Photo Access
To suppress automatic prompting, add this to
Info.plist:<key>PHPhotoLibraryPreventAutomaticLimitedAccessAlert</key> <true/>
import TLPhotoPicker
class ViewController: UIViewController {
@IBAction func openPhotoPicker() {
let picker = TLPhotosPickerViewController()
picker.delegate = self
present(picker, animated: true)
}
}
extension ViewController: TLPhotosPickerViewControllerDelegate {
func dismissPhotoPicker(withTLPHAssets: [TLPHAsset]) {
// Handle selected assets
for asset in withTLPHAssets {
print("Selected: \(asset.originalFileName ?? "Unknown")")
}
}
}// Load images asynchronously
Task {
if let image = await selectedAssets.first?.fullResolutionImage() {
await MainActor.run {
self.imageView.image = image
}
}
}
// Load multiple images concurrently
Task {
let images = await withTaskGroup(of: UIImage?.self) { group in
for asset in selectedAssets {
group.addTask { await asset.fullResolutionImage() }
}
var results: [UIImage] = []
for await image in group {
if let image = image { results.append(image) }
}
return results
}
await MainActor.run {
self.displayImages(images)
}
}let picker = TLPhotosPickerViewController()
// Use presets
picker.configure = .singlePhoto
picker.configure = .videoOnly
picker.configure = .compactGrid
// Or build custom configuration
picker.configure = TLPhotosPickerConfigure()
.numberOfColumns(3)
.maxSelection(20)
.allowVideo(true)
.allowLivePhotos(true)
.selectedColor(.systemPink)
.useCameraButton(true)
// Extend presets
picker.configure = .videoOnly
.numberOfColumns(4)
.selectedColor(.systemBlue)
present(picker, animated: true)For detailed information, see:
- Configuration Guide - Complete configuration options
- Advanced Usage - Custom cells, delegates, and rules
- API Reference - TLPHAsset and helper methods
- Migration Guide - Upgrading from older versions
picker.configure = .singlePhoto
.selectedColor(.systemPurple)picker.configure = TLPhotosPickerConfigure()
.mediaType(.video)
.allowPhotograph(false)
.allowVideoRecording(true)picker.configure = .compactGrid
.maxSelection(10)
.selectedColor(UIColor(red: 0/255, green: 122/255, blue: 255/255, alpha: 1.0))picker.canSelectAsset = { asset in
// Only allow images larger than 300x300
return asset.pixelWidth >= 300 && asset.pixelHeight >= 300
}
picker.didExceedMaximumNumberOfSelection = { picker in
// Show alert when limit reached
}protocol TLPhotosPickerViewControllerDelegate {
func shouldDismissPhotoPicker(withTLPHAssets: [TLPHAsset]) -> Bool
func dismissPhotoPicker(withTLPHAssets: [TLPHAsset])
func dismissPhotoPicker(withPHAssets: [PHAsset])
func photoPickerDidCancel()
func dismissComplete()
func canSelectAsset(phAsset: PHAsset) -> Bool
func didExceedMaximumNumberOfSelection(picker: TLPhotosPickerViewController)
func handleNoAlbumPermissions(picker: TLPhotosPickerViewController)
func handleNoCameraPermissions(picker: TLPhotosPickerViewController)
}The library provides TLPHAsset, a wrapper around PHAsset with convenient helper methods:
public struct TLPHAsset {
public var phAsset: PHAsset?
public var selectedOrder: Int
public var type: AssetType // .photo, .video, .livePhoto
public var originalFileName: String?
public var isSelectedFromCamera: Bool
// Async image loading
public func fullResolutionImage() async -> UIImage?
// iCloud download
public func cloudImageDownload(
progressBlock: @escaping (Double) -> Void,
completionBlock: @escaping (UIImage?) -> Void
) -> PHImageRequestID?
// Export to file
public func tempCopyMediaFile(
convertLivePhotosToJPG: Bool = false,
progressBlock: ((Double) -> Void)? = nil,
completionBlock: @escaping ((URL, String) -> Void)
) -> PHImageRequestID?
// File size
public func photoSize(completion: @escaping (Int) -> Void)
public func videoSize(completion: @escaping (Int) -> Void)
// Static method
public static func asset(with localIdentifier: String) -> TLPHAsset?
}See API Reference for complete documentation.
Issues and pull requests are welcome! Please check existing issues before creating new ones.
TLPhotoPicker is an open-source project maintained in my free time. If you find it useful, please consider supporting its development:
Your support helps me:
- π Fix bugs and maintain compatibility with latest iOS versions
- β¨ Develop new features and improvements
- π Improve documentation and examples
- β‘ Performance optimizations and code quality
Every contribution is appreciated! π
wade.hawk - [email protected]
Does your organization use TLPhotoPicker? Let me know!
TLPhotoPicker is available under the MIT license. See the LICENSE file for details.









