Skip to main content
Module NeurofitVitals, iOS 15 or later, Swift 5 language mode, SwiftPM tools 5.9, one dynamic library product. The module depends on the PpgCore engine (path dependency ../../ports/swift) through internal import, so no engine type appears in the public interface; that import form needs Xcode 16 or newer to build the SDK from source (the binary xcframework has no such requirement). Every public value type is Sendable and Equatable. Events and delegate callbacks are delivered on the main thread. Engine version 1.3.0. This page is hand-maintained from the SDK sources and updated with every SDK change; the Android page documents the mirrored API.

VitalsSDK

The static entry point. There is no instance.
Notes:
  • activate compares the key’s apps with Bundle.main.bundleIdentifier. A process without a bundle identifier fails closed with .licenseNotValidForApp(appId: "") in Release builds; a Debug build of the SDK sources accepts it with the usual relaxed-app-id warning in LicenseStatus.warnings.
  • The relaxed app-id check (a warning instead of .licenseNotValidForApp) is compiled in with #if DEBUG in the SDK module itself. With the SwiftPM source integration that is your build configuration; the binary xcframework is built Release and never relaxes the check. See License keys.
  • deviceSupport() probes with the default fpsTiers ([60, 30, 20]) and the same format selection the camera configures with, so a reported tier is one the camera reaches.
  • isLoggingEnabled logs state transitions and capture decisions only: never metric values, frames or license contents.

LicenseStatus and LicenseType

DeviceSupport

Exactly five fields.
reason is one of: "No rear wide camera", "The iOS Simulator has no rear camera", "The rear camera has no torch", or "No capture format reaches 20 fps at 640 px" (the lowest tier and the extractor’s minimum width).

CameraAuthorization

VitalsConfiguration

AdvancedConfiguration

Capture and gating parameters with the iOS production defaults. Changing them is unvalidated. The Reading lifecycle page lists both platforms side by side.

Configuration validation

VitalsSession.init validates the configuration and throws VitalsError.internalError("Invalid configuration: <detail>") for a value the capture layer cannot run with, before any camera work:

VitalsSession

One session runs one reading. Create a new session for each reading screen. The class is final and @unchecked Sendable; own it from one place.
start() rechecks activation and the device, requests camera access when it is notDetermined, opens the camera at the tier deviceSupport().fpsTier reports (or the next lower tier that has a format), turns the torch on and arms the reading. It throws: A failed start() leaves the session idle, so the host can try again (after granting camera access, say). On iOS a camera that cannot be opened is reported synchronously by start(); on Android the same condition arrives asynchronously as Failed(CameraUnavailable). Other behaviours:
  • Measuring ticks. While measuring, state is updated to .measuring(elapsed:) about once per second without a .state event; the .preview event carries the same elapsed.
  • Cancel without auto-restart. With autoRestartAfterCancel = false a cancel emits .cancelled, then .state(.idle), turns the torch off and stops the camera; start() may be called again on the same session.
  • Backgrounding. UIApplication.didEnterBackgroundNotification, an AVCaptureSession interruption or a media-services reset while positioning or measuring fails the session with .interrupted(reason:) (for backgrounding the reason is "The app moved to the background"). An interruption that arrives while finalizing is ignored and the result is delivered. Lock the reading screen’s orientation so a rotation cannot interrupt a reading.
  • stop(). While positioning, measuring or finalizing, stop() sets state to .idle and emits .state(.idle); a terminal state is kept. The events stream finishes. The session cannot be restarted after stop(). deinit calls stop().
  • The configuration passed to init is not exposed as a public property on iOS (Android exposes VitalsSession.config).

VitalsSessionState

VitalsEvent

completed and failed arrive twice: as their own event and inside the following .state(...) event.

Guidance

Evaluation order per preview: noContact, then frameDrop, then weakSignal, then lowQuality, else compliant. The SDK ships no strings; see Reading lifecycle for the NEUROFIT app’s copy.

LivePreview

rmssdMs is sticky: once shown it keeps the last good value until the attempt ends. abortRisk is warn from preview 21 of the 35 s discard deadline (60%, rounded up), imminent from preview 30 (the last 5 s), and none as soon as a live estimate clears the gate.

CancelReason and CancelContext

toDictionary() always contains these ten keys: reason (the CancelReason.token), quality_score, elapsed_s, engine ("neurofit_vitals"), refine_mode (a per-platform engine constant), effective_fps, device_model, red_dc, exposure_iso, exposure_tune_steps. Absent numbers are -1 (-1.0 for doubles, -1 for exposure_tune_steps). exposure_in_band is added only when exposureInBand is known. The initializer is internal; the SDK builds the context.

VitalsRecoverableError

The SDK recovers from the first recoverable error in a session (the attempt is abandoned, the camera rebound, the session returns to positioning) and fails on the second, whatever its kind: cameraStalled fails with .cameraUnavailable, frameRateUnsustainable with .unsupportedDevice(reason:). In 1.0.0 iOS emits only .cameraStalled (for an AVCaptureSession runtime error); sustained frame drop uses the tier ladder and .reconfiguringCamera instead.

VitalsError

VitalsResult

Codable encodes camelCase keys; diagnostics encodes as its snake_case dictionary. Field meanings and units are on the Metrics page. The members have no public initializer; results come from the session.

VitalsDiagnostics

Every member is a public let. Codable and toDictionary() use the snake_case keys shown; the optionals are absent (not -1) when the step that produces them did not run.
See Diagnostics for every key’s meaning. The Android record differs in a few keys (est_android_estimator_enabled and est_gm_ratio exist only there; the iOS-only camera-format keys do not).

VitalsChange

Provisional helper for pre/post comparisons; see Outcomes best practices.
A metric’s entry is nil when either reading lacks the metric.

Threading

  • VitalsSDK.activate, licenseStatus, deviceSupport(), cameraAuthorization, isLoggingEnabled, VitalsSession.init, state, delegate, cancelReading and stop are safe from any thread. The activation record is lock-guarded; the SDK has no other mutable statics.
  • start() is async and may be awaited from any context; it suspends through the camera prompt and the camera’s own start.
  • Every event reaches the events stream and the delegate on the main thread, in order, never synchronously from the caller’s thread.
  • Frame extraction runs on the SDK’s capture queue, the reading logic on its processing queue, the engine’s finalize on a separate queue. Nothing blocks the main thread.
  • No DispatchQueue.main.sync anywhere in the SDK.

Logging

VitalsSDK.isLoggingEnabled (default false) turns on os.Logger debug messages under subsystem com.neurofit.vitals, category session: state transitions and capture decisions only. Nothing is formatted or emitted while it is off. No metric values, frames or license contents are ever logged.

Package

Package.swift: tools version 5.9, platforms: [.iOS(.v15)], product .library(name: "NeurofitVitals", type: .dynamic), dependency PpgCore at ../../ports/swift, swiftLanguageVersions: [.v5]. The library bundles PrivacyInfo.xcprivacy (no tracking, no collected data types, no required-reason APIs) and no other resources. Dependencies beyond Apple frameworks: none.