Skip to main content

iOS

1. Intro

The iOS SDK is split into three main components: VitalCore, VitalHealthKit and VitalDevices. VitalCore holds common components to both VitalHealthKit and VitalDevices. Among other things, it has the network layer that allows us to send data from a device to a server. As their name hint, VitalHealthKit and VitalDevices are an abstraction over HealthKit and specific Bluetooth devices. If your app is generating data independently, you can use VitalCore directly to push data.


2. Installation

We use SPM (Swift Package Manager) to manage dependencies. You can add https://github.com/tryVital/vital-ios when adding it as a dependency to your project. Independently of using VitalDevices, or VitalHealthKit, make sure you always add VitalCore. The first two depend on the latter.

3. Initial Setup

To use our SDK, start by setting up VitalNetworkClient:

1
2
3
4
5
6
import VitalCore

VitalNetworkClient.configure(
    apiKey: "xyz",
    environment: .sandbox(.us)
)


There are two main topics you should be aware:

  1. userId.
  2. Connected source.

1. userId

A userId serves as a unique identifier of your user in Vital. You create one by sending us an id representing that user in your system (e.g. white_bear, 12832001, b5d36dbe-b745-11ec-b909-0242ac120002, etc). We advise against the use of PII (Personal Identifiable Information). It should also be unique across the users that belong to your team. You can create a userId like this:

1
2
3
4
let result = try await VitalNetworkClient.shared.user.create(
    .init(clientUserId: "white_bear"),
    setUserIdOnSuccess: false
)


A userId is an UUID4. Once you have a userId, you need to set it:

1
VitalNetworkClient.setUserId(result.userId)


tip

By default, when you create a userId, we call VitalNetworkClient.setUserId internally. If you don't want this to happen call VitalNetworkClient.setUserId(result.userId, setUserIdOnSuccess: false).

2. Connected Source

A connected source is a link between a provider (e.g. Omron) and your user. When you post information, it is expected that a connected source exists for that user and the source of information you are using. If it doesn't exist, the request will fail.

Example: You want to post Ben's Omron data to Vital. Two things need to exist: 1) Ben is a user in Vital 2) A connected source linking Ben's user to Omron.

To achieve this you can:

1
2
3
4
5
6

/// 1)
let benUser = try await VitalNetworkClient.shared.user.create(clientUserId: "white_bear")

/// 2)
let result = try await VitalNetworkClient.shared.user.createConnectedSource(for: .omron)


With these two things in place, you can now post Ben's Omron data.


4. VitalNetwork

For the most part, you won't need to instantiate model objects. VitalHealthKit and VitalDevices will generate these models on your behalf. For VitalDevices in particular, you are responsible for sending the data explicitly via VitalNetworkClient.shared.<domain>. VitalNetwork allows you to do exactly that. VitalHealkit however will send the data automatically.

1. TimeSeries and Summaries

There are two main sources of data: time series and summaries. Time series data correspond to points in time (e.g. glucose, heart rate, etc). Summaries are a digest of a particular activity (e.g. workout, sleep, etc). Summaries can have time series data. For example an workout has an array of heart rate data points. For data generated by VitalDevices, typically this will be time series.

For time series we support:

  • Glucose
  • Blood Pressure

For summaries:

  • Workout
  • Activity
  • Sleep
  • Profile
  • Body

Posting time series data is as simple as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let sample = QuantitySample(
    value: 10,
    startDate: .init(),
    endDate: .init(),
    type: "fingerprick",
    unit: "mg/dl"
)

let samples: [BloodPressureSample] = [sample]

try await VitalNetworkClient.shared.timeSeries.post(
    .bloodPressure(samples),
    stage: .daily,
    provider: .appleHealthKit
)


Likewise for summaries:

1
2
3
4
5
6
7
let workoutPatch: WorkoutPatch = ...

try await VitalNetworkClient.shared.summary.post(
    .workout(workoutPatch),
    stage: .daily,
    provider: .appleHealthKit
)


It's important to notice a few things in the above snippet:

  1. We set the stage of data as .daily. If you are sending old data, please use .historical instead. This distinction is used for the Webhooks. If you are not sure if .historical data makes sense, you can stick with .daily. A .daily will always generate a webhook with the full payload of data. For more information, please read the Webhook Flow.
  2. The provider (.appleHealthKit) must match an existing connected source for that user. This means that if there's no connected source linking the user and .appleHealthKit, the request will fail.
  3. Finally if you are generating your own fitness or medical data, use the .manual provider.

2. Creating a Connected Source

The SDK provides two ways to create a connected source. The first method is a manual approach. This is the method you should use for HealthKit, Manual and bluethooth devices.

1
2
3
let userId: UUID = ...

VitalNetworkClient.shared.link.createConnectedSource(userId, provider: .appleHealthKit)


The second method uses web authentication:

1
let url = try await VitalNetworkClient.shared.link.createProviderLink(redirectURL: "vitalExample://")


The redirectURL is the URL that's called after the authentication is done. For an iOS app, you add the following to your Info.plist:



