Как работает Camera API в iOS: полное руководство для разработчиков
UIImagePickerController удобен, но он закрывает доступ к половине возможностей камеры iPhone: кастомный UI, ручная экспозиция, тап-фокус, realtime-обработка кадров — всё это требует AVFoundation. Именно этот фреймворк мы используем в студии oodot при разработке мобильных приложений под iOS, начиная с первой версии Light Painting, где нам нужен был полный контроль над capture pipeline. В этой статье разберём AVFoundation от запроса разрешений до новых API iOS 17/18 — с живыми Swift-сниппетами на каждом шаге.

AVFoundation vs UIImagePickerController: что выбрать
Для большинства задач, связанных с камерой, у iOS-разработчика есть два пути. Сравним их честно:
| Критерий | UIImagePickerController | AVFoundation |
|---|---|---|
| Кастомный UI | Нет (стандартный Apple UI) | Полный контроль |
| Ручные настройки (ISO, выдержка, ББ) | Нет | Да |
| Realtime-обработка кадров | Нет | Да (AVCaptureVideoDataOutput) |
| Тап-фокус / тап-экспозиция | Частично (системный) | Полный контроль |
| Поддержка видео | Ограниченная | Полная |
| Сложность реализации | Низкая (10–20 строк) | Средняя–высокая |
Когда хватит UIImagePickerController: пользователю нужно выбрать фото из галереи или сделать простой снимок без кастомного интерфейса. Одна сцена выбора фото в форме профиля — классический случай.
Когда нужен AVFoundation: вы делаете кастомный camera screen, накладываете real-time-фильтры, нужен ручной контроль экспозиции, реализуете световую живопись, AR-наложение или любую нестандартную механику захвата.
Архитектура AVFoundation: сессии, устройства, входы и выходы
Прежде чем писать код, важно понять, из каких объектов состоит capture pipeline. AVFoundation работает по принципу «сессия → вход → выход».
AVCaptureSession
├── Input: AVCaptureDeviceInput (камера / микрофон)
└── Output: AVCapturePhotoOutput (фото)
AVCaptureVideoDataOutput (кадры в реальном времени)
AVCaptureMovieFileOutput (видео в файл)
AVCaptureSession — сердце системы
AVCaptureSession координирует потоки данных от входов к выходам. У неё есть пресеты качества (sessionPreset), которые влияют на разрешение и производительность:
let session = AVCaptureSession()
session.sessionPreset = .photo // оптимально для фото
// Для видео: .hd1920x1080 или .hd4K3840x2160
Жизненный цикл: вызов session.startRunning() начинает поток данных, session.stopRunning() — останавливает. Оба вызова блокирующие — выполняйте их на фоновой очереди, не на main thread. При уходе приложения в фон сессию нужно останавливать явно.
AVCaptureDevice
AVCaptureDevice представляет физическое устройство ввода — камеру или микрофон. Для выбора нужной камеры используйте AVCaptureDevice.default или AVCaptureDevice.DiscoverySession:
// Основная (задняя) широкоугольная камера
let camera = AVCaptureDevice.default(.builtInWideAngleCamera,
for: .video, position: .back)
// Фронтальная (TrueDepth)
let front = AVCaptureDevice.default(.builtInTrueDepthCamera,
for: .video, position: .front)
// Ультраширокоугольная
let ultra = AVCaptureDevice.default(.builtInUltraWideCamera,
for: .video, position: .back)
Параметр mediaType принимает .video (камера) или .audio (микрофон).
AVCaptureInput и AVCaptureOutput
AVCaptureDeviceInput оборачивает устройство и подключается к сессии. AVCapturePhotoOutput принимает фото-захваты, AVCaptureVideoDataOutput — покадровый видеопоток для обработки. Добавлять входы и выходы нужно внутри блоков beginConfiguration / commitConfiguration.
Запрос разрешений на камеру
Без разрешения пользователя обратиться к камере нельзя. Шаг первый — Info.plist.
Info.plist
Добавьте ключи в Info.plist (или в настройки таргета → Info):
<key>NSCameraUsageDescription</key>
<string>Приложению нужен доступ к камере для съёмки фото</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Приложение сохраняет снимки в вашу галерею</string>
Если ключ отсутствует, приложение упадёт с EXC_CRASH при попытке запросить разрешение.
Запрос доступа: async/await
func checkCameraPermission() async -> Bool {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
return true
case .notDetermined:
return await AVCaptureDevice.requestAccess(for: .video)
case .denied, .restricted:
await showSettingsAlert()
return false
@unknown default:
return false
}
}
Обработка отказа
Если пользователь нажал «Запретить», статус становится .denied и повторно запросить разрешение нельзя. Показывайте алерт с кнопкой «Открыть Настройки»:
func showSettingsAlert() async {
let alert = UIAlertController(
title: "Нет доступа к камере",
message: "Разрешите доступ в Настройках → Конфиденциальность → Камера",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Открыть Настройки", style: .default) { _ in
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
})
alert.addAction(UIAlertAction(title: "Отмена", style: .cancel))
// present alert...
}
В iOS 17 Apple ввела ограниченный доступ к фотобиблиотеке (Limited Photo Library). Используйте PHPickerViewController вместо прямого обращения к PHPhotoLibrary — он работает без запроса разрешений и даёт пользователю выбрать только нужные снимки.
Настройка AVCaptureSession: пошаговый код
Все операции конфигурирования сессии выполняются на фоновой очереди — иначе интерфейс будет зависать.
Создание сессии и добавление устройства
final class CameraViewModel: ObservableObject {
let session = AVCaptureSession()
private let sessionQueue = DispatchQueue(label: "camera.session.queue")
private var photoOutput = AVCapturePhotoOutput()
func configure() {
sessionQueue.async { [weak self] in
guard let self else { return }
self.session.beginConfiguration()
self.session.sessionPreset = .photo
self.addVideoInput()
self.addPhotoOutput()
self.session.commitConfiguration()
self.session.startRunning()
}
}
}
Настройка входа (AVCaptureDeviceInput)
private func addVideoInput() {
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera,
for: .video, position: .back),
let input = try? AVCaptureDeviceInput(device: device) else { return }
if session.canAddInput(input) {
session.addInput(input)
}
}
Настройка выхода (AVCapturePhotoOutput)
private func addPhotoOutput() {
photoOutput.isHighResolutionCaptureEnabled = true
if session.canAddOutput(photoOutput) {
session.addOutput(photoOutput)
}
}
Preview: показываем изображение с камеры
Превью — это живой поток с камеры, который пользователь видит до нажатия кнопки съёмки.
UIKit — AVCaptureVideoPreviewLayer
AVCaptureVideoPreviewLayer — это CALayer, который напрямую показывает поток сессии. Встраивается в UIView:
class PreviewView: UIView {
override class var layerClass: AnyClass {
AVCaptureVideoPreviewLayer.self
}
var previewLayer: AVCaptureVideoPreviewLayer {
layer as! AVCaptureVideoPreviewLayer
}
func setSession(_ session: AVCaptureSession) {
previewLayer.session = session
previewLayer.videoGravity = .resizeAspectFill
}
}
SwiftUI — UIViewRepresentable
Поскольку AVCaptureVideoPreviewLayer — это CALayer, а не SwiftUI View, для интеграции нужен мост через UIViewRepresentable:
struct CameraPreview: UIViewRepresentable {
let session: AVCaptureSession
func makeUIView(context: Context) -> PreviewView {
let view = PreviewView()
view.setSession(session)
return view
}
func updateUIView(_ uiView: PreviewView, context: Context) {
// обновление не требуется — сессия держит живой поток
}
}
// Использование в SwiftUI:
struct CameraScreen: View {
@StateObject private var viewModel = CameraViewModel()
var body: some View {
ZStack {
CameraPreview(session: viewModel.session)
.ignoresSafeArea()
// кнопки управления поверх
}
.onAppear { viewModel.configure() }
.onDisappear { viewModel.session.stopRunning() }
}
}
Съёмка фото: AVCapturePhotoOutput + delegate
Захват фото — асинхронная операция с делегатным коллбэком.
func capturePhoto() {
let settings = AVCapturePhotoSettings()
settings.flashMode = .auto
settings.isHighResolutionPhotoEnabled = true
photoOutput.capturePhoto(with: settings, delegate: self)
}
extension CameraViewModel: AVCapturePhotoCaptureDelegate {
func photoOutput(_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photo: AVCapturePhoto,
error: Error?) {
guard error == nil,
let data = photo.fileDataRepresentation(),
let image = UIImage(data: data) else { return }
DispatchQueue.main.async { [weak self] in
self?.capturedImage = image
}
}
}
Метод fileDataRepresentation() возвращает HEIC или JPEG — формат определяется настройками системы и устройства. Для явного задания формата используйте AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg]).
Вспышка, факел и переключение камер
Управление вспышкой и факелом
Вспышка при съёмке управляется через настройки захвата:
// Вспышка для фото
let settings = AVCapturePhotoSettings()
settings.flashMode = .on // .off / .auto / .on
// Факел (постоянный свет) — отдельно от вспышки
func toggleTorch(on: Bool) {
guard let device = currentDevice, device.hasTorch, device.isTorchAvailable else { return }
try? device.lockForConfiguration()
device.torchMode = on ? .on : .off
device.unlockForConfiguration()
}
Переключение фронтальной и основной камеры
Переключение требует замены входа в сессии — старый input удаляется, новый добавляется:
func switchCamera() {
sessionQueue.async { [weak self] in
guard let self else { return }
let currentPosition = self.currentDevice?.position ?? .back
let newPosition: AVCaptureDevice.Position = currentPosition == .back ? .front : .back
guard let newDevice = AVCaptureDevice.default(.builtInWideAngleCamera,
for: .video,
position: newPosition),
let newInput = try? AVCaptureDeviceInput(device: newDevice) else { return }
self.session.beginConfiguration()
if let currentInput = self.currentInput {
self.session.removeInput(currentInput)
}
if self.session.canAddInput(newInput) {
self.session.addInput(newInput)
self.currentInput = newInput
self.currentDevice = newDevice
}
self.session.commitConfiguration()
}
}
Для анимации при переключении добавьте CATransition на preview layer перед вызовом:
let transition = CATransition()
transition.duration = 0.3
transition.type = .fade
previewLayer.add(transition, forKey: nil)
Тап-фокус и тап-экспозиция
Тап по экрану — одна из ключевых механик кастомной камеры. Реализуется через конвертацию экранных координат в координаты сенсора.
func tapToFocusAndExpose(at point: CGPoint, in view: UIView) {
guard let device = currentDevice else { return }
// конвертация координат экрана → координаты сенсора камеры
let devicePoint = previewLayer.captureDevicePointConverted(fromLayerPoint: point)
do {
try device.lockForConfiguration()
if device.isFocusPointOfInterestSupported,
device.isFocusModeSupported(.autoFocus) {
device.focusPointOfInterest = devicePoint
device.focusMode = .autoFocus
}
if device.isExposurePointOfInterestSupported,
device.isExposureModeSupported(.autoExpose) {
device.exposurePointOfInterest = devicePoint
device.exposureMode = .autoExpose
}
device.unlockForConfiguration()
} catch {
print("lockForConfiguration failed: \(error)")
}
}
В SwiftUI добавьте жест и анимацию кольца фокуса:
// В SwiftUI View
.onTapGesture { location in
viewModel.tapToFocus(at: location)
withAnimation(.easeOut(duration: 0.3)) {
focusPoint = location
showFocusRing = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
showFocusRing = false
}
}
// Кольцо фокуса
if showFocusRing, let point = focusPoint {
Circle()
.stroke(Color.yellow, lineWidth: 1.5)
.frame(width: 72, height: 72)
.position(point)
.transition(.opacity)
}
Pinch-to-zoom
Зум реализуется через videoZoomFactor устройства с учётом допустимых пределов.
// UIKit — UIPinchGestureRecognizer
@objc func handlePinch(_ gesture: UIPinchGestureRecognizer) {
guard let device = currentDevice else { return }
if gesture.state == .began {
lastZoomFactor = device.videoZoomFactor
}
let newFactor = min(
max(lastZoomFactor * gesture.scale, device.minAvailableVideoZoomFactor),
min(device.maxAvailableVideoZoomFactor, 10.0) // ограничим 10x
)
do {
try device.lockForConfiguration()
device.videoZoomFactor = newFactor
device.unlockForConfiguration()
} catch {}
}
// SwiftUI — MagnificationGesture
.gesture(
MagnificationGesture()
.onChanged { value in
viewModel.setZoom(factor: value)
}
)
Для плавного изменения зума без рывков используйте ramp(toVideoZoomFactor:withRate:):
device.ramp(toVideoZoomFactor: targetFactor, withRate: 8.0)
Ручное управление: экспозиция и баланс белого
Ручная экспозиция
Ручное управление экспозицией позволяет задать ISO и выдержку напрямую:
func setManualExposure(iso: Float, duration: CMTime) {
guard let device = currentDevice else { return }
let clampedISO = iso.clamped(to: device.activeFormat.minISO...device.activeFormat.maxISO)
try? device.lockForConfiguration()
device.setExposureModeCustom(duration: duration, iso: clampedISO)
device.unlockForConfiguration()
}
// Пример: ISO 400, выдержка 1/60
setManualExposure(iso: 400,
duration: CMTime(value: 1, timescale: 60))
Баланс белого
func setWhiteBalance(temperature: Float, tint: Float) {
guard let device = currentDevice else { return }
let tempAndTint = AVCaptureDevice.WhiteBalanceTemperatureAndTintValues(
temperature: temperature, tint: tint
)
let gains = device.deviceWhiteBalanceGains(for: tempAndTint)
// нормализуем gains в пределах допустимого
let maxGain = device.maxWhiteBalanceGain
let normalized = AVCaptureDevice.WhiteBalanceGains(
redGain: min(gains.redGain, maxGain),
greenGain: min(gains.greenGain, maxGain),
blueGain: min(gains.blueGain, maxGain)
)
try? device.lockForConfiguration()
device.setWhiteBalanceModeLocked(with: normalized)
device.unlockForConfiguration()
}
Новые API iOS 17/18: Zero Shutter Lag, Responsive Capture, AVCaptureControl
Apple продолжает расширять возможности AVFoundation. iOS 17 и 18 принесли несколько значимых дополнений.
Zero Shutter Lag (iOS 17+)
Zero Shutter Lag устраняет задержку затвора, предзаписывая кадры в циклический буфер. При нажатии кнопки захвата система выбирает лучший уже записанный кадр вместо того, чтобы снимать новый:
if photoOutput.isZeroShutterLagSupported {
photoOutput.isZeroShutterLagEnabled = true
}
Практический выигрыш: у Sports / Action-приложений исчезает задержка между нажатием и снимком. Для световой живописи этот режим не нужен — там важен момент, а не предзапись.
Responsive Capture (iOS 17+)
Responsive Capture позволяет начать следующий захват до того, как система полностью обработала предыдущий. Это критично для серийной съёмки в кастомном камера-приложении:
if photoOutput.isResponsiveCaptureSupported {
photoOutput.isResponsiveCaptureEnabled = true
// можно вызывать capturePhoto повторно, не дожидаясь завершения предыдущего
}
Deferred Photo Processing (iOS 17+)
Тяжёлая обработка (HDR-композитинг, шумоподавление) выполняется в фоне после захвата, не блокируя интерфейс. Флаг processingDeferred в AVCapturePhoto сообщает, что изображение ещё обрабатывается:
func photoOutput(_ output: AVCapturePhotoOutput,
didFinishCapturingDeferredPhotoProxy proxy: AVCaptureDeferredPhotoProxy?,
error: Error?) {
// proxy доступен сразу для превью
// финальное фото приходит позже через didFinishProcessingPhoto
}
AVCaptureControl (iOS 18 / WWDC 2025)
AVCaptureControl — новый слой UI-элементов управления прямо поверх HUD камеры. Разработчики могут добавить слайдеры, кнопки и переключатели, которые органично вписываются в системный Camera Control API:
// iOS 18+
let zoomSlider = AVCaptureSlider("Zoom", symbolName: "plus.magnifyingglass",
in: 1.0...10.0)
zoomSlider.setActionQueue(sessionQueue) { [weak self] newValue in
self?.setZoom(factor: CGFloat(newValue))
}
if photoOutput.isControlSupported(zoomSlider) {
photoOutput.addControl(zoomSlider)
}
Это позволяет строить UI камеры, совместимый со стандартным Camera Control на iPhone — механической кнопкой на боку устройства, появившейся в iPhone 16.
Сохранение фото в галерею
Сохранение в PHPhotoLibrary требует отдельного разрешения и выполняется асинхронно:
import Photos
func saveToPhotoLibrary(_ image: UIImage) {
PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in
guard status == .authorized else { return }
PHPhotoLibrary.shared().performChanges {
PHAssetChangeRequest.creationRequestForAsset(from: image)
} completionHandler: { success, error in
if let error {
print("Ошибка сохранения: \(error)")
}
}
}
}
Ключ NSPhotoLibraryAddUsageDescription в Info.plist обязателен. Если сохраняете только что снятые фото — используйте .addOnly, это минимально необходимое разрешение.
FAQ: частые вопросы по Camera API iOS
Почему приложение с камерой не работает в Simulator?
В iOS Simulator физической камеры нет — AVCaptureSession не найдёт устройство и не запустится. Тестируйте всегда на реальном iPhone. Начиная с iOS 17, есть Simulator Camera Feature (виртуальный источник), но он поддерживает только базовые сценарии — для полноценного тестирования AVFoundation нужен физический iPhone.
Как обработать отказ пользователя от разрешения на камеру?
Проверьте AVCaptureDevice.authorizationStatus(for: .video). При статусе .denied или .restricted покажите UIAlertController с кнопкой «Открыть Настройки», которая ведёт на UIApplication.openSettingsURLString. Никогда не скрывайте функцию камеры молча — пользователь должен понять, что делать.
Можно ли запускать AVCaptureSession в фоновом режиме?
Нет, без специального entitlement (он доступен только ограниченному кругу приложений — видеозвонки, радионяня). При UIApplication.didEnterBackgroundNotification вызовите session.stopRunning(). Если не сделать этого явно, система остановит сессию принудительно и выдаст предупреждение в логах.
Как избежать утечек памяти при работе с AVCaptureSession?
AVCapturePhotoCaptureDelegate удерживается сессией до завершения захвата — используйте [weak self] в замыканиях делегата. Всю работу с сессией (конфигурирование, start/stop) выполняйте на выделенной DispatchQueue, а не на main. Следите, чтобы ViewModel не держала сильную ссылку на саму себя через замыкание делегата.
SwiftUI или UIKit для экрана камеры?
AVCaptureVideoPreviewLayer — это CALayer, его невозможно напрямую встроить в SwiftUI-иерархию. Стандартное решение: UIViewRepresentable, оборачивающий UIView с preview layer. Бизнес-логика (ViewModel, управление сессией) прекрасно живёт в SwiftUI через @Observable или ObservableObject — полный SwiftUI-стек возможен, если использовать мост только для preview.
С какой версии iOS доступен AVCapturePhotoOutput?
AVCapturePhotoOutput доступен с iOS 10 (устаревший AVCaptureStillImageOutput убран в ту же версию). Zero Shutter Lag и Responsive Capture — iOS 17+. AVCaptureControl и поддержка Camera Control button — iOS 18 / WWDC 2025.
AVFoundation даёт полный контроль над тем, что система камеры iPhone делает в каждый момент: от разрешений и настройки сессии до ручной экспозиции и новейших API iOS 17/18. Именно этот уровень контроля позволил нам реализовать в Glow Doodle кастомный capture pipeline с накоплением кадров в реальном времени — то, что невозможно через UIImagePickerController. Официальный пример Apple AVCam — отличная точка отсчёта для дальнейшего изучения; рекомендую держать его рядом, когда разбираете конкретные edge-случаи.