Documentation Index
Fetch the complete documentation index at: https://docs.synheart.ai/llms.txt
Use this file to discover all available pages before exploring further.
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 mean HR locally from sampled biosignals. HRV (rmssd_ms, hr_sdnn_ms, pnn50) and the HSI 1.3 envelope are produced upstream by the Synheart runtime and ingested into the session via ingestHsiMetrics(). Without that ingest call, HRV fields will be 0.0.
Installation
Swift Package Manager
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/synheart-ai/synheart-session-swift.git", from: "0.1.0")
]
Or in Xcode:
- File > Add Packages…
- 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
| Value | Raw | Description |
|---|
.focus | "focus" | Steady-state measurement |
.breathing | "breathing" | Guided breathing with HRV focus |
ComputeProfile
| Parameter | Type | Default | Description |
|---|
windowSec | Int | 60 | Sliding window size in seconds |
emitIntervalSec | Int | 5 | Frame emission interval in seconds |
rawEmitIntervalSec | Int? | nil | Raw biosignal frame interval (defaults to emitIntervalSec) |
SessionError
| Case | Raw | Description |
|---|
.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 |
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
| Constructor | Description |
|---|
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 |
| Method | Description |
|---|
start(config:callback:) | Start a session. Throws if already running. |
stop(sessionId:) | Stop a running session. Throws if no session or ID mismatch. |
ingestHsiMetrics(_:) | Feed runtime-computed HRV/HSI metrics into the active session. The session merges these into the next emitted session_frame. |
getStatus() | Returns status dict or nil if no active session. |
Resources