Skip to main content

Overview

The Synheart Session Swift SDK provides a native SessionEngine that captures heart rate data, computes session metrics on-device, and optionally fuses behavioral signals alongside HR data. It runs on iOS, macOS, watchOS, and tvOS. The engine is timer-driven, emitting session frames at configurable intervals and delivering a session summary when complete. The SDK supports pluggable BiosignalProvider (for HR sources) and BehaviorProvider (for behavioral signals).
The SDK computes flat session metrics (HR, HRV, motion) on-device. When the SynheartFlux.xcframework binary is linked, Flux produces normalized HSI 1.0 output internally. A built-in HsiBuilder serves as a fallback when the Flux binary is not linked (e.g., in unit tests).

Installation

Swift Package Manager

Add to your Package.swift:
dependencies: [
    .package(url: "https://github.com/synheart-ai/synheart-session-swift.git", from: "0.2.0")
]
Or in Xcode:
  1. File > Add Packages…
  2. Enter: https://github.com/synheart-ai/synheart-session-swift.git

Requirements

  • iOS 13.0+ / macOS 10.15+ / watchOS 6.0+ / tvOS 13.0+
  • Swift 5.9+
  • Xcode 15.0+

Quick Start

import SynheartSession

let engine = SessionEngine()

let config = SessionConfig(
    sessionId: UUID().uuidString,
    mode: .focus,
    durationSec: 30,
    profile: ComputeProfile(windowSec: 10, emitIntervalSec: 3)
)

try engine.start(config: config) { event in
    let type = event["type"] as? String ?? ""

    switch type {
    case "session_started":
        print("Session started")

    case "session_frame":
        let seq = event["seq"] as? Int ?? 0
        if let metrics = event["metrics"] as? [String: Any] {
            let hr = metrics["hr_mean_bpm"] as? Double ?? 0
            print("#\(seq) HR: \(hr) bpm")
        }

    case "session_summary":
        let duration = event["duration_actual_sec"] as? Int ?? 0
        print("Session complete: \(duration)s")

    default:
        break
    }
}

// Stop early
try engine.stop(sessionId: config.sessionId)

Types

SessionConfig

let config = SessionConfig(
    sessionId: UUID().uuidString,      // unique session identifier
    mode: .focus,                       // .focus or .breathing
    durationSec: 60,                   // session duration in seconds
    profile: ComputeProfile(
        windowSec: 10,                 // sliding window size
        emitIntervalSec: 3             // frame emission interval
    ),
    windowLabel: "morning-check"       // optional label
)
Set includeRawSamples: true to enable raw biosignal streaming alongside session frames:
let config = SessionConfig(
    sessionId: UUID().uuidString,
    mode: .focus,
    durationSec: 60,
    profile: ComputeProfile(windowSec: 10, emitIntervalSec: 3, rawEmitIntervalSec: 1),
    includeRawSamples: true
)
You can also create a SessionConfig from a dictionary, which is how the WatchConnectivity relay deserializes commands from the phone:
let config = try SessionConfig(from: [
    "session_id": "abc-123",
    "mode": "focus",
    "duration_sec": 30,
    "profile": ["window_sec": 10, "emit_interval_sec": 3]
])

SessionMode

ValueRawDescription
.focus"focus"Steady-state measurement
.breathing"breathing"Guided breathing with HRV focus

ComputeProfile

ParameterTypeDefaultDescription
windowSecInt60Sliding window size in seconds
emitIntervalSecInt5Frame emission interval in seconds
rawEmitIntervalSecInt?nilRaw biosignal frame interval (defaults to emitIntervalSec)

SessionError

CaseRawDescription
.permissionDenied"permission_denied"Health data permissions not granted
.sensorUnavailable"sensor_unavailable"Sensor not available
.invalidState"invalid_state"Invalid engine state
.lowBattery"low_battery"Device battery too low
.osTerminated"os_terminated"OS terminated the session

Event Dictionary Format

The SessionEngine callback receives [String: Any] dictionaries with a "type" key:

session_started

["type": "session_started", "session_id": "...", "started_at_ms": 1700000000000]

session_frame

Contains flat metrics computed over the sliding window:
["type": "session_frame", "session_id": "...", "seq": 1,
 "emitted_at_ms": 1700000003000,
 "metrics": [
    "hr_mean_bpm": 72.3,
    "hr_sdnn_ms": 45.12,
    "rmssd_ms": 36.10,
    "sample_count": 60,
    "start_ms": 1700000000000,
    "end_ms": 1700000060000,
    "motion_rms_g": 1.02,        // optional, when accelerometer available
    "motion_sample_count": 125,   // optional
    "active_energy_kcal": 12.5    // optional, iOS only
 ]]
To extract metrics:
if let metrics = event["metrics"] as? [String: Any] {
    let hr = metrics["hr_mean_bpm"] as? Double ?? 0
    let rmssd = metrics["rmssd_ms"] as? Double ?? 0
    let motionRms = metrics["motion_rms_g"] as? Double  // nil if no accelerometer
}

biosignal_frame

Emitted when includeRawSamples is true. Contains raw biosignal samples for live display:
["type": "biosignal_frame", "session_id": "...", "seq": 1,
 "emitted_at_ms": 1700000003000,
 "samples": [
    ["timestamp_ms": 1700000002000, "bpm": 72.3,
     "rr_interval_ms": 829.9,
     "accelerometer": ["x": 0.01, "y": -0.02, "z": 1.0]],
    ...
 ]]
