🎬 ¿Por qué Camera Control es el futuro de la fotografía móvil?#
Imagina: estás en el concierto de tu banda favorita. La multitud te presiona por todos lados, tus manos están ocupadas y estás intentando tomar la foto perfecta. Antes, tenías que tocar torpemente la pantalla, arriesgándote a dejar caer el teléfono. Ahora, una sola pulsación del botón lateral del iPhone 16 — y el momento está capturado.
Esto no es ciencia ficción. Esto es Camera Control — una de las innovaciones más significativas de Apple en fotografía móvil en los últimos años.
Lo que aprenderás#
En esta guía, recorreremos todo el camino desde cero hasta una integración completa de Camera Control:
- Cómo funciona la arquitectura de Camera Control y por qué funciona así
- Configuración paso a paso del proyecto desde cero
- Creación de Lock Screen Camera Extension (requerido para aparecer en ajustes)
- Agregar controles del sistema de zoom y exposición
- Manejo de eventos del botón disparador
- Integración tanto para UIKit como para SwiftUI
- Solución de problemas comunes y depuración
📱 ¿Qué es Camera Control y cómo funciona?#
Camera Control no es solo "otro botón". Es un ecosistema completo que combina hardware, APIs de software y una interfaz de usuario intuitiva.
Base de Hardware#
En iPhone 16 y modelos más nuevos, el botón lateral está equipado con sensores especiales:
- Force Touch — detecta la presión de pulsación
- Capacitive sensing — reconoce toques y deslizamientos
- Haptic feedback — proporciona respuesta táctil
Esta combinación permite que el botón funcione como:
- Disparador de cámara (pulsación completa)
- Control deslizante de ajustes (deslizar sobre el botón)
- Selector de modos (doble pulsación)
Tres funciones principales de Camera Control#
- Lanzamiento de app — Camera Control puede abrir tu app directamente desde la pantalla de bloqueo
- Botón disparador — pulsación física para captura de fotos o inicio/parada de video
- Ajuste de parámetros — deslizar sobre el botón para ajustar zoom, exposición y otros parámetros
🎯 Requisitos y compatibilidad#
Antes de escribir código, asegúrate de que tu proyecto cumple con los requisitos.
Requisitos del sistema#
| Componente | Versión mínima |
|---|---|
| iOS | 18.0+ |
| Dispositivo | iPhone 16, 16 Plus, 16 Pro, 16 Pro Max |
| Xcode | 16.0+ |
| Swift | 5.9+ |
Frameworks requeridos#
import AVFoundation // Framework principal para trabajo con cámara
import AVKit // Para AVCaptureEventInteraction
import Photos // Para guardar fotos en la galeríaVerificar compatibilidad en código#
Siempre verifica la disponibilidad de Camera Control antes de usar:
func checkCameraControlSupport() -> Bool {
// Verificar versión de iOS
guard #available(iOS 18.0, *) else {
print("❌ Requiere iOS 18.0 o superior")
return false
}
// Verificar soporte del dispositivo
let session = AVCaptureSession()
guard session.supportsControls else {
print("❌ El dispositivo no soporta Camera Control")
return false
}
print("✅ Camera Control completamente soportado")
return true
}⚠️ Importante: La propiedad
supportsControlsdevuelvetruesolo en iPhone 16 y posteriores. En el simulador y dispositivos antiguos, siempre esfalse.
🏗️ Arquitectura del proyecto#
Estructura de archivos recomendada#
CameraControlApp/
├── CameraControlApp.xcodeproj
├── CameraControlApp/
│ ├── App/
│ │ ├── CameraControlApp.swift // Punto de entrada (SwiftUI)
│ │ └── AppDelegate.swift // (opcional, para UIKit)
│ ├── Camera/
│ │ ├── CameraController.swift // Lógica principal de cámara
│ │ ├── CameraPreviewView.swift // Preview para UIKit
│ │ └── CameraModel.swift // ObservableObject para SwiftUI
│ ├── Views/
│ │ ├── CameraView.swift // Vista de cámara SwiftUI
│ │ └── CameraViewController.swift // Controlador UIKit
│ └── Extensions/
│ └── AVCaptureSession+Extensions.swift
├── LockScreenCameraExtension/ // Extensión Lock Screen
│ ├── LockScreenCameraExtension.swift
│ └── Info.plist
└── Tests/
└── CameraControlTests.swift🔧 Paso 1: Crear CameraController#
CameraController es la clase que gestiona toda la lógica de cámara y Camera Control.
import AVFoundation
import AVKit
import Photos
/// Controlador principal de cámara con soporte Camera Control
class CameraController: NSObject, ObservableObject {
// MARK: - Propiedades públicas
/// Factor de zoom actual
@Published var currentZoomFactor: CGFloat = 1.0
/// Compensación de exposición actual
@Published var currentExposureBias: Float = 0.0
/// Si el overlay de Camera Control está activo
@Published var isCameraControlActive: Bool = false
/// Última imagen capturada (para preview)
@Published var lastCapturedImage: UIImage?
/// Estado de permiso de cámara
@Published var permissionStatus: AVAuthorizationStatus = .notDetermined
// MARK: - Propiedades internas
/// Sesión de captura — objeto central de AVFoundation
let session = AVCaptureSession()
/// Cola para configuración de sesión (¡nunca en main thread!)
private let sessionQueue = DispatchQueue(
label: "com.yourapp.camera.session",
qos: .userInitiated
)
/// Salida de foto
private let photoOutput = AVCapturePhotoOutput()
/// Entrada de cámara
private var deviceInput: AVCaptureDeviceInput?
/// Almacenamiento para observadores KVO
private var observations: [NSKeyValueObservation] = []
// MARK: - Inicialización
override init() {
super.init()
checkPermissions()
}
// MARK: - Manejo de permisos
/// Verifica y solicita permiso de cámara
func checkPermissions() {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
permissionStatus = .authorized
configureSession()
case .notDetermined:
sessionQueue.suspend()
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
guard let self = self else { return }
DispatchQueue.main.async {
self.permissionStatus = granted ? .authorized : .denied
}
if granted {
self.configureSession()
}
self.sessionQueue.resume()
}
case .denied, .restricted:
permissionStatus = .denied
print("⚠️ Acceso a cámara denegado")
@unknown default:
break
}
}
// MARK: - Configuración de sesión
/// Configura AVCaptureSession
private func configureSession() {
sessionQueue.async { [weak self] in
guard let self = self else { return }
self.session.beginConfiguration()
self.session.sessionPreset = .photo
// Configurar entrada de cámara
guard self.setupCameraInput() else {
self.session.commitConfiguration()
return
}
// Configurar salida de foto
guard self.setupPhotoOutput() else {
self.session.commitConfiguration()
return
}
// Configurar Camera Control (iOS 18+)
if #available(iOS 18.0, *) {
self.setupCameraControls()
}
self.session.commitConfiguration()
self.session.startRunning()
print("✅ Sesión de cámara iniciada")
}
}
/// Configura entrada de cámara
private func setupCameraInput() -> Bool {
guard let camera = AVCaptureDevice.default(
.builtInWideAngleCamera,
for: .video,
position: .back
) else {
print("❌ Cámara no disponible")
return false
}
do {
let input = try AVCaptureDeviceInput(device: camera)
if session.canAddInput(input) {
session.addInput(input)
deviceInput = input
setupDeviceObservers(device: camera)
return true
} else {
print("❌ No se pudo agregar entrada de cámara")
return false
}
} catch {
print("❌ Error: \(error)")
return false
}
}
/// Configura salida de foto
private func setupPhotoOutput() -> Bool {
guard session.canAddOutput(photoOutput) else {
print("❌ No se pudo agregar salida de foto")
return false
}
session.addOutput(photoOutput)
photoOutput.isHighResolutionCaptureEnabled = true
if let connection = photoOutput.connection(with: .video),
connection.isVideoStabilizationSupported {
connection.preferredVideoStabilizationMode = .auto
}
return true
}
/// Configura observadores de cambios del dispositivo
private func setupDeviceObservers(device: AVCaptureDevice) {
// Observar cambios de zoom
let zoomObserver = device.observe(
\.videoZoomFactor,
options: [.new]
) { [weak self] device, change in
guard let newValue = change.newValue else { return }
DispatchQueue.main.async {
self?.currentZoomFactor = newValue
}
}
observations.append(zoomObserver)
// Observar cambios de exposición
let exposureObserver = device.observe(
\.exposureTargetBias,
options: [.new]
) { [weak self] device, change in
guard let newValue = change.newValue else { return }
DispatchQueue.main.async {
self?.currentExposureBias = newValue
}
}
observations.append(exposureObserver)
}
// MARK: - Captura de foto
/// Captura una foto
func capturePhoto() {
guard let connection = photoOutput.connection(with: .video),
connection.isEnabled else {
print("❌ Conexión de cámara inactiva")
return
}
let settings = AVCapturePhotoSettings()
settings.flashMode = .auto
settings.isHighResolutionPhotoEnabled = true
photoOutput.capturePhoto(with: settings, delegate: self)
print("📸 Captura de foto iniciada")
}
// MARK: - Controles
/// Establece zoom programáticamente
func setZoom(_ factor: CGFloat) {
guard let device = deviceInput?.device else { return }
let clampedFactor = min(
max(factor, device.minAvailableVideoZoomFactor),
device.maxAvailableVideoZoomFactor
)
sessionQueue.async {
do {
try device.lockForConfiguration()
device.videoZoomFactor = clampedFactor
device.unlockForConfiguration()
} catch {
print("❌ Error de zoom: \(error)")
}
}
}
// MARK: - Ciclo de vida
func startSession() {
sessionQueue.async { [weak self] in
guard let self = self, !self.session.isRunning else { return }
self.session.startRunning()
}
}
func stopSession() {
sessionQueue.async { [weak self] in
guard let self = self, self.session.isRunning else { return }
self.session.stopRunning()
}
}
deinit {
observations.removeAll()
stopSession()
}
}🎛️ Paso 2: Agregar Camera Control (iOS 18+)#
// MARK: - Configuración Camera Control (iOS 18+)
@available(iOS 18.0, *)
extension CameraController: AVCaptureSessionControlsDelegate {
/// Configura Camera Control
func setupCameraControls() {
// Verificar soporte
guard session.supportsControls else {
print("⚠️ Camera Control no soportado")
return
}
guard let device = deviceInput?.device else {
print("❌ Dispositivo de cámara no encontrado")
return
}
// Eliminar controles existentes
for control in session.controls {
session.removeControl(control)
}
var controls: [AVCaptureControl] = []
// 1. Slider de zoom del sistema
let zoomSlider = AVCaptureSystemZoomSlider(
device: device
) { [weak self] zoomFactor in
DispatchQueue.main.async {
self?.currentZoomFactor = zoomFactor
}
print("🔍 Zoom: \(zoomFactor)x")
}
controls.append(zoomSlider)
// 2. Slider de exposición del sistema
let exposureSlider = AVCaptureSystemExposureBiasSlider(
device: device
) { [weak self] bias in
DispatchQueue.main.async {
self?.currentExposureBias = bias
}
print("☀️ Exposición: \(bias)")
}
controls.append(exposureSlider)
// Agregar controles a la sesión
for control in controls {
if session.canAddControl(control) {
session.addControl(control)
print("✅ Agregado: \(type(of: control))")
}
}
// Establecer delegado
session.setControlsDelegate(self, queue: sessionQueue)
print("✅ Camera Control configurado")
}
// MARK: - AVCaptureSessionControlsDelegate
func sessionControlsDidBecomeActive(_ session: AVCaptureSession) {
DispatchQueue.main.async { [weak self] in
self?.isCameraControlActive = true
print("🎮 Camera Control activado")
}
}
func sessionControlsDidBecomeInactive(_ session: AVCaptureSession) {
DispatchQueue.main.async { [weak self] in
self?.isCameraControlActive = false
print("🎮 Camera Control desactivado")
}
}
}📸 Paso 3: Manejo de captura de fotos#
// MARK: - AVCapturePhotoCaptureDelegate
extension CameraController: AVCapturePhotoCaptureDelegate {
func photoOutput(
_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photo: AVCapturePhoto,
error: Error?
) {
if let error = error {
print("❌ Error: \(error.localizedDescription)")
return
}
guard let imageData = photo.fileDataRepresentation(),
let image = UIImage(data: imageData) else {
print("❌ No se pudo obtener la imagen")
return
}
DispatchQueue.main.async { [weak self] in
self?.lastCapturedImage = image
}
saveToPhotoLibrary(imageData: imageData)
print("✅ Foto capturada")
}
private func saveToPhotoLibrary(imageData: Data) {
PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in
guard status == .authorized else {
print("⚠️ Sin permiso para guardar")
return
}
PHPhotoLibrary.shared().performChanges {
let request = PHAssetCreationRequest.forAsset()
request.addResource(with: .photo, data: imageData, options: nil)
} completionHandler: { success, error in
if success {
print("✅ Guardado en galería")
}
}
}
}
}🔐 Paso 7: Lock Screen Camera Extension#
¡Este es un paso crítico! Sin Lock Screen Extension, tu app no aparecerá en los ajustes de Camera Control.
Crear Extension Target#
- En Xcode: File → New → Target
- Selecciona "Locked Camera Capture Extension"
- Nómbralo:
YourAppLockScreenCamera - Haz clic en Create
Implementación de Extension#
import LockedCameraCapture
import AVFoundation
import Photos
import UIKit
@main
class LockScreenCameraExtension: NSObject, LockedCameraCaptureExtension {
private var captureSession: LockedCameraCaptureSession?
private var photoOutput: LockedCameraCapturePhotoOutput?
func beginCapture(
with device: LockedCameraCaptureDevice,
completion: @escaping (Error?) -> Void
) {
print("📷 Lock Screen Camera: iniciando captura")
captureSession = LockedCameraCaptureSession()
photoOutput = LockedCameraCapturePhotoOutput()
guard let session = captureSession,
let output = photoOutput else {
completion(LockScreenCameraError.initializationFailed)
return
}
do {
try session.addInput(device)
try session.addOutput(output)
session.startRunning()
print("✅ Lock Screen Camera: sesión iniciada")
completion(nil)
} catch {
print("❌ Lock Screen Camera: \(error)")
completion(error)
}
}
func endCapture(completion: @escaping (Error?) -> Void) {
captureSession?.stopRunning()
captureSession = nil
photoOutput = nil
completion(nil)
}
}
enum LockScreenCameraError: Error {
case initializationFailed
}⚠️ Problemas comunes y soluciones#
Problema 1: Camera Control no aparece#
Síntomas: El botón lateral no responde.
Soluciones:
- Verifica
session.supportsControls— necesita iPhone 16+ - Asegúrate de que la sesión esté ejecutándose
- El preview debe ser visible en pantalla
- Los controles deben estar agregados
Problema 2: App no en ajustes de Camera Control#
Causa: No hay Lock Screen Camera Extension.
Solución: Crea la Extension (Paso 7).
Problema 3: UI no se actualiza#
Causa: Actualizaciones no en main thread.
// ✅ Correcto
let zoomSlider = AVCaptureSystemZoomSlider(device: device) { [weak self] factor in
DispatchQueue.main.async {
self?.currentZoomFactor = factor
}
}🎯 Mejores prácticas#
1. Siempre verifica compatibilidad#
if #available(iOS 18.0, *), session.supportsControls {
setupCameraControls()
} else {
showTraditionalControls()
}2. Oculta tus controles cuando Camera Control está activo#
func sessionControlsDidBecomeActive(_ session: AVCaptureSession) {
DispatchQueue.main.async {
self.zoomSlider.isHidden = true
}
}📋 Lista de verificación pre-lanzamiento#
- Lock Screen Camera Extension creada
- Info.plist contiene todos los permisos
- El código maneja la falta de soporte
- UI se actualiza en main thread
- Sesión configurada en hilo de fondo
- Controles del sistema agregados
- AVCaptureEventInteraction configurado
- Probado en iPhone 16 real
- Degradación elegante funciona
🎬 Conclusión#
Camera Control es un cambio fundamental en cómo los usuarios interactúan con la cámara en iPhone.
Puntos clave#
- Lock Screen Extension es requerido — sin él, la app no aparecerá en ajustes
- Verifica
supportsControls— funciona solo en iPhone 16+ - Usa controles del sistema —
AVCaptureSystemZoomSlideryAVCaptureSystemExposureBiasSlider - Maneja eventos —
AVCaptureEventInteractionpara botón disparador - Sincroniza UI — a través del delegado
AVCaptureSessionControlsDelegate - Prueba en dispositivo real — el simulador no soporta Camera Control
Enlaces útiles:

