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:
- 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. |
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