Мы собрали 20 лет заметок о световой живописи в один гид. Он бесплатный. Читать гид ×
× ПриложенияТехнологииГалерея БлогО студииНаписать нам ›
Главная/Блог/Как работает Camera API в iOS: полное руководство для разработчиков
Технологии

Как работает Camera API в iOS: полное руководство для разработчиков

Андрей Морозов · 2026-09-24 · oodot

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

TL;DR: Вы узнаете, как настроить AVCaptureSession, подключить камеру и микрофон, показать превью через UIKit и SwiftUI, снять фото, реализовать тап-фокус, pinch-to-zoom и переключение камер. Нужны Swift 5.9+, iOS 16+, Xcode 16 и физическое устройство. В итоге получите кастомный capture pipeline, который можно развивать под любую задачу.
iPhone в руке разработчика с интерфейсом Camera API и кольцом фокусировки

AVFoundation vs UIImagePickerController: что выбрать

Для большинства задач, связанных с камерой, у iOS-разработчика есть два пути. Сравним их честно:

КритерийUIImagePickerControllerAVFoundation
Кастомный 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-случаи.

Есть вопрос по теме?

Напишите нам — с удовольствием разберём ваш случай и, может быть, сделаем из него следующую статью.

xdt_software@oodot.com ›