> ## Documentation Index
> Fetch the complete documentation index at: https://developer.neurofit.app/llms.txt
> Use this file to discover all available pages before exploring further.

# iOS quickstart

> Add the NeurofitVitals Swift package, activate your license, request camera access and run a reading.

This page takes you from an empty project to a completed reading (heart rate, HRV (RMSSD), breathing rate and
related measures) on iOS 15 or later. The SDK is a Swift module named `NeurofitVitals`. All public types are
`Sendable`, and events are delivered on the main thread.

<Steps>
  <Step title="Add the package">
    **Binary license.** Your delivery contains `NeurofitVitals.xcframework` (and a zip with its SwiftPM
    checksum). Either drag it into your app target (Frameworks, Libraries and Embedded Content, set to Embed and
    Sign), or reference it from a local Swift package with a `binaryTarget`:

    ```swift Package.swift theme={null}
    .binaryTarget(name: "NeurofitVitals", path: "Vendor/NeurofitVitals.xcframework")
    ```

    **Source license.** Add the SDK folder as a local package dependency and link the `NeurofitVitals` product.
    The package depends on the `PpgCore` engine package that ships next to it (`ppgcore/ports/swift`), and the
    sources use access-level imports (`internal import PpgCore`), so building them needs **Xcode 16 or newer**.

    ```swift Package.swift theme={null}
    dependencies: [
      .package(path: "../neurofit-vitals-sdk/ppgcore/sdk/ios")
    ],
    targets: [
      .target(name: "YourApp", dependencies: [
        .product(name: "NeurofitVitals", package: "ios")
      ])
    ]
    ```

    The package manifest uses Swift tools 5.9, `platforms: [.iOS(.v15)]` and Swift 5 language mode; the product
    is a dynamic library. There are no dependencies beyond Apple frameworks.
  </Step>

  <Step title="Declare camera usage">
    Add `NSCameraUsageDescription` to your `Info.plist`. iOS shows this text in the permission prompt.

    ```xml Info.plist theme={null}
    <key>NSCameraUsageDescription</key>
    <string>Measure your heart rate and HRV with a 60-second fingertip reading over the rear camera.</string>
    ```

    The SDK samples the accelerometer through Core Motion to help estimate breathing rate. That does not show a
    permission prompt; adding `NSMotionUsageDescription` is still recommended, and name the accelerometer in your
    consent copy. See [Privacy and compliance](/privacy-compliance).

    ```xml Info.plist theme={null}
    <key>NSMotionUsageDescription</key>
    <string>Your phone's motion sensor helps follow your breathing during a reading.</string>
    ```
  </Step>

  <Step title="Activate the SDK">
    Call `activate(licenseKey:)` once, early in the app's life, before you create a session. Verification is
    offline and fast; call it synchronously at launch. The call throws when the key is malformed, signed by
    another key, issued for a different bundle identifier, or does not cover this SDK build's date.

    ```swift theme={null}
    import NeurofitVitals

    do {
      let status = try VitalsSDK.activate(licenseKey: Secrets.neurofitVitalsLicenseKey)
      print("Licensed to \(status.licensee), updates until \(status.updatesUntil)")
      for warning in status.warnings { print("License warning: \(warning)") }
    } catch let error as VitalsError {
      // .licenseInvalid, .licenseNotValidForApp(appId:), .licenseUpdatesExpired(updatesUntil:)
      print("Activation failed: \(error.code): \(error.localizedDescription)")
    } catch {
      print("Activation failed: \(error)")
    }
    ```

    With the binary xcframework the key must list your bundle identifier, including any development identifier
    you run under; only a Debug build of the SDK sources relaxes that check to a warning. See
    [License keys](/license-keys) for the key format, `updatesUntil` and the debug behaviour.
  </Step>

  <Step title="Check device support and camera access">
    ```swift theme={null}
    let support = VitalsSDK.deviceSupport()
    guard support.isSupported else {
      // support.reason explains why: no rear camera, no torch, or no capture format at a usable frame rate.
      return
    }

    switch VitalsSDK.cameraAuthorization {
    case .authorized:
      break
    case .notDetermined:
      let granted = await VitalsSDK.requestCameraAccess()
      guard granted else { return }
    case .denied, .restricted:
      // Send the user to Settings; start() would throw .cameraDenied / .cameraRestricted.
      return
    }
    ```

    `start()` also prompts when access is not yet determined, so the explicit request is for your own flow (a
    rationale screen before the system prompt).
  </Step>

  <Step title="Create a session and listen for events">
    A `VitalsSession` owns the camera, the torch and one reading at a time. Consume `events` as an `AsyncStream`,
    or set a `delegate` if you prefer callbacks. Both deliver on the main thread.

    ```swift theme={null}
    @MainActor
    final class ReadingModel: ObservableObject {
      @Published var guidance: Guidance = .noContact
      @Published var startProgress: Double = 0
      @Published var preview: LivePreview?
      @Published var result: VitalsResult?
      @Published var failure: VitalsError?

      private var session: VitalsSession?
      private var eventTask: Task<Void, Never>?

      func begin() async {
        do {
          let session = try VitalsSession(configuration: VitalsConfiguration())
          self.session = session
          eventTask = Task { [weak self] in
            for await event in session.events {
              self?.handle(event)
            }
          }
          try await session.start()
        } catch let error as VitalsError {
          failure = error
        } catch {
          failure = .internalError(String(describing: error))
        }
      }

      private func handle(_ event: VitalsEvent) {
        switch event {
        case .guidance(let guidance, let startProgress):
          self.guidance = guidance
          self.startProgress = startProgress
        case .started:
          startProgress = 1
          preview = nil
        case .preview(let preview):
          // preview.rmssdMs is nil until about 20 s; preview.breathingRate until about 30 s.
          self.preview = preview
        case .heartbeat:
          // Optional: a light haptic per detected beat.
          break
        case .cancelled(let reason, _):
          // The session returns to positioning and re-arms by itself. Show a short note if you like.
          print("Reading restarted: \(reason.token)")
        case .reconfiguringCamera(let lowPowerMode):
          // iOS drops to a lower frame-rate tier in place. Tell the user to turn off Low Power Mode when true.
          print("Reconfiguring camera, low power mode: \(lowPowerMode)")
        case .recoverableError(let kind):
          // One automatic recovery per session; a second recoverable error fails the session.
          print("Recovering from \(kind)")
        case .measurementComplete:
          // 60 s captured; the result follows within a few seconds.
          break
        case .completed(let result):
          self.result = result
        case .failed(let error):
          failure = error
        case .state:
          // Transitions only; the measuring clock is in preview.elapsed (and session.state), not here.
          break
        }
      }

      func cancelAttempt() {
        session?.cancelReading(reason: "user_tapped_cancel")   // no-op unless measuring
      }

      func stop() {
        eventTask?.cancel()
        session?.stop()
      }
    }
    ```

    `start()` turns on the camera and torch and moves the session to `positioning`. It throws `.alreadyRunning`
    if called while running, `.notActivated` if activation was lost, `.cameraDenied`, `.cameraRestricted` or
    `.cameraUnavailable` for camera problems, `.unsupportedDevice(reason:)` when the device cannot sustain a
    reading, and `.internalError` on a session that was stopped or has finished. A failed `start()` leaves the
    session idle, so you can call it again after fixing the cause. `VitalsSession.init` itself throws
    `.notActivated`, `.unsupportedDevice(reason:)` or `.internalError("Invalid configuration: ...")`.
  </Step>

  <Step title="Show the camera preview (optional)">
    The session exposes an `AVCaptureVideoPreviewLayer` you can place in your own view. Many apps show a small
    red circle of the fingertip so the user sees their pulse in the glow.

    ```swift theme={null}
    final class PreviewView: UIView {
      func attach(_ session: VitalsSession) {
        guard let layer = session.previewLayer else { return }
        layer.videoGravity = .resizeAspectFill
        layer.frame = bounds
        self.layer.addSublayer(layer)
      }
    }
    ```
  </Step>

  <Step title="Use the result">
    ```swift theme={null}
    func show(_ result: VitalsResult) {
      switch result.quality {
      case .withheld:
        // Metrics may be nil. Offer a retry rather than showing numbers.
        return
      case .usable, .clean:
        break
      }
      let hr = result.heartRateBpm.map { "\(Int($0.rounded())) bpm" } ?? "n/a"
      let rmssd = result.rmssdMs.map { "\(Int($0.rounded())) ms" } ?? "n/a"
      let rr = result.breathingRateBrpm.map { String(format: "%.1f brpm", $0) } ?? "n/a"
      let rrNote = (result.breathingRateConfidence ?? 0) < 0.5 ? " (estimate)" : ""
      print("HR \(hr), HRV (RMSSD) \(rmssd), breathing \(rr)\(rrNote)")
    }
    ```

    `VitalsResult` is `Codable`, so you can store or upload it as JSON. Field meanings, units and how to treat
    the `withheld` tier are on the [Metrics](/metrics) page.
  </Step>
