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.activatecompares the key’sappswithBundle.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 inLicenseStatus.warnings.- The relaxed app-id check (a warning instead of
.licenseNotValidForApp) is compiled in with#if DEBUGin 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 defaultfpsTiers([60, 30, 20]) and the same format selection the camera configures with, so a reported tier is one the camera reaches.isLoggingEnabledlogs 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 isfinal 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,
stateis updated to.measuring(elapsed:)about once per second without a.stateevent; the.previewevent carries the sameelapsed. - Cancel without auto-restart. With
autoRestartAfterCancel = falsea 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, anAVCaptureSessioninterruption 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()setsstateto.idleand emits.state(.idle); a terminal state is kept. The events stream finishes. The session cannot be restarted afterstop().deinitcallsstop(). - The configuration passed to
initis not exposed as a public property on iOS (Android exposesVitalsSession.config).
VitalsSessionState
VitalsEvent
completed and failed arrive twice: as their own event and inside the following .state(...) event.
Guidance
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
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 apublic 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.
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.nil when either reading lacks the metric.
Threading
VitalsSDK.activate,licenseStatus,deviceSupport(),cameraAuthorization,isLoggingEnabled,VitalsSession.init,state,delegate,cancelReadingandstopare safe from any thread. The activation record is lock-guarded; the SDK has no other mutable statics.start()isasyncand may be awaited from any context; it suspends through the camera prompt and the camera’s own start.- Every event reaches the
eventsstream 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.syncanywhere 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.