Building a responsive camera app that launches quickly
Show a camera preview sooner by deferring capture output setup and postponing noncritical interface elements.
Overview
Someone who opens your camera app wants to see a preview immediately. A capture session takes time to configure, because it prepares every output you add before it starts delivering data. Showing a preview requires almost none of that work, so it can appear while the session finishes the rest.
AVCam adopts deferred start in CaptureService, the actor that manages its capture pipeline, so the preview doesn’t wait for the photo and movie outputs to start. The app postpones part of its interface for the same reason: controls that depend on a running session stay hidden until one is running.
The same smoothness matters when the app returns from the background. A preview that isn’t covered on the way out comes back holding a stale frame, and covering it after frames stop arriving is too late. AVCam covers the preview as the app leaves, then fades the cover away as live frames resume. For more information about the app’s design, see AVCam: Building a camera app.
Configure the sample code project
Because Simulator doesn’t have access to device cameras, run the sample on a device. To build and run it, you need the following:
Xcode 27 or later
A device running iOS or iPadOS 26 or later
Defer starting noncritical outputs
Preparing capture outputs is one of the more expensive parts of starting a capture session, and rendering a preview requires none of them. A photo or movie output matters only the moment someone takes a picture or starts recording, which is always later than the moment the preview appears. Deferred start separates those two events: the session brings up the preview path first, then prepares the remaining outputs after the preview is onscreen.
Linking the iOS 26 SDK enables both halves of this behavior by default: the session runs deferred start automatically, and a photo output or a file output defers its start whenever it supports deferral. Every other output type stays undeferred, so an output that feeds preview keeps working as it always has. AVCam sets both values explicitly, so the code shows which properties control the behavior.
The CaptureService actor makes the assignments while configuring the session:
// Configure the session preset based on the current capture mode.
captureSession.sessionPreset = captureMode == .photo ? .photo : .high
// Deferred start brings up the preview first, and prepares the capture outputs a moment
// later. An app that links the iOS 26 SDK gets this behavior by default; these
// assignments show the properties that control it.
captureSession.automaticallyRunsDeferredStart = true
// Add the photo capture output as the default output type.
photoCapture.output.isDeferredStartEnabled = photoCapture.output.isDeferredStartSupported
try addOutput(photoCapture.output)
// If the capture mode is set to `video`, add a movie capture output.
if captureMode == .video {
// Add the movie output as the default output type.
movieCapture.output.isDeferredStartEnabled = movieCapture.output.isDeferredStartSupported
try addOutput(movieCapture.output)
setHDRVideoEnabled(isHDRVideoEnabled)
}Each output tests isDeferredStartSupported before enabling isDeferredStartEnabled, because enabling it on an output that doesn’t support deferral throws an exception. Because the property defers starting an output, not adding one, the sample still adds the movie output only in video mode. The code doesn’t touch AVCaptureVideoPreviewLayer, which defaults to not deferring because its job is to display preview.
AVCam uses automatic mode, in which the session chooses when to start the deferred outputs. The preview layer is the only client the app doesn’t defer. The session runs the deferred start shortly after the layer displays its first frame. An app that leaves a data-providing output undeferred waits longer, because the session waits for that output’s first frame instead. An app that draws camera frames itself, such as one rendering AVCaptureVideoDataOutput buffers into a CAMetalLayer, knows that moment when the session can’t. It sets automaticallyRunsDeferredStart to false and calls runDeferredStartWhenNeeded() instead.
Hide controls until the session runs
A mode picker, a thumbnail of the last capture, and a camera-switching button all depend on a running session, so showing them before one exists puts controls onscreen that can’t do anything. AVCam keeps them hidden until the session runs, which also leaves the preview alone onscreen as it appears.
AVCam defines a view modifier for that condition. In the following code example, hidden(until:) fades a view in and out as the session’s state changes:
extension View {
/// Hides this view until the capture session is running.
func hidden(until isRunning: Bool) -> some View {
opacity(isRunning ? 1 : 0)
.animation(.easeIn(duration: 0.3), value: isRunning)
}
}Fading opacity rather than adding and removing the views keeps the layout fixed, so controls don’t shift position as the session comes up.
The main toolbar applies the modifier to the controls that depend on the session, and deliberately doesn’t apply it to one of them:
var body: some View {
HStack {
ThumbnailButton(camera: camera)
.hidden(until: camera.status == .running)
Spacer()
// The capture button stays visible, because a capture that arrives before
// the deferred start finishes is still honored.
CaptureButton(camera: camera)
Spacer()
SwitchCameraButton(camera: camera)
.hidden(until: camera.status == .running)
}
}The capture button stays onscreen throughout. When a shutter press arrives before the deferred outputs finish starting, the session runs the deferred start on the app’s behalf to service it. The photo output also enables responsive capture, which lets more than one press queue before that start completes.
Cover the preview in the background
A preview that isn’t covered can reappear holding the last frame it rendered before the app entered the background. Covering it after frames stop arriving is too late. Instead, the app covers the preview on its way out and reveals it when it’s active again.
In the following code example, PreviewContainer observes the scene phase and sets the opacity of a black overlay from it:
var previewView: some View {
content
.blur(radius: blurRadius, opaque: true)
.overlay(Color.black.opacity(coverOpacity))
.onChange(of: camera.isSwitchingModes, updateBlurRadius(_:_:))
.onChange(of: camera.isSwitchingVideoDevices, updateBlurRadius(_:_:))
.onChange(of: scenePhase, updateCoverOpacity(_:_:))
}
/// Cover the preview as the app enters the background, and reveal it on return.
func updateCoverOpacity(_: ScenePhase, _ phase: ScenePhase) {
switch phase {
case .background:
coverOpacity = 1
case .active:
withAnimation(.easeIn(duration: 0.3)) {
coverOpacity = 0
}
default:
break
}
}The cover appears without animation, because the app doesn’t have time to animate as it enters the background. On return, the app fades the cover out instead of immediately removing it, which smooths the return to a live preview without needing a signal that frames have resumed.
See Also
Capture sessions
Setting up a capture sessionAccessing the camera while multitasking on iPadAVCam: Building a camera appCapturing Cinematic videoSupporting Center Stage front camera in your iOS appAVMultiCamPiP: Capturing from Multiple CamerasAVCamBarcode: detecting barcodes and facesRegistering a camera capture accessory on iPhone DuoAVCaptureSessionAVCaptureMultiCamSessionAVCaptureInputAVCaptureOutputAVCaptureConnection