Si eres un iOS Developer que busca crear experiencias inmersivas y personalizadas, tarde o temprano te enfrentarás a un desafío fascinante: integrar y controlar la cámara en SwiftUI.
Históricamente, muchos desarrolladores optaban por la solución rápida: usar UIImagePickerController o PhotosUI. Aunque son excelentes para tareas básicas, no te permiten crear una interfaz de usuario personalizada (un viewfinder a medida), aplicar filtros en tiempo real, o escanear códigos de barras de forma nativa. Para tener control total en la programación Swift, necesitamos descender un nivel y utilizar el potente framework AVFoundation.
En este extenso tutorial, aprenderás cómo construir una cámara completamente personalizada desde cero utilizando AVFoundation y cómo integrarla fluidamente en SwiftUI. Exploraremos cómo estructurar este código en Swift y cómo adaptarlo en Xcode para iOS, macOS y las realidades técnicas de watchOS.
1. ¿Por qué usar AVFoundation para la Cámara en SwiftUI?
En el ecosistema de SwiftUI, Apple nos empuja hacia soluciones declarativas. Sin embargo, el hardware de la cámara es intrínsecamente imperativo y basado en flujos de datos continuos.
Como iOS Developer, usar AVFoundation te da los superpoderes para:
- Diseñar interfaces 100% personalizadas: Puedes poner botones, superposiciones y animaciones exactas sobre la vista previa de la cámara.
- Procesamiento de fotogramas (Frames): Extraer cada fotograma de video en tiempo real para usar CoreML (Inteligencia Artificial) o Vision (reconocimiento de texto/rostros).
- Control de Hardware: Ajustar manualmente el enfoque, la exposición, el balance de blancos y el flash.
2. Arquitectura de una Sesión de Captura
Antes de escribir código en Swift, debemos entender cómo funciona AVFoundation. Piensa en ello como un sistema de tuberías.
La arquitectura se compone de cuatro elementos principales:
AVCaptureSession: Es el “cerebro” o motor. Administra el flujo de datos desde las entradas hasta las salidas.AVCaptureDevice: Representa el hardware físico (la cámara frontal, la cámara trasera gran angular, el micrófono).AVCaptureDeviceInput: Es el adaptador que conecta el dispositivo de hardware (la cámara) a la sesión de captura.AVCaptureOutput: Es el destino final de los datos. Puede ser una salida de foto (AVCapturePhotoOutput), una salida de video en bruto (AVCaptureVideoDataOutput) o una salida de película (AVCaptureMovieFileOutput).
3. Paso 0: Permisos en Xcode (El Info.plist)
Ninguna aplicación de Swift puede acceder a la cámara sin el permiso explícito del usuario. Si intentas ejecutar el código sin este paso, tu aplicación se cerrará inesperadamente (crash).
- Abre tu proyecto en Xcode.
- Ve a la configuración de tu Target, pestaña Info.
- Añade una nueva clave llamada
Privacy - Camera Usage Description(NSCameraUsageDescription). - En el valor, escribe un mensaje claro para el usuario, por ejemplo: “Necesitamos acceso a la cámara para que puedas tomar fotos increíbles de tu entorno.”
4. Construyendo el Motor: CameraManager
Vamos a crear una clase que maneje toda la lógica pesada de AVFoundation. Utilizaremos el protocolo ObservableObject para que nuestra interfaz en SwiftUI pueda reaccionar a sus cambios.
Crea un nuevo archivo de Swift llamado CameraManager.swift.
import Foundation
import AVFoundation
import Combine
class CameraManager: ObservableObject {
// 1. La sesión de captura principal
let session = AVCaptureSession()
// 2. Salida para tomar fotos
private let photoOutput = AVCapturePhotoOutput()
// 3. Estados para la UI
@Published var isCameraAuthorized = false
@Published var capturedImage: Data?
// Hilo dedicado para no bloquear la Interfaz de Usuario
private let sessionQueue = DispatchQueue(label: "com.tudominio.cameraQueue")
init() {
checkPermissions()
}
// MARK: - Permisos
private func checkPermissions() {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
self.isCameraAuthorized = true
self.setupCamera()
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
DispatchQueue.main.async {
self?.isCameraAuthorized = granted
if granted { self?.setupCamera() }
}
}
default:
self.isCameraAuthorized = false
}
}
// MARK: - Configuración de la Cámara
private func setupCamera() {
sessionQueue.async { [weak self] in
guard let self = self else { return }
self.session.beginConfiguration()
// a. Buscar el dispositivo (Cámara trasera por defecto)
guard let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
print("No se encontró una cámara")
self.session.commitConfiguration()
return
}
// b. Crear la entrada
do {
let videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice)
if self.session.canAddInput(videoDeviceInput) {
self.session.addInput(videoDeviceInput)
}
} catch {
print("Error al crear input: \(error.localizedDescription)")
self.session.commitConfiguration()
return
}
// c. Crear la salida de foto
if self.session.canAddOutput(self.photoOutput) {
self.session.addOutput(self.photoOutput)
}
self.session.commitConfiguration()
}
}
// MARK: - Control de la Sesión
func startSession() {
sessionQueue.async {
if !self.session.isRunning {
self.session.startRunning()
}
}
}
func stopSession() {
sessionQueue.async {
if self.session.isRunning {
self.session.stopRunning()
}
}
}
// MARK: - Captura de Foto
func capturePhoto() {
let settings = AVCapturePhotoSettings()
// Aquí puedes configurar el flash, alta resolución, etc.
self.photoOutput.capturePhoto(with: settings, delegate: self)
}
}
// Implementación del delegado para recibir la foto
extension CameraManager: AVCapturePhotoCaptureDelegate {
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
guard let data = photo.fileDataRepresentation() else { return }
DispatchQueue.main.async {
self.capturedImage = data
}
}
}
Notas clave de programación Swift en este módulo:
- Hilos (Threading):
session.startRunning()es una operación bloqueante. Si la llamas en el hilo principal (Main Thread), tu aplicación se congelará momentáneamente. Por eso, como buen iOS Developer, hemos creado unasessionQueuededicada. - Manejo de Permisos: Antes de intentar acceder al hardware, debemos verificar el estado de autorización de manera asíncrona.
5. El Puente hacia SwiftUI: CameraPreviewView
SwiftUI no tiene una vista nativa (todavía) para mostrar el flujo de la cámara. Necesitamos usar una capa especializada de CoreAnimation llamada AVCaptureVideoPreviewLayer y envolverla utilizando el protocolo UIViewRepresentable (en iOS) o NSViewRepresentable (en macOS).
Crea un archivo llamado CameraPreviewView.swift. Aquí utilizaremos directivas del compilador para asegurar el funcionamiento multiplataforma en Xcode.
import SwiftUI
import AVFoundation
#if os(iOS)
struct CameraPreviewView: UIViewRepresentable {
let session: AVCaptureSession
// Creamos una subclase de UIView que está respaldada por una capa de video
class VideoPreviewView: UIView {
override class var layerClass: AnyClass {
AVCaptureVideoPreviewLayer.self
}
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
return layer as! AVCaptureVideoPreviewLayer
}
}
func makeUIView(context: Context) -> VideoPreviewView {
let view = VideoPreviewView()
view.videoPreviewLayer.session = session
view.videoPreviewLayer.videoGravity = .resizeAspectFill
return view
}
func updateUIView(_ uiView: VideoPreviewView, context: Context) {
// Aquí manejaríamos rotaciones de pantalla si fuera necesario
}
}
#elseif os(macOS)
struct CameraPreviewView: NSViewRepresentable {
let session: AVCaptureSession
class VideoPreviewView: NSView {
var videoPreviewLayer: AVCaptureVideoPreviewLayer!
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
self.wantsLayer = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func makeBackingLayer() -> CALayer {
videoPreviewLayer = AVCaptureVideoPreviewLayer()
videoPreviewLayer.videoGravity = .resizeAspectFill
return videoPreviewLayer
}
}
func makeNSView(context: Context) -> VideoPreviewView {
let view = VideoPreviewView()
view.videoPreviewLayer.session = session
return view
}
func updateNSView(_ nsView: VideoPreviewView, context: Context) {
}
}
#endif
Con este código, hemos creado un puente perfecto que inyecta los datos visuales generados por AVFoundation directamente dentro de nuestro ecosistema declarativo de SwiftUI.
6. Construyendo la Interfaz Principal de la Cámara en SwiftUI
Ahora viene la parte divertida: ensamblar los bloques. Vamos a usar un ZStack para colocar nuestros botones personalizados sobre la vista previa en vivo de la cámara en SwiftUI.
Navega a tu ContentView.swift o crea una nueva vista llamada CameraMainView.swift.
import SwiftUI
struct CameraMainView: View {
@StateObject private var cameraManager = CameraManager()
var body: some View {
ZStack {
// Fondo oscuro para la cámara
Color.black.ignoresSafeArea()
if cameraManager.isCameraAuthorized {
// 1. La capa de previsualización en el fondo
CameraPreviewView(session: cameraManager.session)
.ignoresSafeArea()
.onAppear {
cameraManager.startSession()
}
.onDisappear {
cameraManager.stopSession()
}
// 2. Elementos de Interfaz de Usuario superpuestos
VStack {
Spacer()
// Controles de la cámara
HStack {
Spacer()
// Botón de Captura
Button(action: {
cameraManager.capturePhoto()
}) {
Circle()
.stroke(Color.white, lineWidth: 3)
.frame(width: 70, height: 70)
.overlay(
Circle()
.fill(Color.white)
.frame(width: 60, height: 60)
)
}
.padding(.bottom, 30)
Spacer()
}
}
// 3. Mostrar la foto capturada si existe
if let capturedImage = cameraManager.capturedImage,
#available(iOS 16.0, macOS 13.0, *),
let image = platformImage(from: capturedImage) {
VStack {
HStack {
Spacer()
Image(nsImageOrUIImage: image)
.resizable()
.scaledToFill()
.frame(width: 100, height: 150)
.cornerRadius(10)
.shadow(radius: 5)
.padding()
.onTapGesture {
// Descartar la foto
withAnimation {
cameraManager.capturedImage = nil
}
}
}
Spacer()
}
}
} else {
// Pantalla de Permisos Denegados
VStack(spacing: 20) {
Image(systemName: "camera.slash")
.font(.system(size: 50))
.foregroundColor(.gray)
Text("No hay acceso a la cámara")
.font(.title3)
.foregroundColor(.white)
Text("Por favor, habilita el acceso en Configuración.")
.foregroundColor(.gray)
}
}
}
}
// Función auxiliar multiplataforma para procesar la Data a Imagen
#if os(iOS)
func platformImage(from data: Data) -> UIImage? {
UIImage(data: data)
}
#elseif os(macOS)
func platformImage(from data: Data) -> NSImage? {
NSImage(data: data)
}
#endif
}
// Extensión para facilitar la inicialización multiplataforma en SwiftUI
extension Image {
#if os(iOS)
init(nsImageOrUIImage image: UIImage) {
self.init(uiImage: image)
}
#elseif os(macOS)
init(nsImageOrUIImage image: NSImage) {
self.init(nsImage: image)
}
#endif
}
Analizando nuestra Vista:
- Control del Ciclo de Vida: Usamos
.onAppeary.onDisappearpara encender y apagar el motor de la cámara. Esto es crítico para ahorrar batería y no mantener bloqueado el hardware cuando el usuario navega a otra pantalla. - Multiplataforma Nativa: Hemos integrado helpers con
#if os(iOS)y#elseif os(macOS)para queUIImageyNSImageconvivan pacíficamente. Esta es la marca de un verdadero iOS Developer sénior operando en Xcode.
7. La Realidad Técnica: iOS vs. macOS vs. watchOS
El título de este artículo prometía abordar iOS, macOS y watchOS. Hemos implementado una arquitectura sólida y un puente representable que funciona maravillosamente tanto en un iPhone como en un Mac.
Sin embargo, como desarrollador, debes conocer las limitaciones del hardware de Apple. Aquí va una dosis de realidad técnica vital:
En iOS y iPadOS
El código que acabamos de escribir funciona perfectamente. Tienes control total, acceso a lentes ultra gran angulares, teleobjetivos, sensores LiDAR y procesamiento profundo usando Swift.
En macOS
El Mac soporta AVFoundation con una arquitectura muy similar. El puente NSViewRepresentable que escribimos asegura que tu app de SwiftUI detectará la cámara web integrada de tu MacBook o cualquier cámara Continuity (como usar tu iPhone como webcam) de forma nativa.
En watchOS: El Gran Obstáculo
Aquí debemos hacer una corrección técnica muy importante: watchOS NO soporta AVCaptureSession ni las APIs de captura en tiempo real de AVFoundation de la misma manera que iOS.
El Apple Watch no tiene una cámara integrada. Las aplicaciones nativas en watchOS interactúan con la cámara de dos maneras exclusivas:
- App de Cámara Remota: Controlando el visor de la cámara del iPhone emparejado (esto lo maneja el sistema, no tu código directo de AVFoundation).
- Cámaras en Correas de Terceros (Muy raras): E incluso entonces, la transmisión de video directa a través de
AVFoundationpara crear un visor personalizado (AVCaptureVideoPreviewLayer) está fuertemente bloqueada o no existe en la API pública de watchOS.
Si en el futuro Apple lanza un Apple Watch con cámara integrada, este mismo paradigma de programación Swift se adaptará, pero actualmente, AVFoundation para captura de video en vivo debe descartarse en la compilación para watchOS en Xcode.
8. Mejores Prácticas para Dominar la Cámara
Para cerrar este extenso tutorial, aquí tienes una lista de verificación mental que debes seguir al integrar una cámara en SwiftUI:
- Evita Fugas de Memoria: Un flujo de video es pesado. Asegúrate de siempre invalidar o detener (
stopRunning()) tu sesión deAVFoundationcuando la vista de SwiftUI desaparece. - Rotación del Dispositivo: Por defecto, el
AVCaptureVideoPreviewLayerno rota automáticamente cuando giras el teléfono. Tendrás que detectar los cambios de orientación en tuupdateUIViewy aplicar la transformación avideoPreviewLayer.connection?.videoOrientation. - Pellizcar para hacer Zoom: Puedes añadir un
MagnificationGesturea tu vista principal en SwiftUI y actualizar el parámetrovideoZoomFactorde tuAVCaptureDevice. ¡Inténtalo como reto personal! - Gestión de Errores Exhaustiva: Las cámaras pueden fallar. Otra app podría estar usándola, el dispositivo podría estar demasiado caliente y el sistema bloquear el acceso. En un entorno de producción, asegúrate de capturar (
catch) y mostrar notificaciones amigables en tu UI cuando algo falle.
Conclusión
Construir una cámara en SwiftUI combinando el poder subyacente de AVFoundation es uno de los proyectos más gratificantes en la programación Swift. Te obliga a entender cómo interactúa el mundo declarativo (interfaces de usuario rápidas y reactivas) con el mundo imperativo (flujos de datos de hardware en tiempo real).
Si tienes cualquier duda sobre este artículo, contacta conmigo y estaré encantado de ayudarte . Puedes contactar conmigo en mi perfil de X o en mi perfil de Instagram










