> ## 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`. The SDK yields every event on the main thread, in order. A `for await` loop runs on its task's
executor, so iterate `events` from the main actor (for example a `Task` started inside a `@MainActor` type) to
handle events 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
    building the sources 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
    `NeurofitVitals` product is a dynamic library. To link the module statically into your app target instead (no
    framework to embed), use the `NeurofitVitalsStatic` product. Link one of the two, not both. There are no
    third-party dependencies: the binary xcframework has the engine built in, and the source package also needs
    the bundled `PpgCore` engine package (`ppgcore/ports/swift`) next to it.
  </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. It throws `VitalsError.licenseInvalid(reason:)` 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 {
      try VitalsSDK.activate(licenseKey: Secrets.neurofitVitalsLicenseKey)
    } catch {
      // VitalsError.licenseInvalid(reason:); the reason names the check that failed.
      print("Activation failed: \(error.localizedDescription)")
    }
    ```

    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. See [License keys](/license-keys).
  </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, or no capture format at a usable frame rate.
      return
    }

    if !VitalsSDK.hasCameraPermission {
      let granted = await VitalsSDK.requestCameraPermission()
      guard granted else {
        // Declined now or earlier (iOS asks only once), or blocked by a device policy. Offer Settings.
        return
      }
    }
    ```

    `start()` also prompts when access has not been asked yet, 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. Consume `events` as an `AsyncStream`. The SDK
    yields every event on the main thread, in order. A `for await` loop runs on its task's executor, so iterate
    `events` from the main actor (for example a `Task` started inside a `@MainActor` type) to handle events on the
    main thread. The model below does exactly that.

    ```swift theme={null}
    @MainActor
    final class ReadingModel: ObservableObject {
      @Published var guidance: Guidance = .noContact
      @Published var startProgress: Double = 0
      @Published var preview: LivePreview?
      @Published var notice: String?
      @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()
          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(message: String(describing: error))
        }
      }

      private func handle(_ event: VitalsEvent) {
        switch event {
        case .guidance(let guidance, let startProgress):
          self.guidance = guidance
          self.startProgress = startProgress
        case .started:
          notice = nil
          preview = nil   // a preview at progress 0 follows at once
        case .preview(let preview):
          // preview.rmssdMs is nil until about 20 s. preview.signalLevel is ready to display as it is,
          // and nil until the reading's first scored preview.
          self.preview = preview
        case .heartbeat:
          // Optional: a light haptic per detected beat.
          break
        case .cancelled(let reason):
          // The session returns to positioning and starts again by itself once the fingertip is steady.
          notice = reason == .contactLost ? "Keep your fingertip on the camera." : "Let's try that again."
        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
        @unknown default:
          // An event added in a later SDK version.
          break
        }
      }

      func cancelAttempt() {
        session?.cancelReading()   // no-op unless a reading is measuring
      }

      func stop() {
        // The events stream ends by itself after stop(). A session stopped after measurementComplete
        // delivers its result first, so the loop above still receives it.
        session?.stop()
      }
    }
    ```

    The binary framework is built with library evolution, so a switch over an SDK enum needs an
    `@unknown default` case (Swift 6 language mode makes a missing one an error).

    `VitalsSession()` throws `.notActivated` or `.unsupportedDevice(reason:)`. `start()` turns on the camera and
    torch and begins positioning. It throws `.notActivated`, `.cameraPermissionDenied`, `.cameraUnavailable`,
    `.unsupportedDevice(reason:)`, or `.internalError(message:)` when the session is already running or has
    ended. A failed `start()` leaves the session as it was, so you can call it again after fixing the cause.
  </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) {
      guard result.quality != .withheld else {
        // HRV metrics may be absent. Offer a retry rather than showing numbers.
        return
      }
      let hr = "\(Int(result.heartRateBpm.rounded())) bpm"
      let rmssd = result.rmssdMs.map { "\(Int($0.rounded())) ms" } ?? "n/a"
      var rr = result.breathingRateBrpm.map { String(format: "%.1f brpm", $0) } ?? "n/a"
      if result.breathingRateConfidence == .low { rr = "about \(rr)" }  // a weaker estimate
      print("HR \(hr), HRV (RMSSD) \(rmssd), breathing \(rr)")
    }
    ```

    `VitalsResult` is `Codable`, so you can store or upload it as JSON. Field meanings, units, how to treat the
    `withheld` tier and what the breathing-rate confidence tiers mean 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. Once `.measurementComplete` has arrived, a
  stopped session still delivers `.completed` (or `.failed(.noHeartRate)`) and keeps itself alive until it has,
  so you may drop your reference; keep its event loop running if you want the result.
* Create a new `VitalsSession` for each reading screen. A completed or failed session does not restart.
* Backgrounding, a phone call, Split View, another app taking the camera or a media-services reset does not end
  the session. A reading in progress is cancelled with `.cancelled(.interrupted)` and the session resumes
  positioning by itself when the cause clears. Lock the reading screen's orientation while a session runs.
* To keep the camera positioning without starting a reading (an intro or a sheet covers the screen, say), set
  `session.isAutoStartEnabled = false`; set it back to `true` when the user is ready. A reading already measuring
  is unaffected.
* Keep the phone still and the screen on for the duration. Consider `UIApplication.shared.isIdleTimerDisabled`
  while a session is running.

## Running the package tests

With a source license, 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-Package \
  -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 event and guidance value, with suggested copy.
* [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.