Scanning

Two integration depths: present the full member experience with one call, or run the scan and own the UI around it.

The member home (recommended)

One surface with everything: preparation, the scan flow, results, history, trends, and the co-branded share card. The title defaults to your app's display name; pass brandName or brandLogo to control the branding.

swift
Visualize.presentHome(
    from: viewController,
    brandName: "Acme Fitness",          // optional
    brandLogo: UIImage(named: "logo"),  // optional — on the share card
    subject: nil                        // nil → SDK collects details once
) {
    try await myBackend.mintSessionToken()
}

Can this device scan?

Gate your scan entry point on device support — capture needs the TrueDepth camera. Add NSCameraUsageDescription to your Info.plist (see the Quickstart configuration); without it iOS terminates the app at capture.

swift
if await Visualize.isDeviceSupported {
    // show the scan button
}
// In the Simulator this is false: your app builds and every screen
// works there, but capture itself needs the hardware.

Direct scanning

startScan presents capture and returns the result to your code. Pass a ScanSubject to skip the SDK's demographics screen when you already know the member — initializers exist for metric (heightCm/weightKg) and imperial (heightInches/weightPounds).

swift
let subject = ScanSubject(
    gender: .female, heightCm: 168, weightKg: 62, ageYears: 29)

let result = try await Visualize.startScan(
    sessionToken: token,
    subject: subject,
    presentingFrom: viewController)

result.measurements.bodyFatPercent
result.measurements.girths.waistCm
// Computed on this device. Nothing here was sent to Visualize.

Preparation and the model download

The first scan on a device downloads the ~180 MB model bundle, one time. Call prepare at onboarding or sign-in to move that wait out of the first scan; either way the SDK overlaps loading with its first screens, so members are rarely blocked.

swift
try await Visualize.prepare(sessionToken: token) { progress in
    // .downloadingModel(fractionComplete:), .unlockingModel, .loadingModel
}
BehaviorDetail
One-timeThe bundle is cached on the device after the first download. Later scans skip it; a resumable download survives interruptions mid-transfer.
CellularNetworkingPolicy.allowsCellularModelDownload defaults to Wi-Fi only.
EncryptedThe bundle is encrypted at rest and unlocked per scan session by an attested device — the decryption key is never stored on the phone. Members and your code never see any of this.
UpdatesWhen Visualize publishes a new model version, devices pick it up on their next scan automatically — no app release needed.
Warm scansModels stay loaded for the app run — the second scan starts immediately.
OfflineA finished scan returns its result even offline; the usage report queues and drains on reconnect. Members never lose a result to connectivity.

Draining queued reports

A scan finished offline returns its result immediately; the usage report it owes queues on the device. Drain the queue when your app comes to the foreground — mint a fresh token for it:

swift
// e.g. on scenePhase == .active
if await Visualize.pendingReportCount > 0 {
    let token = try await myBackend.mintSessionToken()
    let drained = await Visualize.flushPendingReports(sessionToken: token)
}

Owning the member's history

By default the SDK stores completed scans on the device, and its history and trend screens read from that store. To own storage yourself, pass your own ScanPersisting implementation at configure(persistence:) — the SDK's screens then read from yours. Either way, measurements never reach Visualize unless you enable results delivery.

Real capture needs a Face ID iPhone (TrueDepth camera). On the Simulator your app builds and every screen works, and the SDK stubs device attestation so configuration succeeds — but a scan itself needs the hardware. isDeviceSupported is your signal.