To extract the latest BPM:
case "biosignal_frame":
    if let samples = event["samples"] as? [[String: Any]],
       let latest = samples.last {
        let bpm = latest["bpm"] as? Double ?? 0
        print("Live BPM: \(bpm)")
    }

session_summary

["type": "session_summary", "session_id": "...",
 "duration_actual_sec": 30, "metrics": ["hr_mean_bpm": 72.3, ...]]

WatchOS Integration

The Swift SDK is designed to run on Apple Watch. A typical setup uses PhoneSessionRelay on the watch to receive commands from the phone and run SessionEngine locally.

Watch-Side Relay

import WatchConnectivity
import SynheartSession

class PhoneSessionRelay: NSObject, WCSessionDelegate, ObservableObject {
    private let engine = SessionEngine()

    @Published var isRemoteRunning = false
    @Published var lastHr: Double = 0

    override init() {
        super.init()
        guard WCSession.isSupported() else { return }
        WCSession.default.delegate = self
        WCSession.default.activate()
    }

    func session(_ session: WCSession,
                 didReceiveMessage message: [String: Any]) {
        guard let command = message["command"] as? String else { return }

        switch command {
        case "start_session":
            do {
                let config = try SessionConfig(from: message)
                try engine.start(config: config) { [weak self] event in
                    self?.sendToPhone(event)
                    // Update @Published state for local UI
                }
            } catch {
                // Send error back to phone
            }

        case "stop_session":
            if let sessionId = message["session_id"] as? String {
                try? engine.stop(sessionId: sessionId)
            }

        default: break
        }
    }

    private func sendToPhone(_ event: [String: Any]) {
        guard WCSession.default.isReachable else { return }
        WCSession.default.sendMessage(event, replyHandler: nil)
    }

    // Required delegate methods
    func session(_ session: WCSession,
                 activationDidCompleteWith state: WCSessionActivationState,
                 error: Error?) {}
}

Watch App Entry Point

import SwiftUI
import WatchKit

class AppDelegate: NSObject, WKApplicationDelegate {
    let phoneRelay = PhoneSessionRelay()
    func applicationDidFinishLaunching() {}
}

@main
struct SynheartWatchApp: App {
    @WKApplicationDelegateAdaptor(AppDelegate.self) var delegate

    var body: some Scene {
        WindowGroup {
            SessionView()
                .environmentObject(delegate.phoneRelay)
        }
    }
}

Phone-Side Relay (iOS Plugin)

The phone-side WatchSessionRelay sends commands to the watch and receives events back. It is used inside the Flutter iOS plugin to bridge between Dart and watchOS:
import WatchConnectivity

class WatchSessionRelay: NSObject, WCSessionDelegate {
    var isReachable: Bool {
        guard WCSession.isSupported() else { return false }
        return WCSession.default.isReachable
    }

    func startSession(config: SessionConfig,
                      callback: @escaping ([String: Any]) -> Void,
                      onSendFailed: @escaping () -> Void) {
        // Send command via WCSession.sendMessage
        // Store callback to forward events from watch
        // Call onSendFailed if sendMessage fails (triggers local fallback)
    }
}

XcodeGen Configuration

If you use XcodeGen for your watchOS project, add the SDK as a local package and link WatchConnectivity:
name: SynheartWatchDemo
options:
  deploymentTarget:
    watchOS: "10.0"

packages:
  SynheartSession:
    path: path/to/synheart-session-swift

targets:
  SynheartWatchApp:
    type: application
    platform: watchOS
    dependencies:
      - package: SynheartSession
      - sdk: WatchConnectivity.framework

Behavioral Signals

The SessionEngine accepts an optional BehaviorProvider that fuses behavioral data into session frames.

Mock Behavior (Development)

import SynheartSession

let engine = SessionEngine(behaviorProvider: MockBehaviorProvider())

try engine.start(config: config) { event in
    if let behavior = event["behavior"] as? [String: Any] {
        print("Stability: \(behavior["stability_index"] ?? "")")
    }
}

Production (via synheart-behavior)

import SynheartSession
import SynheartSessionBehavior
import SynheartBehavior

let behaviorSdk = SynheartBehavior()
try behaviorSdk.initialize()

let engine = SessionEngine(
    provider: myBiosignalProvider,
    behaviorProvider: BehaviorSdkProvider(sdk: behaviorSdk)
)
The "behavior" key is only present in frames when a BehaviorProvider is configured and returns data. See the SDK Usage Guide for the full list of behavioral fields.

API Reference

SessionEngine

ConstructorDescription
SessionEngine()Default mock biosignal provider, no behavior
SessionEngine(provider:)Custom biosignal provider
SessionEngine(provider:behaviorProvider:)Custom biosignal + behavioral providers
SessionEngine(behaviorProvider:)Default mock biosignal + behavioral provider
MethodDescription
start(config:callback:)Start a session. Throws if already running.
stop(sessionId:)Stop a running session. Throws if no session or ID mismatch.
getStatus()Returns status dict or nil if no active session.

HsiBuilder (Internal)

Used internally by SessionEngine to compute metrics from heart rate samples when the Flux binary is not available:
let samples: [HsiBuilder.HrSample] = [
    (timestampMs: 1700000000000, bpm: 72.0),
    (timestampMs: 1700000001000, bpm: 73.5),
]

let hsi = HsiBuilder.build(samples: samples, config: config, seq: 1)
// Returns [String: Any] HSI 1.0 dict

Resources


Author: Israel Goytom