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.
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.
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).
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.
try await Visualize.prepare(sessionToken: token) { progress in
// .downloadingModel(fractionComplete:), .unlockingModel, .loadingModel
}| Behavior | Detail |
|---|---|
| One-time | The bundle is cached on the device after the first download. Later scans skip it; a resumable download survives interruptions mid-transfer. |
| Cellular | NetworkingPolicy.allowsCellularModelDownload defaults to Wi-Fi only. |
| Encrypted | The 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. |
| Updates | When Visualize publishes a new model version, devices pick it up on their next scan automatically — no app release needed. |
| Warm scans | Models stay loaded for the app run — the second scan starts immediately. |
| Offline | A 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:
// 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.
isDeviceSupported is your signal.