A production-ready, privacy-first Swift library for taking photos with the camera or picking from the photo library. Handles all permission lifecycles, graceful degradation, iPad popover anchoring, and Settings deep-linking out of the box with zero third-party dependencies.
- ⚡️ Modern Concurrency — Full
async/awaitsupport and@MainActor/Sendableisolation for Swift 6 strict concurrency. - 🔄 Completion Handler Support — Backward-compatible callback APIs for existing projects.
- 🔒 Privacy-First — Uses
PHPickerViewController(iOS 14+) which runs out-of-process and requires zero photo library permissions. - 📱 Graceful Fallbacks — Automatic fallback to
UIImagePickerControlleron iOS 13, plus simulator detection for camera-less environments. - 🧭 Permission Lifecycle & Recovery — Handles
.notDetermined,.denied,.restricted, and.limitedstates with deep-linking to Settings. - 📲 iPad Popover Safe — Supports anchoring to
UIViewandUIBarButtonItemwithout interactive dismissal hangs. - 🌐 Fully Localizable — Customize or translate all action sheet and alert strings easily.
- 📦 Zero Third-Party Dependencies — Pure Swift built atop
UIKit,AVFoundation, andPhotosUI.
| Item | Requirement |
|---|---|
| iOS | 13.0+ |
| Swift | 5.9+ / 6.0 |
| Xcode | 15.0+ |
| Frameworks | UIKit, AVFoundation, Photos, PhotosUI |
In Xcode, select File → Add Package Dependencies... and enter the repository URL:
https://github.com/bhargavkukadiya/PhotoCaptureManager.git
Or add it to your Package.swift:
dependencies: [
.package(url: "https://github.com/bhargavkukadiya/PhotoCaptureManager.git", from: "1.0.0")
]Drag and drop Sources/PhotoCaptureManager/PhotoCaptureManager.swift into your Xcode project.
Add the required usage descriptions to your app's Info.plist:
<!-- Required for Camera capture -->
<key>NSCameraUsageDescription</key>
<string>We need camera access to capture profile photos.</string>
<!-- Required only if saving photos back to the user's library -->
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need permission to save photos to your photo library.</string>Note on
NSPhotoLibraryUsageDescription:NSPhotoLibraryUsageDescriptionis not required on iOS 14+ becausePHPickerViewControlleroperates out-of-process. Only include this key if your deployment target includes iOS 13.
import UIKit
import PhotoCaptureManager
@MainActor
final class ProfileViewController: UIViewController {
private let avatarImageView = UIImageView()
@IBAction func changePhotoTapped(_ sender: UIButton) {
Task {
let result = await PhotoCaptureManager.shared.presentPhotoOptions(
from: self,
anchor: .view(sender)
)
switch result {
case .success(let image):
self.avatarImageView.image = image
case .cancelled:
break // User dismissed the picker
case .failure(let error):
self.showErrorAlert(error.localizedDescription)
}
}
}
}PhotoCaptureManager.shared.presentPhotoOptions(
from: self,
anchor: .view(senderButton)
) { result in
switch result {
case .success(let image):
self.avatarImageView.image = image
case .cancelled:
break
case .failure(let error):
print("Capture failed: \(error.localizedDescription)")
}
}Skip the action sheet and open the camera directly:
// Async / Await
let result = await PhotoCaptureManager.shared.capturePhoto(from: self)
// Completion Handler
PhotoCaptureManager.shared.capturePhoto(from: self) { result in
// Handle result
}Open the photo picker directly:
// Async / Await
let result = await PhotoCaptureManager.shared.pickPhoto(from: self)
// Completion Handler
PhotoCaptureManager.shared.pickPhoto(from: self) { result in
// Handle result
}Avoid iPad popover crashes and misplacement by providing an anchor:
// Anchor to a UIView
let result = await PhotoCaptureManager.shared.presentPhotoOptions(
from: self,
anchor: .view(button, sourceRect: button.bounds)
)
// Anchor to a UIBarButtonItem
let result = await PhotoCaptureManager.shared.presentPhotoOptions(
from: self,
anchor: .barButtonItem(navigationItem.rightBarButtonItem!)
)Convert captured UIImage instances to compressed JPEG Data using the configured quality:
case .success(let image):
if let data = PhotoCaptureManager.shared.jpegData(from: image) {
uploadAvatar(data)
}Customize settings at app launch (e.g., in AppDelegate or @main App struct):
var config = PhotoCaptureConfiguration()
// Behavior & Editing
config.promptForPhotoLibraryIfCameraDenied = true
config.allowsEditing = false
config.preferredCameraDevice = .front
config.imageCompressionQuality = 0.85
config.actionTintColor = .systemIndigo
// Localization / Custom Text
config.strings.selectPhotoTitle = NSLocalizedString("select_photo", comment: "")
config.strings.takePhotoAction = NSLocalizedString("take_photo", comment: "")
config.strings.chooseFromLibraryAction = NSLocalizedString("choose_library", comment: "")
PhotoCaptureManager.shared.configuration = config| Property | Type | Default | Description |
|---|---|---|---|
promptForPhotoLibraryIfCameraDenied |
Bool |
true |
Offers photo library fallback when camera access is denied. |
allowsEditing |
Bool |
false |
Enables built-in cropping UI for camera & iOS 13 picker. |
preferredCameraDevice |
CameraDevice |
.rear |
Default camera device (.rear or .front). |
imageCompressionQuality |
CGFloat |
0.9 |
Compression quality for jpegData(from:). |
actionTintColor |
UIColor |
.systemBlue |
Tint color for alert and action sheet buttons. |
strings |
PhotoCaptureStrings |
(English) | Struct containing all localizable UI strings. |
PhotoCaptureError provides localized error descriptions:
switch error {
case .cameraUnavailable:
// Device has no camera (e.g. iOS Simulator)
case .permissionDenied(let source):
// Access denied for .camera or .photoLibrary
case .permissionRestricted:
// Device restricted by parental controls / MDM
case .imageExtractionFailed:
// Selected item could not be converted to UIImage
case .requestAlreadyInProgress:
// Another capture request is currently active
case .unknown(let error):
// Underlying system or item provider error
}PhotoCaptureManager is released under the MIT License. See LICENSE for details.