camera freezes, preview rotated wrong, capture slow, session interrupted, black preview, front camera mirrored, camera not starting, AVCaptureSession errors, startRunning blocks, phone call interrupts camera
Installation
Details
Usage
After installing, this skill will be available to your AI coding assistant.
Verify installation:
skills listSkill Instructions
name: axiom-camera-capture-diag description: camera freezes, preview rotated wrong, capture slow, session interrupted, black preview, front camera mirrored, camera not starting, AVCaptureSession errors, startRunning blocks, phone call interrupts camera skill_type: diagnostic version: 1.0.0 last_updated: 2026-01-03 apple_platforms: iOS 17+, iPadOS 17+, macOS 14+, tvOS 17+
Camera Capture Diagnostics
Systematic troubleshooting for AVFoundation camera issues: frozen preview, wrong rotation, slow capture, session interruptions, and permission problems.
Overview
Core Principle: When camera doesn't work, the problem is usually:
- Threading (session work on main thread) - 35%
- Session lifecycle (not started, interrupted, not configured) - 25%
- Rotation (deprecated APIs, missing coordinator) - 20%
- Permissions (denied, not requested) - 15%
- Configuration (wrong preset, missing input/output) - 5%
Always check threading and session state BEFORE debugging capture logic.
Red Flags
Symptoms that indicate camera-specific issues:
| Symptom | Likely Cause |
|---|---|
| Preview shows black screen | Session not started, permission denied, no camera input |
| UI freezes when opening camera | startRunning() called on main thread |
| Camera freezes on phone call | No interruption handling |
| Preview rotated 90ยฐ wrong | Not using RotationCoordinator (iOS 17+) |
| Captured photo rotated wrong | Rotation angle not applied to output connection |
| Front camera photo not mirrored | This is correct! (preview mirrors, photo does not) |
| "Camera in use by another app" | Another app has exclusive access |
| Capture takes 2+ seconds | photoQualityPrioritization set to .quality |
| Session won't start on iPad | Split View - camera unavailable |
| Crash on older iOS | Using iOS 17+ APIs without availability check |
Mandatory First Steps
Before investigating code, run these diagnostics:
Step 1: Check Session State
print("๐ท Session state:")
print(" isRunning: \(session.isRunning)")
print(" inputs: \(session.inputs.count)")
print(" outputs: \(session.outputs.count)")
for input in session.inputs {
if let deviceInput = input as? AVCaptureDeviceInput {
print(" Input: \(deviceInput.device.localizedName)")
}
}
for output in session.outputs {
print(" Output: \(type(of: output))")
}
Expected output:
- โ isRunning: true, inputs โฅ 1, outputs โฅ 1 โ Session working
- โ ๏ธ isRunning: false โ Session not started or interrupted
- โ inputs: 0 โ Camera not added (permission? configuration?)
Step 2: Check Threading
print("๐งต Thread check:")
// When setting up session
sessionQueue.async {
print(" Setup thread: \(Thread.isMainThread ? "โ MAIN" : "โ
Background")")
}
// When starting session
sessionQueue.async {
print(" Start thread: \(Thread.isMainThread ? "โ MAIN" : "โ
Background")")
}
Expected output:
- โ All background โ Correct
- โ Any main thread โ UI will freeze
Step 3: Check Permissions
let status = AVCaptureDevice.authorizationStatus(for: .video)
print("๐ Camera permission: \(status.rawValue)")
switch status {
case .authorized: print(" โ
Authorized")
case .notDetermined: print(" โ ๏ธ Not yet requested")
case .denied: print(" โ Denied by user")
case .restricted: print(" โ Restricted (parental controls?)")
@unknown default: print(" โ Unknown")
}
Step 4: Check for Interruptions
// Add temporary observer to see interruptions
NotificationCenter.default.addObserver(
forName: .AVCaptureSessionWasInterrupted,
object: session,
queue: .main
) { notification in
if let reason = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as? Int {
print("๐จ Interrupted: reason \(reason)")
}
}
Decision Tree
Camera not working as expected?
โ
โโ Black/frozen preview?
โ โโ Check Step 1 (session state)
โ โ โโ isRunning = false โ See Pattern 1 (session not started)
โ โ โโ inputs = 0 โ See Pattern 2 (no camera input)
โ โ โโ isRunning = true, inputs > 0 โ See Pattern 3 (preview layer)
โ
โโ UI freezes when opening camera?
โ โโ Check Step 2 (threading)
โ โโ Main thread โ See Pattern 4 (move to session queue)
โ
โโ Camera freezes during use?
โ โโ After phone call โ See Pattern 5 (interruption handling)
โ โโ In Split View (iPad) โ See Pattern 6 (multitasking)
โ โโ Random freezes โ See Pattern 7 (thermal pressure)
โ
โโ Preview/photo rotated wrong?
โ โโ Preview rotated โ See Pattern 8 (RotationCoordinator preview)
โ โโ Captured photo rotated โ See Pattern 9 (capture rotation)
โ โโ Front camera "wrong" โ See Pattern 10 (mirroring expected)
โ
โโ Capture too slow?
โ โโ 2+ seconds delay โ See Pattern 11 (quality prioritization)
โ โโ Slight delay โ See Pattern 12 (deferred processing)
โ
โโ Permission issues?
โ โโ Status: notDetermined โ See Pattern 13 (request permission)
โ โโ Status: denied โ See Pattern 14 (settings prompt)
โ
โโ Crash on some devices?
โโ See Pattern 15 (API availability)
Diagnostic Patterns
Pattern 1: Session Not Started
Symptom: Black preview, isRunning = false
Common causes:
startRunning()never calledstartRunning()called but session has no inputs- Session stopped and never restarted
Diagnostic:
// Check if startRunning was called
print("isRunning before start: \(session.isRunning)")
session.startRunning()
print("isRunning after start: \(session.isRunning)")
Fix:
// Ensure session is started on session queue
func startSession() {
sessionQueue.async { [self] in
guard !session.isRunning else { return }
// Verify we have inputs before starting
guard !session.inputs.isEmpty else {
print("โ Cannot start - no inputs configured")
return
}
session.startRunning()
}
}
Time to fix: 10 min
Pattern 2: No Camera Input
Symptom: session.inputs.count = 0
Common causes:
- Camera permission denied
AVCaptureDeviceInputcreation failedcanAddInput()returned false- Configuration not committed
Diagnostic:
// Step through input setup
guard let camera = AVCaptureDevice.default(for: .video) else {
print("โ No camera device found")
return
}
print("โ
Camera: \(camera.localizedName)")
do {
let input = try AVCaptureDeviceInput(device: camera)
print("โ
Input created")
if session.canAddInput(input) {
print("โ
Can add input")
} else {
print("โ Cannot add input - check session preset compatibility")
}
} catch {
print("โ Input creation failed: \(error)")
}
Fix: Ensure permission is granted BEFORE creating input, and wrap in configuration block:
session.beginConfiguration()
// Add input here
session.commitConfiguration()
Time to fix: 15 min
Pattern 3: Preview Layer Not Connected
Symptom: isRunning = true, inputs configured, but preview is black
Common causes:
- Preview layer session not set
- Preview layer not in view hierarchy
- Preview layer frame is zero
Diagnostic:
print("Preview layer session: \(previewLayer.session != nil)")
print("Preview layer superlayer: \(previewLayer.superlayer != nil)")
print("Preview layer frame: \(previewLayer.frame)")
print("Preview layer connection: \(previewLayer.connection != nil)")
Fix:
// Ensure preview layer is properly configured
previewLayer.session = session
previewLayer.videoGravity = .resizeAspectFill
// Ensure frame is set (common in SwiftUI)
previewLayer.frame = view.bounds
Time to fix: 10 min
Pattern 4: Main Thread Blocking
Symptom: UI freezes for 1-3 seconds when camera opens
Root cause: startRunning() is a blocking call executed on main thread
Diagnostic:
// If this prints on main thread, that's the problem
print("startRunning on thread: \(Thread.current)")
session.startRunning()
Fix:
// Create dedicated serial queue
private let sessionQueue = DispatchQueue(label: "camera.session")
func startSession() {
sessionQueue.async { [self] in
session.startRunning()
}
}
Time to fix: 15 min
Pattern 5: Phone Call Interruption
Symptom: Camera works, then freezes when phone call comes in
Root cause: Session interrupted but no handling/UI feedback
Diagnostic:
// Check if session is still running after returning from call
print("Session running: \(session.isRunning)")
// Will be false during active call, true after call ends
Fix: Add interruption observers (see camera-capture skill Pattern 5)
Key point: Session AUTOMATICALLY resumes after interruption ends. You don't need to call startRunning() again. Just update your UI.
Time to fix: 30 min
Pattern 6: Split View Camera Unavailable
Symptom: Camera stops working when iPad enters Split View
Root cause: Camera not available with multiple foreground apps
Diagnostic:
// Check interruption reason
// InterruptionReason.videoDeviceNotAvailableWithMultipleForegroundApps
Fix: Show appropriate UI message and resume when user exits Split View:
case .videoDeviceNotAvailableWithMultipleForegroundApps:
showMessage("Camera unavailable in Split View. Use full screen.")
Time to fix: 15 min
Pattern 7: Thermal Pressure
Symptom: Camera stops randomly, especially after prolonged use
Root cause: Device getting hot, system reducing resources
Diagnostic:
// Check thermal state
print("Thermal state: \(ProcessInfo.processInfo.thermalState.rawValue)")
// 0 = nominal, 1 = fair, 2 = serious, 3 = critical
Fix: Reduce quality or show cooling message:
case .videoDeviceNotAvailableDueToSystemPressure:
// Reduce quality
session.sessionPreset = .medium
showMessage("Camera quality reduced due to device temperature")
Time to fix: 20 min
Pattern 8: Preview Rotation Wrong
Symptom: Preview is rotated 90ยฐ from expected
Root cause: Not using RotationCoordinator (iOS 17+) or not observing updates
Diagnostic:
print("Preview connection rotation: \(previewLayer.connection?.videoRotationAngle ?? -1)")
Fix:
// Create and observe RotationCoordinator
let coordinator = AVCaptureDevice.RotationCoordinator(device: camera, previewLayer: previewLayer)
// Set initial rotation
previewLayer.connection?.videoRotationAngle = coordinator.videoRotationAngleForHorizonLevelPreview
// Observe changes
observation = coordinator.observe(\.videoRotationAngleForHorizonLevelPreview) { [weak previewLayer] coord, _ in
DispatchQueue.main.async {
previewLayer?.connection?.videoRotationAngle = coord.videoRotationAngleForHorizonLevelPreview
}
}
Time to fix: 30 min
Pattern 9: Captured Photo Rotation Wrong
Symptom: Preview looks correct, but captured photo is rotated
Root cause: Rotation angle not applied to photo output connection
Diagnostic:
if let connection = photoOutput.connection(with: .video) {
print("Photo connection rotation: \(connection.videoRotationAngle)")
}
Fix:
func capturePhoto() {
// Apply current rotation to capture
if let connection = photoOutput.connection(with: .video) {
connection.videoRotationAngle = rotationCoordinator.videoRotationAngleForHorizonLevelCapture
}
photoOutput.capturePhoto(with: settings, delegate: self)
}
Time to fix: 15 min
Pattern 10: Front Camera Mirroring
Symptom: Designer says "front camera photo doesn't match preview"
Reality: This is CORRECT behavior, not a bug.
Explanation:
- Preview is mirrored (like looking in a mirror - user expectation)
- Captured photo is NOT mirrored (text reads correctly when shared)
- This matches the system Camera app behavior
If business requires mirrored photos (selfie apps):
func mirrorImage(_ image: UIImage) -> UIImage? {
guard let cgImage = image.cgImage else { return nil }
return UIImage(cgImage: cgImage, scale: image.scale, orientation: .upMirrored)
}
Time to fix: 5 min (explanation) or 15 min (if mirroring required)
Pattern 11: Slow Capture (Quality Priority)
Symptom: Photo capture takes 2+ seconds
Root cause: photoQualityPrioritization = .quality (default for some devices)
Diagnostic:
print("Max quality prioritization: \(photoOutput.maxPhotoQualityPrioritization.rawValue)")
// Check what you're requesting in AVCapturePhotoSettings
Fix:
var settings = AVCapturePhotoSettings()
// For fast capture (social/sharing)
settings.photoQualityPrioritization = .speed
// For balanced (general use)
settings.photoQualityPrioritization = .balanced
// Only use .quality when image quality is critical
Time to fix: 5 min
Pattern 12: Deferred Processing
Symptom: Want maximum responsiveness (zero-shutter-lag)
Solution: Enable deferred processing (iOS 17+)
photoOutput.isAutoDeferredPhotoDeliveryEnabled = true
// Then handle proxy in delegate:
// - didFinishProcessingPhoto gives proxy for immediate display
// - didFinishCapturingDeferredPhotoProxy gives final image later
Time to fix: 30 min
Pattern 13: Permission Not Requested
Symptom: authorizationStatus = .notDetermined
Fix:
// Must request before setting up session
Task {
let granted = await AVCaptureDevice.requestAccess(for: .video)
if granted {
setupSession()
}
}
Time to fix: 10 min
Pattern 14: Permission Denied
Symptom: authorizationStatus = .denied
Fix: Show settings prompt
func showSettingsPrompt() {
let alert = UIAlertController(
title: "Camera Access Required",
message: "Please enable camera access in Settings to use this feature.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Settings", style: .default) { _ in
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert, animated: true)
}
Time to fix: 15 min
Pattern 15: API Availability Crash
Symptom: Crash on iOS 16 or earlier
Root cause: Using iOS 17+ APIs without availability check
Fix:
if #available(iOS 17.0, *) {
// Use RotationCoordinator
let coordinator = AVCaptureDevice.RotationCoordinator(device: camera, previewLayer: preview)
} else {
// Fallback to deprecated videoOrientation
if let connection = previewLayer.connection {
connection.videoOrientation = .portrait
}
}
Time to fix: 20 min
Quick Reference Table
| Symptom | Check First | Likely Pattern |
|---|---|---|
| Black preview | Step 1 (session state) | 1, 2, or 3 |
| UI freezes | Step 2 (threading) | 4 |
| Freezes on call | Step 4 (interruptions) | 5 |
| Wrong rotation | Print rotation angle | 8 or 9 |
| Slow capture | Print quality setting | 11 |
| Denied access | Step 3 (permissions) | 14 |
| Crash on old iOS | Check @available | 15 |
Checklist
Before escalating camera issues:
Basics:
- โ Session has at least one input
- โ Session has at least one output
- โ Session isRunning = true
- โ Preview layer connected to session
- โ Preview layer has non-zero frame
Threading:
- โ All session work on sessionQueue
- โ startRunning() on background thread
- โ UI updates on main thread
Permissions:
- โ Authorization status checked
- โ Permission requested if notDetermined
- โ Graceful UI for denied state
Rotation:
- โ RotationCoordinator created with device AND previewLayer
- โ Observation set up for preview angle changes
- โ Capture angle applied when taking photos
Interruptions:
- โ Interruption observer registered
- โ UI feedback for interrupted state
- โ Tested with incoming phone call
Resources
WWDC: 2021-10247, 2023-10105
Docs: /avfoundation/avcapturesession, /avfoundation/avcapturesessionwasinterruptednotification
Skills: axiom-camera-capture, axiom-camera-capture-ref
More by CharlesWiltgen
View allUse when porting OpenGL/DirectX to Metal - translation layer vs native rewrite decisions, migration planning, anti-patterns
Use when UI is slow, scrolling lags, animations stutter, or when asking 'why is my SwiftUI view slow', 'how do I optimize List performance', 'my app drops frames', 'view body is called too often', 'List is laggy' - SwiftUI performance optimization with Instruments 26 and WWDC 2025 patterns
Use for Core Location troubleshooting - no location updates, background location broken, authorization denied, geofence not triggering
Reference โ Comprehensive SwiftUI navigation guide covering NavigationStack (iOS 16+), NavigationSplitView (iOS 16+), NavigationPath, deep linking, state restoration, Tab+Navigation integration (iOS 18+), Liquid Glass navigation (iOS 26+), and coordinator patterns