</Steps>

## Guidance copy

`Guidance` is an enum; the SDK does not ship strings. Map each case to your own localized copy. The NEUROFIT
app's copy is listed on the [Reading lifecycle](/reading-lifecycle) page as a starting point.

## Lifecycle notes

* Call `stop()` when your reading screen disappears. It is idempotent, turns the torch off and releases the
  camera. A stopped session cannot be restarted; create a new one.
* Create a new `VitalsSession` for each reading screen. A completed or failed session does not restart. The one
  exception is a cancel with `autoRestartAfterCancel = false`: the session goes back to `idle` with the camera off
  and `start()` works again.
* The reading needs the app in the foreground. Backgrounding, a phone call, another app taking the camera or a
  media-services reset while positioning or measuring ends the session with `.failed(.interrupted(reason:))`
  (for backgrounding the reason is "The app moved to the background"); recreate the session when the user
  returns. Lock the reading screen's orientation while a session runs.
* Keep the phone still and the screen on for the duration. Consider `UIApplication.shared.isIdleTimerDisabled`
  while a session is running.
* For debugging, `VitalsSDK.isLoggingEnabled = true` logs state transitions to `os.Logger` (subsystem
  `com.neurofit.vitals`); it is off by default and never logs values or frames.

## Running the package tests

The package's unit tests run on the Simulator (no camera needed). Pick the simulator by id, because a name such as
`iPhone 15` can match several installed runtimes:

```bash theme={null}
cd ppgcore/sdk/ios
xcodebuild -scheme NeurofitVitals \
  -destination "id=$(xcrun simctl list devices available | grep -m1 'iPhone 15 (' | sed -E 's/.*\(([0-9A-F-]+)\).*/\1/')" \
  test
```

## Next steps

* [Reading lifecycle](/reading-lifecycle): every state, event and guidance value.
* [Metrics](/metrics): definitions, units and validated accuracy.
* [API reference (iOS)](/api-reference-ios): every public type.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.