Circadify

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()
)
swift
  • camera — the CircadifyCamera whose session is rendered. Required.
  • showGuide — draw the tracking heat-glow and guide oval. Defaults to true. When true, the preview turns on face tracking for you; set it to false to render a clean preview with no overlay.
  • isScanning — switch the glow into its hotter, faster measuring state. Defaults to false. 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)
            }
    }
}
swift

To 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()
swift
Note

When 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
FieldTypeDefaultDescription
coreColorColor#FF331ARed core of the glow ramp — fully present only at peak scanning intensity.
hotColorColor#FF401FHot orange in the glow ramp.
midColorColor#FF801FMid orange in the glow ramp.
goldColorColor#F2B340Gold — the resting / idle tone of the glow.
ovalColorColor#F2B340Color of the thin oval outline drawn under the glow.
scanRegionDurationDouble2.0Seconds each region stays lit while measuring (faster, hotter).
idleRegionDurationDouble3.5Seconds each region stays lit while idle / framing (slower, calmer).
scanPeakDouble1.0Peak glow intensity (0…1) while measuring.
idlePeakDouble0.4Peak 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()
swift

Custom 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?
swift

It 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`
swift

The 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)
}
swift

All 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() }
    }
}
swift
Tip

The 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