After having the url, you can open it with SFSafariViewController or WKWebView. On completion, your app will be called with an URL with the following shape:

  1. On success: vitalExample://?state=success&isMobile=true&provider=<provider>.
  2. On failure: vitalExample://?state=error&isMobile=true&provider=<provider>&error=<error description>.

5. VitalDevices

VitalDevices connects your app to other devices via bluetooth. Currently we support glucose meters and blood pressure readers.

You start by fetching all devices supported:

1
2
3
4
5
6
7
import VitalDevices

/// Get the brands
let brands = DevicesManager.brands()

/// Each brand has devices
let devices = DevicesManager.devices(for: brands[0])


Based on the device, you start scanning your surroundings to find it. This approach filters out devices we are not interested in:

1
2
3
4
let device = devices[0]

let manager = DevicesManager()
let publisher: AnyPublisher<ScannedDevice, Never> = manager.search(for: device)


You can observe the publisher (e.g. via sink) until you find a device. Once you find a device you create a reader:

1
2
3
4
5
6
7
8
9
let scannedDevice: ScannedDevice = ...

if scannedDevice.kind == .bloodPressure {
    let reader: BloodPressureReadable = manager.bloodPressureReader(for: scannedDevice)
}

if scannedDevice.kind == .glucoseMeter {
    let reader: GlucoseMeterReadable = manager.glucoseMeter(for: scannedDevice)
}


Depending on the flow of your app, and/or the device you are working with, you can either just pair, or pair and read. The "just" pair is needed for devices that can only pair while in pairing mode. The devices we tested were able to pair and read while not in pairing mode, but your experience might be different.

For blood pressure monitors:

1
2
3
4
let reader: BloodPressureReadable = manager.bloodPressureReader(for: scannedDevice)

let justPair: AnyPublisher<Void, Error> = reader.pair(device: scannedDevice)
let pairAndRead: AnyPublisher<[BloodPressureSample], Error> = reader.read(device: scannedDevice)


And for glucose meters:

1
2
3
4
let reader: GlucoseMeterReadable = manager.glucoseMeter(for: scannedDevice)

let justPair: AnyPublisher<Void, Error> = reader.pair(device: scannedDevice)
let pairAndRead: AnyPublisher<[QuantitySample], Error> = reader.read(device: scannedDevice)


Finally, you can monitor the connection to the device itself via:

1
let monitorDevice: AnyPublisher<Bool, Never> = manager.monitorConnection(for: device)


caution

When you finish scanning for a device, you need to terminate the scanning. If you don't do this, you won't be able to connect and extract data from the device. You can achieve this by holding onto a Cancellable (via sink) and call cancel(). Or by using a more declarative approach (e.g. publisher.first()).

You can check our example app, to see how we do this.


6. VitalHealthKit

VitalHealthKit is an abstraction on top of HealthKit that 1) automates the extraction of data 2) pushs that data to Vital.

You start by enabling HealthKit capabilities in your app. Please follow this guide. It should looks like this:



You then set-up the client:

1
2
3
import VitalHealthKit

VitalHealthKitClient.configure()


You need to call this method, before doing anything else with the VitalHealthKitClient. Otherwise you will have a fatalError.

Just like VitalDevices, you are also required to setup the VitalNetworkClient:

1
2
3
4
5
6
import VitalCore

VitalNetworkClient.configure(
    apiKey: "xyz",
    environment: .sandbox(.us)
)


This is required, because we use VitalNetworkClient to push HealthKit data.

info

When configuring VitalHealthKitClient, you can enable logs and auto-sync. The latter in particular, will immediately try to push data, once the configuration method is called. Be aware that you need to setup VitalNetworkClient beforehand, otherwise the app will crash.

After this initial setup, you need to ask permission to acccess the user's data:

1
2
let resources = [.profile, .body]
let outcome = await VitalHealthKitClient.shared.ask(for: resources)


After calling ask(for:), check the outcome to see if the user granted permission. On success, you can start syncing data:

1
2
3
4
5
6
7
8
import VitalHealthKit

let resources = [.profile, .body]
VitalHealthKitClient.shared.syncData(for: resources)


/// You can also sync all resources
VitalHealthKitClient.shared.syncData()


caution

Everything that holds true for HealthKit, holds true for VitalHealthKit. For example, if someone declines to give permission to a particular data point, the SDK won't be aware. For more information please read HealthKit's Protecting User Privacy.

Although syncData() and syncData(for:) make your life easier, they can sometimes feel like a blackbox. To help you understand what's happening under the hood, we expose an status: AnyPublisher<Status, Never>. A Status looks like this:

1
2
3
4
5
6
  public enum Status {
    case syncing(VitalResource)
    case failedSyncing(VitalResource, Error?)
    case successSyncing(VitalResource)
    case nothingToSync(VitalResource)
  }


You can observe and convey this information like this:

1
2
3
cancellable = VitalHealthKitClient.shared.status.sink { value in
    print(value)
}