Scan Overlay
The Circadify iOS SDK's thermal heat-glow scan overlay — show it, theme it, or draw your own.
CircadifyCameraPreview draws a branded scan overlay on top of the live camera feed: a warm, thermal heat-glow that tracks the user's face while they frame themselves and pulses harder once a measurement is running. It gives the scan screen a recognizable look and shows the user that their face is being detected, without any extra wiring on your part.
What it looks like
The overlay sits on top of the camera preview and follows the detected face in real time:
- A thin guide oval marks where the face should sit in frame.
- A heat-glow blooms over the forehead and both cheeks — the regions the measurement actually samples — in a warm gold → orange → red ramp.
- It pulses. While the user is just framing themselves, the glow breathes slowly in a calm gold tone. Once a measurement starts, it runs hotter and faster, ramping toward a red core to signal that capture is underway.
The face geometry comes from the same on-device FaceLandmarker the measurement pipeline uses, so the glow lands exactly where the scan is reading.
Showing the overlay
The overlay is part of CircadifyCameraPreview — you don't add a separate view. It is on by default; the only thing you typically drive is whether a scan is in progress.
public init(
camera: CircadifyCamera,
showGuide: Bool = true,
isScanning: Bool = false,
style: CircadifyScanStyle = CircadifyScanStyle()
)swiftcamera— theCircadifyCamerawhose session is rendered. Required.showGuide— draw the tracking heat-glow and guide oval. Defaults totrue. Whentrue, the preview turns on face tracking for you; set it tofalseto render a clean preview with no overlay.isScanning— switch the glow into its hotter, faster measuring state. Defaults tofalse. Bind this to your own "is a measurement running" flag.style— overrides the glow colors and timings. Defaults to the Circadify thermal palette. See Theming.
Drive isScanning from the same state you use to gate your scan button, so the glow heats up exactly while measureVitals(camera:) is running:
import SwiftUI
import CircadifySDK
struct ScanView: View {
@State private var camera = CircadifyCamera()
@State private var isScanning = false
private let sdk = try! CircadifySDK(apiKey: "ck_live_your_key_here")
var body: some View {
CircadifyCameraPreview(camera: camera, showGuide: true, isScanning: isScanning)
.ignoresSafeArea()
.task { try? await camera.start() }
.onDisappear { camera.stop() }
.overlay(alignment: .bottom) {
Button(isScanning ? "Scanning…" : "Start scan") {
Task {
isScanning = true
defer { isScanning = false }
_ = try? await sdk.measureVitals(camera: camera)
}
}
.disabled(isScanning)
}
}
}swiftTo hide the overlay entirely — for instance to render your own UI on top of a plain preview — pass showGuide: false:
CircadifyCameraPreview(camera: camera, showGuide: false)
.ignoresSafeArea()swiftWhen showGuide is true, the preview calls camera.startFaceTracking() on appear and camera.stopFaceTracking() on disappear automatically. You only need to manage tracking yourself when you build a custom overlay with showGuide: false.
Theming
Pass a CircadifyScanStyle to recolor the glow or change how it pulses. Every field has a default that matches the Circadify thermal palette, so override only what you want to change.
public struct CircadifyScanStyle: Sendable {
public var coreColor: Color // #FF331A — red core, only at peak intensity
public var hotColor: Color // #FF401F — hot orange
public var midColor: Color // #FF801F — mid orange
public var goldColor: Color // #F2B340 — resting / idle tone
public var ovalColor: Color // #F2B340 — the thin oval outline
public var scanRegionDuration: Double // seconds each region stays lit while measuring
public var idleRegionDuration: Double // seconds each region stays lit while idle / framing
public var scanPeak: Double // peak glow intensity (0…1) while measuring
public var idlePeak: Double // peak glow intensity (0…1) while idle / framing
}swift| Field | Type | Default | Description |
|---|---|---|---|
coreColor | Color | #FF331A | Red core of the glow ramp — fully present only at peak scanning intensity. |
hotColor | Color | #FF401F | Hot orange in the glow ramp. |
midColor | Color | #FF801F | Mid orange in the glow ramp. |
goldColor | Color | #F2B340 | Gold — the resting / idle tone of the glow. |
ovalColor | Color | #F2B340 | Color of the thin oval outline drawn under the glow. |
scanRegionDuration | Double | 2.0 | Seconds each region stays lit while measuring (faster, hotter). |
idleRegionDuration | Double | 3.5 | Seconds each region stays lit while idle / framing (slower, calmer). |
scanPeak | Double | 1.0 | Peak glow intensity (0…1) while measuring. |
idlePeak | Double | 0.4 | Peak glow intensity (0…1) while idle / framing. |
// A cooler, calmer overlay
let style = CircadifyScanStyle(
coreColor: Color(red: 0.20, green: 0.55, blue: 1.0),
hotColor: Color(red: 0.25, green: 0.65, blue: 1.0),
midColor: Color(red: 0.40, green: 0.80, blue: 1.0),
goldColor: Color(red: 0.70, green: 0.90, blue: 1.0),
idlePeak: 0.3
)
CircadifyCameraPreview(camera: camera, isScanning: isScanning, style: style)
.ignoresSafeArea()swiftCustom overlay
If you want a completely different look, render your own overlay and read the live face geometry off the camera. CircadifyCamera publishes the current face as an observable scanFace:
public private(set) var scanFace: CircadifyScanFace?swiftIt is nil when no face is detected or tracking is off, and updates on the main actor as the face moves — so a SwiftUI view that reads camera.scanFace redraws automatically. When you draw your own overlay (and pass showGuide: false so the built-in glow is hidden), turn tracking on and off yourself:
public func startFaceTracking() // begin updating `scanFace`
public func stopFaceTracking() // stop tracking and clear `scanFace`swiftThe geometry itself is a CircadifyScanFace:
public struct CircadifyScanFace: Sendable, Equatable {
public var forehead: CGPoint // glow anchor over the forehead
public var leftCheek: CGPoint // glow anchor over the subject's left cheek
public var rightCheek: CGPoint // glow anchor over the subject's right cheek
public var ovalCenter: CGPoint // center of the face oval
public var ovalSize: CGSize // size of the face oval
public var imageAspect: CGFloat // aspect ratio (width / height) of the detection image
public var isMirrored: Bool // true when the preview is mirrored (front camera)
}swiftAll points and sizes are normalized to [0, 1] in the detector's upright, unmirrored image space, with a top-left origin — the same space the measurement pipeline reads from. To draw them over the preview you project them into view coordinates yourself, applying aspect-fill using imageAspect and flipping horizontally when isMirrored is true (the front camera shows a mirrored selfie view).
struct CustomScanOverlay: View {
let camera: CircadifyCamera
var body: some View {
GeometryReader { geo in
if let face = camera.scanFace {
// forehead / leftCheek / rightCheek are normalized [0,1]; map into `geo.size`,
// applying aspect-fill via face.imageAspect and mirroring when face.isMirrored.
Circle()
.fill(.orange.opacity(0.5))
.frame(width: 80, height: 80)
.position(project(face.forehead, in: geo.size, face: face))
}
}
.allowsHitTesting(false)
.onAppear { camera.startFaceTracking() }
.onDisappear { camera.stopFaceTracking() }
}
}swiftThe three glow anchors (forehead, leftCheek, rightCheek) mark the regions the measurement actually samples. Anchoring your own overlay to them keeps it visually aligned with what the scan reads.
Next Steps
- Methods —
CircadifyCameraPreviewand the full method reference - Configuration — Camera setup and measurement options
- Camera & Permissions — Bring the preview live