TLPhotoPicker

June 3, 2026 ยท View on GitHub

Version License Platform Swift Sponsor

TLPhotoPicker

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.

Demo ๐Ÿ™‰

Facebook PickerTLPhotoPicker
Facebook PickerTLPhotoPicker

Features

  • โœ… 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 AlbumsLive PhotoVideoPhotoCustom Cell
Smart AlbumLivePhotoVideoPhotoCustom

Custom Camera Cell

Live Camera Cell
Camera Cell

Requirements

  • iOS 13.0+
  • Swift 5.0+
  • Xcode 14.0+

Installation

CocoaPods

platform :ios, '13.0'
pod "TLPhotoPicker"

Swift Package Manager

Add TLPhotoPicker as a dependency in your Package.swift:

dependencies: [
    .package(url: "https://github.com/tilltue/TLPhotoPicker.git", .upToNextMajor(from: "2.1.0"))
]

Privacy Configuration

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/>

Learn more

Quick Start

Basic Usage

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")")
        }
    }
}

Modern Async/Await (iOS 13+)

// 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)
    }
}

Configuration with Builder Pattern

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)

Documentation

For detailed information, see:

Common Use Cases

Single Photo Selection

picker.configure = .singlePhoto
    .selectedColor(.systemPurple)

Video Recording Only

picker.configure = TLPhotosPickerConfigure()
    .mediaType(.video)
    .allowPhotograph(false)
    .allowVideoRecording(true)

Instagram-style Grid

picker.configure = .compactGrid
    .maxSelection(10)
    .selectedColor(UIColor(red: 0/255, green: 122/255, blue: 255/255, alpha: 1.0))

Custom Selection Rules

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
}

Capture Camera Media URL

let picker = TLPhotosPickerViewController()
picker.didCaptureMediaURL = { url in
    // Move or copy this temporary file if you need to keep it.
}

present(picker, animated: true)

When set, camera captures are returned as temporary file URLs instead of being saved to the Photo Library.

Delegate Methods

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)
}

TLPHAsset

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.

Contributing

Issues and pull requests are welcome! Please check existing issues before creating new ones.

๐Ÿ’– Support This Project

TLPhotoPicker is an open-source project maintained in my free time. If you find it useful, please consider supporting its development:

GitHub Sponsors

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! ๐Ÿ™

Author

wade.hawk - junhyi.park@gmail.com

Does your organization use TLPhotoPicker? Let me know!

License

TLPhotoPicker is available under the MIT license. See the LICENSE file for details.