Building a Camera Feature with AVFoundation on iOS
Modern iOS applications often require more than just static content; many leverage the device's powerful camera for features like scanning QR codes, taking profile pictures, or creating augmented reality experiences. While UIImagePickerController offers a quick way to access the camera, it provides limited customization. For full control over the camera experience, from live preview to advanced capture settings, AVFoundation is the framework you need.
AVFoundation is Apple's comprehensive framework for working with time-based audiovisual media. It's powerful, but can also be intimidating due to its granular control. In this article, we'll demystify AVFoundation by walking through the essential steps to build a basic custom camera feature in your iOS app, capable of displaying a live preview and capturing photos.
We'll cover: 1. Requesting camera permissions. 2. Setting up an AVCaptureSession. 3. Displaying a live camera preview. 4. Capturing still photos. 5. Switching between front and back cameras. 6. Managing the session lifecycle.
Let's dive in!
1. Requesting Camera Permissions
Before your app can access the camera, you must request user permission. This involves two steps: 1. Adding a usage description to your Info.plist. 2. Programmatically requesting access.
First, open your Info.plist file (or your target's Info tab) and add the Privacy - Camera Usage Description key (or NSCameraUsageDescription in source code view) with a descriptive string explaining why your app needs camera access. For example: "This app needs camera access to take photos."
Next, you'll request access at runtime. It's good practice to do this before attempting to use the camera.
import AVFoundation
import UIKit
class CameraViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
checkCameraPermissions()
}
private func checkCameraPermissions() {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
// The user has previously granted access to the camera.
setupCameraSession()
case .notDetermined:
// The user has not yet been asked for camera access.
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
DispatchQueue.main.async {
if granted {
self?.setupCameraSession()
} else {
self?.handlePermissionDenied()
}
}
}
case .denied, .restricted:
// The user has previously denied or restricted camera access.
handlePermissionDenied()
@unknown default:
fatalError("Unknown authorization status for camera.")
}
}
private func handlePermissionDenied() {
let alert = UIAlertController(
title: "Camera Access Denied",
message: "Please enable camera access for this app in Settings to use this feature.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
// ... rest of the camera setup
}
2. Setting Up the Capture Session
The heart of AVFoundation's camera functionality is AVCaptureSession. It coordinates the flow of data from inputs (like the camera) to outputs (like photo or video recorders).
We'll need: An AVCaptureSession instance. An AVCaptureDeviceInput to feed camera data into the session. * An AVCapturePhotoOutput to capture still images.
It's crucial to perform all AVCaptureSession configuration on a dedicated serial DispatchQueue to avoid blocking the main thread and ensure thread safety.
class CameraViewController: UIViewController {
private var captureSession: AVCaptureSession?
private var photoOutput: AVCapturePhotoOutput?
private var previewLayer: AVCaptureVideoPreviewLayer?
private let sessionQueue = DispatchQueue(label: "session queue")
// ... (checkCameraPermissions and handlePermissionDenied methods)
private func setupCameraSession() {
captureSession = AVCaptureSession()
sessionQueue.async { [weak self] in
guard let self = self, let captureSession = self.captureSession else { return }
captureSession.beginConfiguration()
defer { captureSession.commitConfiguration() } // Ensure commitConfiguration is called
// 1. Add input (camera device)
guard let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
print("Failed to get video device.")
return
}
guard let videoInput = try? AVCaptureDeviceInput(device: videoDevice) else {
print("Failed to create video input.")
return
}
if captureSession.canAddInput(videoInput) {
captureSession.addInput(videoInput)
} else {
print("Could not add video input to the session.")
return
}
// Store the current input for camera switching
self.currentVideoInput = videoInput
// 2. Add output (photo output)
self.photoOutput = AVCapturePhotoOutput()
self.photoOutput?.isHighResolutionCaptureEnabled = true // Enable high resolution photos
if captureSession.canAddOutput(self.photoOutput!) {
captureSession.addOutput(self.photoOutput!)
} else {
print("Could not add photo output to the session.")
return
}
// 3. Setup preview layer (on main thread as it's UI)
DispatchQueue.main.async {
self.setupPreviewLayer(session: captureSession)
}
}
}
}
3. Displaying the Camera Preview
To show the live camera feed, you'll use an AVCaptureVideoPreviewLayer. This is a CALayer subclass that displays video from a capture session. You'll add it as a sublayer to one of your UIView's layers.
class CameraViewController: UIViewController {
// ... (previous properties and methods)
private func setupPreviewLayer(session: AVCaptureSession) {
previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer?.videoGravity = .resizeAspectFill // Fills the layer bounds, cropping if necessary
previewLayer?.connection?.videoOrientation = .portrait // Ensure correct orientation
// Add the preview layer to your view's layer hierarchy
if let previewLayer = previewLayer {
view.layer.insertSublayer(previewLayer, at: 0) // Add it at index 0 so other UI elements are on top
previewLayer.frame = view.bounds // Match the view's bounds
}
// Start the session (important!)
sessionQueue.async { [weak self] in
self?.captureSession?.startRunning()
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
previewLayer?.frame = view.bounds // Update frame on rotation/layout changes
}
}
4. Capturing a Photo
With the session running and preview visible, capturing a photo is straightforward using AVCapturePhotoOutput. You'll configure AVCapturePhotoSettings and then call capturePhoto(with:delegate:). You'll also need to conform to AVCapturePhotoCaptureDelegate to receive the captured photo data.
import Photos // For saving to photo library
class CameraViewController: UIViewController, AVCapturePhotoCaptureDelegate {
// ... (previous properties and methods)
private var currentVideoInput: AVCaptureDeviceInput? // Keep track of the current camera input
// Call this method when a "capture" button is tapped
@IBAction func captureButtonTapped(_ sender: UIButton) {
sessionQueue.async { [weak self] in
guard let self = self, let photoOutput = self.photoOutput else { return }
let photoSettings = AVCapturePhotoSettings()
photoSettings.flashMode = .off // Or .on, .auto
// If you want high resolution, ensure `isHighResolutionCaptureEnabled` is true on photoOutput
if photoOutput.isHighResolutionCaptureEnabled {
photoSettings.isHighResolutionPhotoEnabled = true
}
// Capture the photo using the delegate
photoOutput.capturePhoto(with: photoSettings, delegate: self)
}
}
// MARK: - AVCapturePhotoCaptureDelegate
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
if let error = error {
print("Error capturing photo: \(error.localizedDescription)")
return
}
guard let imageData = photo.fileDataRepresentation() else {
print("Could not get image data from photo.")
return
}
// Process the image data, e.g., save it to the photo library or display it
UIImage(data: imageData).map { capturedImage in
DispatchQueue.main.async {
// For demonstration, let's just print the size and save to Photos
print("Captured image size: \(capturedImage.size)")
// Request permission to save to photo library
PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in
if status == .authorized {
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAsset(from: capturedImage)
}) { success, error in
if let error = error {
print("Error saving photo to library: \(error.localizedDescription)")
} else {
print("Photo saved successfully!")
}
}
} else {
print("Photo library access denied.")
}
}
}
}
}
}
5. Switching Cameras (Front/Back)
Switching between front and back cameras involves finding the desired device, removing the current input, and adding the new input to the AVCaptureSession. Remember to perform these operations within beginConfiguration() and commitConfiguration() blocks on your sessionQueue.
class CameraViewController: UIViewController {
// ... (previous properties and methods)
@IBAction func switchCameraButtonTapped(_ sender: UIButton) {
sessionQueue.async { [weak self] in
guard let self = self, let captureSession = self.captureSession,
let currentInput = self.currentVideoInput else { return }
captureSession.beginConfiguration()
defer { captureSession.commitConfiguration() }
// Determine the current camera position and find the opposite
let currentPosition = currentInput.device.position
let newPosition: AVCaptureDevice.Position = (currentPosition == .back) ? .front : .back
// Find the new camera device
guard let newVideoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: newPosition) else {
print("Could not find camera for position: \(newPosition.rawValue)")
return
}
// Create new input
guard let newVideoInput = try? AVCaptureDeviceInput(device: newVideoDevice) else {
print("Could not create new video input.")
return
}
// Remove the current input and add the new one
captureSession.removeInput(currentInput)
if captureSession.canAddInput(newVideoInput) {
captureSession.addInput(newVideoInput)
self.currentVideoInput = newVideoInput // Update current input
} else {
print("Could not add new video input to the session. Re-adding original input.")
captureSession.addInput(currentInput) // Fallback to original input
}
}
}
}
6. Basic Camera UI
To make our camera usable, we'll need a simple UI. For a UIViewController managing the camera, you might lay it out like this:
┌───────────────────────────────────────┐
│ Camera View │
│ ┌───────────────────────────────────┐ │
│ │ │ │
│ │ AVCaptureVideoPreviewLayer │ │
│ │ (filling this area) │ │
│ │ │ │
│ └───────────────────────────────────┘ │
│ │
│ ┌───────────────┐ ┌─────────────────┐ │
│ │ Switch Camera │ │ Capture Photo │ │
│ └───────────────┘ └─────────────────┘ │
└───────────────────────────────────────┘
This ASCII diagram represents a UIViewController's view. The AVCaptureVideoPreviewLayer covers most of the screen, providing the live feed. At the bottom, we have two buttons: one to switch between front/back cameras, and another to capture a photo. These buttons would be connected to the @IBAction methods we defined earlier.
7. Handling Session Lifecycle
Starting and stopping the AVCaptureSession correctly is vital for performance and battery life.
startRunning(): Call this when your camera view controller appears or is about to become active. This should be done on yoursessionQueue.stopRunning(): Call this when your camera view controller disappears or becomes inactive. This also needs to be on yoursessionQueue.
class CameraViewController: UIViewController {
// ... (previous properties and methods)
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
sessionQueue.async { [weak self] in
self?.captureSession?.startRunning()
}
}
override func viewWillDisappear(_ animated: Bool) {
sessionQueue.async { [weak self] in
self?.captureSession?.stopRunning()
}
super.viewWillDisappear(animated)
}
}
It's also good practice to observe AVCaptureSession.wasInterruptedNotification and AVCaptureSession.interruptionEndedNotification to handle scenarios where your app's camera session might be temporarily interrupted (e.g., by a phone call).
Summary
Building a custom camera feature with AVFoundation gives you immense power and flexibility compared to UIImagePickerController. We've covered the fundamental steps: requesting permissions, setting up the AVCaptureSession with inputs and outputs, displaying a live preview, capturing photos, and even switching cameras.
While this article provides a solid foundation, AVFoundation offers much more, including video recording, advanced focus and exposure controls, depth data, and more. Remember to always handle permissions gracefully, perform session operations on a background queue, and manage the session's lifecycle correctly to provide a robust and efficient camera experience.
Happy Swifting!