├── Tests
├── UITests
│ ├── TestApp
│ │ ├── Support Files
│ │ │ ├── Assets.xcassets
│ │ │ │ ├── Contents.json
│ │ │ │ ├── AccentColor.colorset
│ │ │ │ │ ├── Contents.json
│ │ │ │ │ └── Contents.json.license
│ │ │ │ ├── Contents.json.license
│ │ │ │ └── AppIcon.appiconset
│ │ │ │ │ ├── Contents.json.license
│ │ │ │ │ └── Contents.json
│ │ │ ├── Info.plist
│ │ │ ├── Info.plist.license
│ │ │ ├── TestApp.entitlements.license
│ │ │ └── TestApp.entitlements
│ │ ├── TestApp.swift
│ │ ├── Views
│ │ │ ├── ContentView.swift
│ │ │ ├── JSONView.swift
│ │ │ ├── WriteDataView.swift
│ │ │ ├── ReadDataView.swift
│ │ │ ├── CreateWorkoutView.swift
│ │ │ ├── HealthRecordsTestView.swift
│ │ │ ├── CheckMappingCompletenessView.swift
│ │ │ ├── ExportDataView.swift
│ │ │ └── ElectrocardiogramTestView.swift
│ │ ├── HKElectrocardiogram+Context.swift
│ │ └── HealthKitManager.swift
│ ├── UITests.xcodeproj
│ │ ├── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ ├── contents.xcworkspacedata.license
│ │ │ └── xcshareddata
│ │ │ │ ├── IDEWorkspaceChecks.plist.license
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ ├── project.pbxproj.license
│ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ ├── TestApp.xcscheme.license
│ │ │ └── TestApp.xcscheme
│ ├── TestApp.xctestplan.license
│ ├── TestApp.xctestplan
│ └── TestAppUITests
│ │ └── TestAppUITests.swift
├── HealthKitOnFHIRTests
│ ├── SupportedCodeSystem.swift
│ ├── HKElectrocardiogramTests.swift
│ ├── FHIRExtensionsTests.swift
│ ├── HKSampleTypeResourceTypeMapping.swift
│ ├── CustomMappingsTests.swift
│ ├── HKCorrelationTests.swift
│ ├── HKStateOfMindTests.swift
│ └── HKWorkoutTests.swift
└── HealthKitOnFHIRMacrosTests
│ └── HealthKitOnFHIRMacrosTests.swift
├── Sources
├── HealthKitOnFHIR
│ ├── Resources
│ │ └── HKSampleMapping.json.license
│ ├── Foundation Extensions
│ │ ├── Utils.swift
│ │ └── Bundle+JSON.swift
│ ├── HealthKit Extensions
│ │ ├── FHIRObservationBuildable.swift
│ │ ├── HKSample+TimeZone.swift
│ │ ├── HKQuantityType+Coding.swift
│ │ ├── HKClinicalRecord+ResourceProxy.swift
│ │ ├── HKCorrelationType+Observation.swift
│ │ ├── HKWorkout+Observation.swift
│ │ ├── HKSampleType+ResourceType.swift
│ │ ├── HKQuantitySample+Observation.swift
│ │ ├── HKMetadataEnum+Coding.swift
│ │ ├── HKCategoryValue+Coding.swift
│ │ ├── HKSample+ResourceProxy.swift
│ │ ├── HKWorkoutActivityType+String.swift
│ │ └── HKStateOfMind+Observation.swift
│ ├── FHIR Extensions
│ │ ├── Observation+Value.swift
│ │ ├── Observation+Dates.swift
│ │ ├── FHIR+Utils.swift
│ │ ├── Resource+Mutation.swift
│ │ ├── FHIR Extension Builder
│ │ │ ├── FHIRExtensionBuilder.swift
│ │ │ ├── FHIRExtensionBuilder+AbsoluteTimeRange.swift
│ │ │ └── FHIRExtensionBuilder+Source.swift
│ │ └── Resource+Collections.swift
│ ├── HealthKitOnFHIR.docc
│ │ ├── SupportedHKCorrelationTypes.md
│ │ ├── HKSampleSupportTables.md
│ │ ├── SupportedHKClinicalTypes.md
│ │ ├── HealthKitOnFHIR.md
│ │ └── SupportedHKCategoryTypes.md
│ ├── HealthKitOnFHIRError.swift
│ └── HKSampleMapping
│ │ ├── HKCategorySampleMapping.swift
│ │ ├── MappedCode.swift
│ │ ├── HKWorkoutSampleMapping.swift
│ │ ├── HKQuantitySampleMapping.swift
│ │ ├── HKCorrelationMapping.swift
│ │ ├── HKStateOfMindSampleMapping.swift
│ │ ├── MappedUnit.swift
│ │ ├── HKElectrocardiogramMapping.swift
│ │ └── HKSampleMapping.swift
├── HealthKitOnFHIRMacrosImpl
│ ├── HealthKitOnFHIRMacros.swift
│ └── AddDisplayPropertyMacro.swift
└── HealthKitOnFHIRMacros
│ └── Macro.swift
├── .spi.yml
├── CONTRIBUTORS.md
├── .linkspector.yml
├── .gitignore
├── .github
└── workflows
│ ├── pull-request.yml
│ ├── build-docs.yml
│ └── build-and-test.yml
├── CITATION.cff
├── LICENSE.md
├── LICENSES
└── MIT.txt
├── Package.swift
└── README.md
/Tests/UITests/TestApp/Support Files/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Tests/UITests/UITests.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Tests/UITests/TestApp/Support Files/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Tests/UITests/TestApp.xctestplan.license:
--------------------------------------------------------------------------------
1 | This source file is part of the HealthKitOnFHIR open-source project
2 |
3 | SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
4 |
5 | SPDX-License-Identifier: MIT
--------------------------------------------------------------------------------
/Tests/UITests/TestApp/Support Files/Info.plist.license:
--------------------------------------------------------------------------------
1 | This source file is part of the HealthKitOnFHIR open-source project
2 |
3 | SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
4 |
5 | SPDX-License-Identifier: MIT
--------------------------------------------------------------------------------
/Tests/UITests/TestApp/Support Files/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Tests/UITests/UITests.xcodeproj/project.pbxproj.license:
--------------------------------------------------------------------------------
1 | This source file is part of the HealthKitOnFHIR open-source project
2 |
3 | SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
4 |
5 | SPDX-License-Identifier: MIT
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/Resources/HKSampleMapping.json.license:
--------------------------------------------------------------------------------
1 | This source file is part of the ResearchKitOnFHIR open source project
2 |
3 | SPDX-FileCopyrightText: 2022 CardinalKit and the project authors (see CONTRIBUTORS.md)
4 |
5 | SPDX-License-Identifier: MIT
6 |
--------------------------------------------------------------------------------
/Tests/UITests/TestApp/Support Files/TestApp.entitlements.license:
--------------------------------------------------------------------------------
1 | This source file is part of the HealthKitOnFHIR open-source project
2 |
3 | SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
4 |
5 | SPDX-License-Identifier: MIT
--------------------------------------------------------------------------------
/Tests/UITests/TestApp/Support Files/Assets.xcassets/Contents.json.license:
--------------------------------------------------------------------------------
1 | This source file is part of the HealthKitOnFHIR open-source project
2 |
3 | SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
4 |
5 | SPDX-License-Identifier: MIT
--------------------------------------------------------------------------------
/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme.license:
--------------------------------------------------------------------------------
1 | This source file is part of the HealthKitOnFHIR open-source project
2 |
3 | SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
4 |
5 | SPDX-License-Identifier: MIT
--------------------------------------------------------------------------------
/Tests/UITests/UITests.xcodeproj/project.xcworkspace/contents.xcworkspacedata.license:
--------------------------------------------------------------------------------
1 | This source file is part of the HealthKitOnFHIR open-source project
2 |
3 | SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
4 |
5 | SPDX-License-Identifier: MIT
--------------------------------------------------------------------------------
/Tests/UITests/TestApp/Support Files/Assets.xcassets/AccentColor.colorset/Contents.json.license:
--------------------------------------------------------------------------------
1 | This source file is part of the HealthKitOnFHIR open-source project
2 |
3 | SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
4 |
5 | SPDX-License-Identifier: MIT
--------------------------------------------------------------------------------
/Tests/UITests/TestApp/Support Files/Assets.xcassets/AppIcon.appiconset/Contents.json.license:
--------------------------------------------------------------------------------
1 | This source file is part of the HealthKitOnFHIR open-source project
2 |
3 | SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
4 |
5 | SPDX-License-Identifier: MIT
--------------------------------------------------------------------------------
/Tests/UITests/UITests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist.license:
--------------------------------------------------------------------------------
1 | This source file is part of the HealthKitOnFHIR open-source project
2 |
3 | SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
4 |
5 | SPDX-License-Identifier: MIT
--------------------------------------------------------------------------------
/Tests/UITests/TestApp/Support Files/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Tests/UITests/UITests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | #
2 | # This source file is part of the HealthKitOnFHIR open source project
3 | #
4 | # SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | #
6 | # SPDX-License-Identifier: MIT
7 | #
8 |
9 | version: 1
10 | builder:
11 | configs:
12 | - platform: ios
13 | documentation_targets:
14 | - HealthKitOnFHIR
15 |
--------------------------------------------------------------------------------
/Tests/UITests/TestApp/TestApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open-source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKitOnFHIR
10 | import SwiftUI
11 |
12 |
13 | @main
14 | struct UITestsApp: App {
15 | var body: some Scene {
16 | WindowGroup {
17 | ContentView()
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Tests/HealthKitOnFHIRTests/SupportedCodeSystem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 |
10 | enum SupportedCodeSystem: String {
11 | case loinc = "http://loinc.org"
12 | case apple = "http://developer.apple.com/documentation/healthkit"
13 | case ucum = "http://unitsofmeasure.org"
14 | }
15 |
--------------------------------------------------------------------------------
/Tests/UITests/TestApp/Support Files/TestApp.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.developer.healthkit
6 |
7 | com.apple.developer.healthkit.access
8 |
9 | health-records
10 |
11 | com.apple.developer.healthkit.background-delivery
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 |
12 |
13 | HealthKitOnFHIR contributors
14 | ====================
15 |
16 | * [Paul Schmiedmayer](https://github.com/PSchmiedmayer)
17 | * [Vishnu Ravi](https://github.com/vishnuravi)
18 | * [Lukas Kollmer](https://github.com/lukaskollmer)
19 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIRMacrosImpl/HealthKitOnFHIRMacros.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2025 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import SwiftCompilerPlugin
10 | import SwiftSyntaxMacros
11 |
12 |
13 | @main
14 | struct HealthKitOnFHIRMacros: CompilerPlugin {
15 | var providingMacros: [any Macro.Type] = [
16 | SynthesizeDisplayPropertyMacro.self
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/Foundation Extensions/Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2025 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 |
10 | extension RangeReplaceableCollection {
11 | @inlinable
12 | mutating func removeElements(at indices: some Collection) {
13 | for idx in indices.sorted().reversed() {
14 | self.remove(at: idx)
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HealthKit Extensions/FHIRObservationBuildable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2025 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 | import ModelsR4
11 |
12 |
13 | /// A Type that can be used to build up a FHIR `Observation`.
14 | protocol FHIRObservationBuildable {
15 | func build(_ observation: Observation, mapping: HKSampleMapping) throws
16 | }
17 |
--------------------------------------------------------------------------------
/.linkspector.yml:
--------------------------------------------------------------------------------
1 | #
2 | # This source file is part of the Stanford Biodesign Digital Health HealthKitOnFHIR open-source project
3 | #
4 | # SPDX-FileCopyrightText: 2025 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | #
6 | # SPDX-License-Identifier: MIT
7 | #
8 |
9 | dirs:
10 | - .
11 | useGitIgnore: true
12 | ignorePatterns:
13 | - pattern: '^doc:.*$'
14 | - pattern: '^http://localhost.*$'
15 | - pattern: 'http://unitsofmeasure\.org'
16 | replacementPatterns:
17 | - pattern: '(.*)#gh-dark-mode-only'
18 | replacement: '$1'
19 | - pattern: '(.*)#gh-light-mode-only'
20 | replacement: '$1'
21 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/FHIR Extensions/Observation+Value.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import Foundation
10 | import HealthKit
11 | import ModelsR4
12 |
13 |
14 | extension Observation {
15 | func setValue(_ quantity: Quantity) {
16 | value = .quantity(quantity)
17 | }
18 |
19 | func setValue(_ string: String) {
20 | value = .string(string.asFHIRStringPrimitive())
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #
2 | # This source file is part of the HealthKitOnFHIR open source project
3 | #
4 | # SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | #
6 | # SPDX-License-Identifier: MIT
7 | #
8 |
9 | # Swift Package Manager
10 | Package.resolved
11 | *.xcodeproj
12 | .swiftpm
13 | .build/
14 | .xcodebuild/
15 | coverage.lcov
16 | *.xcresult
17 |
18 | # IDE related folders
19 | .idea
20 |
21 | # Xcode User settings
22 | xcuserdata/
23 |
24 | # Other files
25 | .DS_Store
26 | .env
27 |
28 | # Documentation generation
29 | *.doccarchive
30 | docs/
31 |
32 | # UITests Project
33 | !UITests.xcodeproj
--------------------------------------------------------------------------------
/.github/workflows/pull-request.yml:
--------------------------------------------------------------------------------
1 | #
2 | # This source file is part of the Stanford Spezi open-source project
3 | #
4 | # SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | #
6 | # SPDX-License-Identifier: MIT
7 | #
8 |
9 | name: Pull Request
10 |
11 | on:
12 | pull_request:
13 | workflow_dispatch:
14 |
15 | jobs:
16 | reuse_action:
17 | name: REUSE Compliance Check
18 | uses: StanfordBDHG/.github/.github/workflows/reuse.yml@v2
19 | swiftlint:
20 | name: SwiftLint
21 | uses: StanfordBDHG/.github/.github/workflows/swiftlint.yml@v2
22 | markdown_link_check:
23 | name: Markdown Link Check
24 | uses: StanfordBDHG/.github/.github/workflows/markdown-link-check.yml@v2
25 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HealthKitOnFHIR.docc/SupportedHKCorrelationTypes.md:
--------------------------------------------------------------------------------
1 | # Supported HKCorrelationTypes
2 |
11 |
12 | HealthKitOnFHIR supports 1 of 2 correlation types.
13 |
14 | |HKCorrelationType|Supported|Code|
15 | |----|----|----|
16 | |[BloodPressure](https://developer.apple.com/documentation/healthkit/HKCorrelationTypeIdentifierBloodPressure)|✅|[85354-9](http://loinc.org/85354-9)|
17 | |[Food](https://developer.apple.com/documentation/healthkit/HKCorrelationTypeIdentifierFood)|❌|-|
18 |
--------------------------------------------------------------------------------
/CITATION.cff:
--------------------------------------------------------------------------------
1 | #
2 | # This source file is part of the HealthKitOnFHIR open source project
3 | #
4 | # SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | #
6 | # SPDX-License-Identifier: MIT
7 | #
8 |
9 | cff-version: 1.2.0
10 | message: "If you use this software, please cite it as below."
11 | authors:
12 | - family-names: "Ravi"
13 | given-names: "Vishnu"
14 | orcid: "https://orcid.org/0000-0003-0359-1275"
15 | - family-names: "Schmiedmayer"
16 | given-names: "Paul"
17 | orcid: "https://orcid.org/0000-0002-8607-9148"
18 | - family-names: "Aalami"
19 | given-names: "Oliver"
20 | orcid: "https://orcid.org/0000-0002-7799-2429"
21 | title: "HealthKitOnFHIR"
22 | doi: 10.5281/zenodo.7538171
23 | url: "https://github.com/StanfordBDHG/HealthKitOnFHIR"
24 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HealthKit Extensions/HKSample+TimeZone.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 |
11 |
12 | extension HKSample {
13 | /// Gets the `TimeZone` from the sample's metadata if available
14 | /// - Returns: A `TimeZone` if the metadata contains a valid HKMetadataKeyTimeZone value, otherwise nil
15 | internal var timeZone: TimeZone? {
16 | guard let timeZoneIdentifier = metadata?[HKMetadataKeyTimeZone] as? String else {
17 | return nil
18 | }
19 | return TimeZone(identifier: timeZoneIdentifier)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HealthKitOnFHIR.docc/HKSampleSupportTables.md:
--------------------------------------------------------------------------------
1 | # HKSample Support Tables
2 |
11 |
12 |
13 | - [HKCategoryType]()
14 | - HealthKitOnFHIR supports 63 of 63 category types.
15 | - [HKCorrelation]()
16 | - HealthKitOnFHIR supports 1 of 2 correlation types.
17 | - [HKClinicalType]()
18 | - HealthKitOnFHIR supports 8 of 8 clinical types.
19 | - [HKQuantityType]()
20 | - HealthKitOnFHIR supports 87 of 89 quantity types.
21 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HealthKitOnFHIRError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 |
10 | /// Error thrown by the HealthKitOnFHIR module if transforming a specific `HKSample` type to an FHIR resource was not possible.
11 | public enum HealthKitOnFHIRError: Error {
12 | /// Indicates that a specific `HKSample` type is currently not supported by HealthKitOnFHIR.
13 | case notSupported
14 | /// Indicates that a specific value is not valid
15 | case invalidValue
16 | /// Indicates that the given FHIR resource encoded in an `HKClinicalRecord` uses an unsupported version
17 | case unsupportedFHIRVersion
18 | /// Indicates that the fhirResource property of an `HKClinicalRecord` is nil
19 | case invalidFHIRResource
20 | }
21 |
--------------------------------------------------------------------------------
/Tests/UITests/TestApp.xctestplan:
--------------------------------------------------------------------------------
1 | {
2 | "configurations" : [
3 | {
4 | "id" : "724DC806-4B5D-4F9D-8A55-BF542B7A7DE2",
5 | "name" : "Default",
6 | "options" : {
7 |
8 | }
9 | }
10 | ],
11 | "defaultOptions" : {
12 | "codeCoverage" : {
13 | "targets" : [
14 | {
15 | "containerPath" : "container:..\/..",
16 | "identifier" : "HealthKitOnFHIR",
17 | "name" : "HealthKitOnFHIR"
18 | }
19 | ]
20 | },
21 | "targetForVariableExpansion" : {
22 | "containerPath" : "container:UITests.xcodeproj",
23 | "identifier" : "2F6D139128F5F384007C25D6",
24 | "name" : "TestApp"
25 | }
26 | },
27 | "testTargets" : [
28 | {
29 | "target" : {
30 | "containerPath" : "container:UITests.xcodeproj",
31 | "identifier" : "2F6D13AB28F5F386007C25D6",
32 | "name" : "TestAppUITests"
33 | }
34 | }
35 | ],
36 | "version" : 1
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HealthKit Extensions/HKQuantityType+Coding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import Foundation
10 | import HealthKit
11 | import ModelsR4
12 |
13 |
14 | extension HKQuantityType {
15 | /// Converts an HKQuantityType into corresponding FHIR Coding(s) based on a specified mapping
16 | var codes: [Coding] {
17 | codes()
18 | }
19 |
20 |
21 | /// Converts an HKQuantityType into corresponding FHIR Coding(s) based on a specified mapping
22 | func codes(mappings: [HKQuantityType: HKQuantitySampleMapping] = HKQuantitySampleMapping.default) -> [Coding] {
23 | guard let mapping = mappings[self] else {
24 | return []
25 | }
26 | return mapping.codings.map(\.coding)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HealthKit Extensions/HKClinicalRecord+ResourceProxy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 | import ModelsR4
11 |
12 |
13 | @available(watchOS, unavailable)
14 | extension HKClinicalRecord {
15 | /// Converts an `HKClinicalRecord` into a corresponding FHIR resource, encapsulated in a `ResourceProxy`
16 | func resource() throws -> ResourceProxy {
17 | guard let fhirResource = self.fhirResource else {
18 | throw HealthKitOnFHIRError.invalidFHIRResource
19 | }
20 | guard fhirResource.fhirVersion == HKFHIRVersion.primaryR4() else {
21 | throw HealthKitOnFHIRError.unsupportedFHIRVersion
22 | }
23 | return try JSONDecoder().decode(ResourceProxy.self, from: fhirResource.data)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/LICENSES/MIT.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIRMacros/Macro.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2025 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | /// Synthesizes a `var display: String?` property into an enum; the `display` String is derived from the enum case names.
10 | ///
11 | /// ## Example:
12 | /// ```swift
13 | /// @SynthesizeDisplayProperty(HKDevicePlacementSide.self, .unknown, .left, .right, .central)
14 | /// extension HKDevicePlacementSide {}
15 | /// ```
16 | ///
17 | /// - parameter type: The type the macro should operate on.
18 | /// - parameter cases: The enum's cases.
19 | /// - parameter additionalCases: Additional cases not listed in `cases`. This property exists to support cases whose availability is newer than the package's deployment target.
20 | @attached(member, names: named(display))
21 | public macro SynthesizeDisplayProperty(
22 | _ type: T.Type,
23 | _ cases: T...,
24 | additionalCases: StaticString...
25 | ) = #externalMacro(module: "HealthKitOnFHIRMacrosImpl", type: "SynthesizeDisplayPropertyMacro")
26 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/Foundation Extensions/Bundle+JSON.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | extension Foundation.Bundle {
13 | func decode(_ type: T.Type = T.self, from file: String) -> T {
14 | // swiftlint:disable:previous function_default_parameter_at_end
15 | // We use the parameter order here with the default parameter at the beginning to follow the Swift API guidelines to
16 | // form API calls similar to English sentences.
17 |
18 | guard let url = self.url(forResource: file, withExtension: nil),
19 | let data = try? Data(contentsOf: url) else {
20 | fatalError("Could not load \(file) in the module.")
21 | }
22 |
23 | do {
24 | return try JSONDecoder().decode(T.self, from: data)
25 | } catch {
26 | fatalError("Could not decode \(file) in the module: \(error)")
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/FHIR Extensions/Observation+Dates.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import Foundation
10 | import HealthKit
11 | import ModelsR4
12 |
13 |
14 | extension Observation {
15 | /// Sets the `Observation`'s effective date.
16 | @inlinable
17 | public func setEffective(startDate: Date, endDate: Date, timeZone: TimeZone) throws {
18 | if startDate == endDate {
19 | effective = .dateTime(FHIRPrimitive(try DateTime(date: startDate, timeZone: timeZone)))
20 | } else {
21 | effective = .period(Period(
22 | end: FHIRPrimitive(try DateTime(date: endDate, timeZone: timeZone)),
23 | start: FHIRPrimitive(try DateTime(date: startDate, timeZone: timeZone))
24 | ))
25 | }
26 | }
27 |
28 | /// Sets the `Observation`'s issued date.
29 | @inlinable
30 | public func setIssued(on date: Date) throws {
31 | issued = FHIRPrimitive(try Instant(date: date))
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/.github/workflows/build-docs.yml:
--------------------------------------------------------------------------------
1 | #
2 | # This source file is part of the HealthKitOnFHIR open source project
3 | #
4 | # SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | #
6 | # SPDX-License-Identifier: MIT
7 | #
8 |
9 | name: Build Docs
10 |
11 | on:
12 | workflow_dispatch:
13 |
14 | jobs:
15 | build_docs:
16 | name: Build HealthKit Support Table
17 | runs-on: ubuntu-latest
18 | steps:
19 | - name: Checkout repo
20 | uses: actions/checkout@v3
21 | - name: Setup python
22 | uses: actions/setup-python@v4
23 | with:
24 | python-version: '3.10'
25 | - name: Run python script
26 | run: |
27 | cd Scripts
28 | python support_table_generator.py
29 | - name: Commit updated files
30 | run: |
31 | git config --local user.email "github-actions[bot]@users.noreply.github.com"
32 | git config --local user.name "github-actions[bot]"
33 | git add -A
34 | git commit -am "Update HealthKit Support Table"
35 | - name: Push changes
36 | uses: ad-m/github-push-action@v0.6.0
37 | with:
38 | github_token: ${{ secrets.GITHUB_TOKEN }}
39 | branch: ${{ github.ref }}
40 |
--------------------------------------------------------------------------------
/Tests/UITests/TestApp/Views/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open-source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import SwiftUI
10 |
11 |
12 | struct ContentView: View {
13 | var body: some View {
14 | NavigationStack {
15 | Form {
16 | Section {
17 | NavigationLink("Write Data", destination: WriteDataView())
18 | NavigationLink("Read Data", destination: ReadDataView())
19 | NavigationLink("Electrocardiogram", destination: ElectrocardiogramTestView())
20 | NavigationLink("Health Records", destination: HealthRecordsTestView())
21 | NavigationLink("Create Workout", destination: CreateWorkoutView())
22 | NavigationLink("Export Data", destination: ExportDataView())
23 | NavigationLink("Mapping Completeness", destination: CheckMappingCompletenessView())
24 | }
25 | }
26 | .navigationBarTitle("HealthKitOnFHIR Tests")
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HealthKit Extensions/HKCorrelationType+Observation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 | import ModelsR4
11 |
12 |
13 | extension HKCorrelation: FHIRObservationBuildable {
14 | func build(_ observation: Observation, mapping: HKSampleMapping) throws {
15 | guard let mapping = mapping.correlationMapping[self.correlationType] else {
16 | throw HealthKitOnFHIRError.notSupported
17 | }
18 | for code in mapping.codings {
19 | observation.appendCoding(code.coding)
20 | }
21 | for category in mapping.categories {
22 | observation.appendCategory(
23 | CodeableConcept(coding: [category.coding])
24 | )
25 | }
26 | for object in self.objects {
27 | guard let sample = object as? HKQuantitySample else {
28 | throw HealthKitOnFHIRError.notSupported
29 | }
30 | observation.appendComponent(try sample.quantity.buildObservationComponent(for: sample.quantityType))
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HealthKitOnFHIR.docc/SupportedHKClinicalTypes.md:
--------------------------------------------------------------------------------
1 | # Supported HKClinicalTypes
2 |
11 |
12 | HealthKitOnFHIR supports 8 of 8 clinical types.
13 |
14 | |HKClinicalType|Supported|
15 | |----|----|
16 | |[AllergyRecord](https://developer.apple.com/documentation/healthkit/HKClinicalTypeIdentifierAllergyRecord)|✅|
17 | |[ConditionRecord](https://developer.apple.com/documentation/healthkit/HKClinicalTypeIdentifierConditionRecord)|✅|
18 | |[CoverageRecord](https://developer.apple.com/documentation/healthkit/HKClinicalTypeIdentifierCoverageRecord)|✅|
19 | |[ImmunizationRecord](https://developer.apple.com/documentation/healthkit/HKClinicalTypeIdentifierImmunizationRecord)|✅|
20 | |[LabResultRecord](https://developer.apple.com/documentation/healthkit/HKClinicalTypeIdentifierLabResultRecord)|✅|
21 | |[MedicationRecord](https://developer.apple.com/documentation/healthkit/HKClinicalTypeIdentifierMedicationRecord)|✅|
22 | |[ProcedureRecord](https://developer.apple.com/documentation/healthkit/HKClinicalTypeIdentifierProcedureRecord)|✅|
23 | |[VitalSignRecord](https://developer.apple.com/documentation/healthkit/HKClinicalTypeIdentifierVitalSignRecord)|✅|
24 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HKSampleMapping/HKCategorySampleMapping.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 |
11 |
12 | /// An ``HKCategorySampleMapping`` allows developers to customize the mapping of `HKCategorySample`s to FHIR observations.
13 | public struct HKCategorySampleMapping: Decodable, Sendable {
14 | /// A default instance of an ``HKCategorySampleMapping`` instance allowing developers to customize the ``HKCategorySampleMapping``.
15 | ///
16 | /// The default values are loaded from the `HKSampleMapping.json` resource in the ``HealthKitOnFHIR`` Swift Package.
17 | public static let `default` = HKSampleMapping.default.categorySampleMapping
18 |
19 | /// The FHIR codings defined as ``MappedCode``s used for the specified `HKCategorySample` type
20 | public var codings: [MappedCode]
21 |
22 | /// An ``HKCategorySampleMapping`` allows developers to customize the mapping of `HKCategorySample`s to an FHIR Observations.
23 | /// - Parameters:
24 | /// - codings: The FHIR codings defined as ``MappedCode``s used for the specified `HKCategorySample` type
25 | public init(
26 | codings: [MappedCode]
27 | ) {
28 | self.codings = codings
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Tests/UITests/TestApp/Views/JSONView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open-source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import SwiftUI
10 |
11 |
12 | struct JSONView: View {
13 | @Environment(\.dismiss) var dismiss
14 | @Binding var json: String
15 | @State var lines: [(linenumber: Int, text: String)] = []
16 |
17 |
18 | var body: some View {
19 | NavigationStack {
20 | ScrollView {
21 | LazyVStack(alignment: .leading) {
22 | ForEach(lines, id: \.linenumber) { line in
23 | Text(line.text)
24 | .multilineTextAlignment(.leading)
25 | .font(.system(size: 12, design: .monospaced))
26 | }
27 | }
28 | }
29 | .toolbar {
30 | ToolbarItem(placement: .cancellationAction) {
31 | Button("Dismiss") {
32 | dismiss()
33 | }
34 | }
35 | }
36 | .onAppear {
37 | var lineNumber = 0
38 | print(json)
39 | json.enumerateLines { line, _ in
40 | lines.append((lineNumber, line))
41 | lineNumber += 1
42 | }
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HKSampleMapping/MappedCode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import Foundation
10 | import ModelsR4
11 |
12 |
13 | /// A ``MappedCode`` instance is used to specify codings for FHIR observations mapped from HealthKit's `HKSample`s.
14 | public struct MappedCode: Decodable, Sendable {
15 | /// Symbol in syntax defined by the system.
16 | public var code: String
17 | /// Representation defined by the system.
18 | public var display: String
19 | /// Identity of the terminology system.
20 | public var system: URL
21 |
22 |
23 | var coding: Coding {
24 | Coding(
25 | code: code.asFHIRStringPrimitive(),
26 | display: display.asFHIRStringPrimitive(),
27 | system: FHIRPrimitive(FHIRURI(system))
28 | )
29 | }
30 |
31 |
32 | /// A ``MappedCode`` instance is used to specify codings for FHIR observations mapped from HealthKit's `HKSample`s.
33 | /// - Parameters:
34 | /// - code: Symbol in syntax defined by the system.
35 | /// - display: Representation defined by the system.
36 | /// - system: Identity of the terminology system.
37 | public init(
38 | code: String,
39 | display: String,
40 | system: URL
41 | ) {
42 | self.code = code
43 | self.display = display
44 | self.system = system
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HealthKit Extensions/HKWorkout+Observation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 | import ModelsR4
11 |
12 |
13 | extension HKWorkout: FHIRObservationBuildable {
14 | /// Generates an observation that captures the type of physical activity performed for a single instance of physical activity, based on https://build.fhir.org/ig/HL7/physical-activity/StructureDefinition-pa-observation-activity-measure.html
15 | /// Note: An `HKWorkout` object can also act as a container for other `HKSample` objects, which will need to be converted to observations individually.
16 | func build(_ observation: Observation, mapping: HKSampleMapping) throws {
17 | let mapping = mapping.workoutSampleMapping
18 | for code in mapping.codings {
19 | observation.appendCoding(code.coding)
20 | }
21 | for category in mapping.categories {
22 | observation.appendCategory(CodeableConcept(coding: [category.coding]))
23 | }
24 | let valueCodeableConcept = CodeableConcept(
25 | coding: [
26 | Coding(
27 | code: try self.workoutActivityType.fhirWorkoutTypeValue.asFHIRStringPrimitive(),
28 | system: "http://developer.apple.com/documentation/healthkit".asFHIRURIPrimitive()
29 | )
30 | ]
31 | )
32 | observation.value = .codeableConcept(valueCodeableConcept)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Tests/HealthKitOnFHIRTests/HKElectrocardiogramTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 | @testable import HealthKitOnFHIR
11 | import Testing
12 |
13 |
14 | @MainActor // to work around https://github.com/apple/FHIRModels/issues/36
15 | struct HKElectrocardiogramTests {
16 | @Test
17 | func electrocardiogramCategoryTests() {
18 | #expect(HKElectrocardiogram.SymptomsStatus.notSet.display == "not set")
19 | #expect(HKElectrocardiogram.SymptomsStatus.none.display == "none")
20 | #expect(HKElectrocardiogram.SymptomsStatus.present.display == "present")
21 |
22 | #expect(HKElectrocardiogram.Classification.notSet.display == "not set")
23 | #expect(HKElectrocardiogram.Classification.sinusRhythm.display == "sinus rhythm")
24 | #expect(HKElectrocardiogram.Classification.atrialFibrillation.display == "atrial fibrillation")
25 | #expect(HKElectrocardiogram.Classification.inconclusiveLowHeartRate.display == "inconclusive low heart rate")
26 | #expect(HKElectrocardiogram.Classification.inconclusiveHighHeartRate.display == "inconclusive high heart rate")
27 | #expect(HKElectrocardiogram.Classification.inconclusivePoorReading.display == "inconclusive poor reading")
28 | #expect(HKElectrocardiogram.Classification.inconclusiveOther.display == "inconclusive other")
29 | #expect(HKElectrocardiogram.Classification.unrecognized.display == "unrecognized")
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Tests/HealthKitOnFHIRTests/FHIRExtensionsTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2025 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import Foundation
10 | @testable import HealthKitOnFHIR
11 | import ModelsR4
12 | import Testing
13 |
14 |
15 | @MainActor
16 | struct FHIRExtensionsTests {
17 | @Test
18 | func dateTimeUtils() throws {
19 | let timeZone = try #require(TimeZone(identifier: "Europe/Berlin"))
20 | let date = try #require(Calendar.current.date(
21 | from: .init(timeZone: timeZone, year: 2025, month: 07, day: 09, hour: 12, minute: 24)
22 | ))
23 | let instant = try Instant(date: date, timeZone: timeZone)
24 | let dateTime1 = try DateTime(date: date, timeZone: timeZone)
25 | let dateTime2 = try DateTime(instant: instant)
26 | #expect(dateTime1 == dateTime2)
27 | }
28 |
29 | @Test
30 | func fhirDateUtils() throws {
31 | let timeZone = try #require(TimeZone(identifier: "Europe/Berlin"))
32 | let date = try #require(Calendar.current.date(
33 | from: .init(timeZone: timeZone, year: 2025, month: 07, day: 09, hour: 12, minute: 24)
34 | ))
35 | let instantDate = InstantDate(year: 2025, month: 07, day: 09)
36 | let fhirDate1 = try FHIRDate(date: date, timeZone: timeZone)
37 | let fhirDate2 = FHIRDate(instantDate: instantDate)
38 | let fhirDate3 = FHIRDate(year: 2025, month: 07, day: 09)
39 | #expect(fhirDate1 == fhirDate2)
40 | #expect(fhirDate1 == fhirDate3)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HKSampleMapping/HKWorkoutSampleMapping.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 |
11 |
12 | /// An ``HKWorkoutSampleMapping`` allows developers to customize the mapping of `HKWorkout` samples to FHIR Observations.
13 | public struct HKWorkoutSampleMapping: Decodable, Sendable {
14 | /// A default instance of an ``HKWorkoutSampleMapping`` allowing developers to customize the ``HKWorkoutSampleMapping``
15 | /// The default values are loaded from the `HKSampleMapping.json` resource in the ``HealthKitOnFHIR`` Swift Package.
16 | public static let `default` = HKSampleMapping.default.workoutSampleMapping
17 |
18 | /// The FHIR codings defined as ``MappedCode``s to be used for `HKWorkout` samples
19 | public var codings: [MappedCode]
20 | /// The FHIR categories defined as ``MappedCode``s to be used for `HKWorkout` samples
21 | public var categories: [MappedCode]
22 |
23 |
24 | /// An ``HKWorkoutSampleMapping`` allows developers to customize the mapping of `HKWorkout`s to FHIR observations.
25 | /// - Parameters:
26 | /// - codings: The FHIR codings defined as ``MappedCode``s used for the `HKWorkout` sample
27 | /// - categories: The FHIR categories defined as ``MappedCode``s used for the `HKWorkout` sample
28 | public init(
29 | codings: [MappedCode] = Self.default.codings,
30 | categories: [MappedCode] = Self.default.categories
31 | ) {
32 | self.codings = codings
33 | self.categories = categories
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HKSampleMapping/HKQuantitySampleMapping.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 |
11 |
12 | /// An ``HKQuantitySampleMapping`` allows developers to customize the mapping of `HKQuantitySample`s to an FHIR observations.
13 | public struct HKQuantitySampleMapping: Decodable, Sendable {
14 | /// A default instance of an ``HKQuantitySampleMapping`` instance allowing developers to customize the ``HKQuantitySampleMapping``.
15 | ///
16 | /// The default values are loaded from the `HKSampleMapping.json` resource in the ``HealthKitOnFHIR`` Swift Package.
17 | public static let `default` = HKSampleMapping.default.quantitySampleMapping
18 |
19 |
20 | /// The FHIR codings defined as ``MappedCode``s used for the specified `HKQuantitySample` type
21 | public var codings: [MappedCode]
22 | /// The FHIR units defined as ``MappedUnit``s used for the specified `HKQuantitySample` type
23 | public var unit: MappedUnit
24 |
25 |
26 | /// An ``HKQuantitySampleMapping`` allows developers to customize the mapping of `HKQuantitySample`s to FHIR observations.
27 | /// - Parameters:
28 | /// - codings: The FHIR codings defined as ``MappedCode``s used for the specified `HKQuantitySample` type
29 | /// - unit: The FHIR units defined as ``MappedUnit``s used for the specified `HKQuantitySample` type
30 | public init(
31 | codings: [MappedCode],
32 | unit: MappedUnit
33 | ) {
34 | self.codings = codings
35 | self.unit = unit
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HKSampleMapping/HKCorrelationMapping.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 |
11 |
12 | /// An ``HKCorrelationMapping`` allows developers to customize the mapping of `HKCorrelation`s to an FHIR observations.
13 | public struct HKCorrelationMapping: Decodable, Sendable {
14 | /// A default instance of an ``HKCorrelationMapping`` instance allowing developers to customize the ``HKCorrelationMapping``.
15 | ///
16 | /// The default values are loaded from the `HKSampleMapping.json` resource in the ``HealthKitOnFHIR`` Swift Package.
17 | public static let `default` = HKSampleMapping.default.correlationMapping
18 |
19 |
20 | /// The FHIR codings defined as ``MappedCode``s used for the specified `HKCorrelation` type
21 | public var codings: [MappedCode]
22 | /// The FHIR categories defined as ``MappedCode``s used for the specified `HKCorrelation` type
23 | public var categories: [MappedCode]
24 |
25 |
26 | /// An ``HKCorrelationMapping`` allows developers to customize the mapping of `HKCorrelation`s to an FHIR Observations.
27 | /// - Parameters:
28 | /// - codings: The FHIR codings defined as ``MappedCode``s used for the specified `HKCorrelation` type
29 | /// - categories: The FHIR categories defined as ``MappedCode``s used for the specified `HKCorrelation` type
30 | public init(
31 | codings: [MappedCode],
32 | categories: [MappedCode]
33 | ) {
34 | self.codings = codings
35 | self.categories = categories
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Tests/HealthKitOnFHIRTests/HKSampleTypeResourceTypeMapping.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 | import HealthKitOnFHIR
11 | import ModelsR4
12 | import Testing
13 |
14 |
15 | @MainActor // to work around https://github.com/apple/FHIRModels/issues/36
16 | struct HKSampleTypeResourceTypeMapping {
17 | @Test
18 | func hkSampleTypeMappingToObservation() throws {
19 | #expect(try HKQuantityType(.activeEnergyBurned).resourceType == .observation)
20 | #expect(try HKCorrelationType(.bloodPressure).resourceType == .observation)
21 | #expect(try HKCategoryType(.abdominalCramps).resourceType == .observation)
22 |
23 | #expect(throws: HealthKitOnFHIRError.self) {
24 | try HKSampleType.workoutType().resourceType
25 | }
26 | }
27 |
28 | @Test
29 | func hkClinicalTypeMappingToResourceType() throws {
30 | #expect(try HKClinicalType(.allergyRecord).resourceType == .allergyIntolerance)
31 | #expect(try HKClinicalType(.conditionRecord).resourceType == .condition)
32 | #expect(try HKClinicalType(.coverageRecord).resourceType == .coverage)
33 | #expect(try HKClinicalType(.immunizationRecord).resourceType == .immunization)
34 | #expect(try HKClinicalType(.labResultRecord).resourceType == .observation)
35 | #expect(try HKClinicalType(.medicationRecord).resourceType == .medication)
36 | #expect(try HKClinicalType(.procedureRecord).resourceType == .procedure)
37 | #expect(try HKClinicalType(.vitalSignRecord).resourceType == .observation)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Tests/UITests/TestApp/Views/WriteDataView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open-source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import Foundation
10 | import HealthKit
11 | import SwiftUI
12 |
13 |
14 | struct WriteDataView: View {
15 | private var manager = HealthKitManager()
16 | @State private var steps: Double?
17 | @State private var status = ""
18 |
19 |
20 | var body: some View {
21 | Form {
22 | Section {
23 | TextField("Number of steps...", value: $steps, format: .number)
24 | Button("Write Step Count") {
25 | Task {
26 | try await writeSteps()
27 | }
28 | }
29 | .disabled(steps == nil)
30 | }
31 | Section {
32 | if !self.status.isEmpty {
33 | Text(status)
34 | }
35 | }
36 | }
37 | .navigationBarTitle("Write Data")
38 | }
39 |
40 |
41 | private func writeSteps() async throws {
42 | guard let steps else {
43 | return
44 | }
45 |
46 | try await manager.requestStepAuthorization()
47 |
48 | try await manager.writeSteps(
49 | startDate: Date() - 60 * 60,
50 | endDate: Date(),
51 | steps: steps
52 | )
53 |
54 | self.status = "Data successfully written!"
55 | }
56 | }
57 |
58 |
59 | struct WriteDataView_Previews: PreviewProvider {
60 | static var previews: some View {
61 | WriteDataView()
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Tests/UITests/TestApp/Views/ReadDataView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open-source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | @preconcurrency import HealthKit
10 | import SwiftUI
11 |
12 |
13 | struct ReadDataView: View {
14 | @State private var manager = HealthKitManager()
15 |
16 | @State private var json = ""
17 | @State private var showingSheet = false
18 |
19 |
20 | var body: some View {
21 | Form {
22 | Section {
23 | Button("Read Step Count") {
24 | Task {
25 | try await readSteps()
26 | showingSheet.toggle()
27 | }
28 | }
29 | .sheet(isPresented: $showingSheet) {
30 | JSONView(json: $json)
31 | }
32 | }
33 | }
34 | .navigationBarTitle("Read Data")
35 | }
36 |
37 |
38 | private func readSteps() async throws {
39 | try await manager.requestStepAuthorization()
40 |
41 | let observations = try await manager.readStepCount(
42 | sorted: [.init(\.startDate, order: .reverse)],
43 | limit: 1
44 | )
45 | .compactMap { sample in
46 | try? sample.resource().get()
47 | }
48 |
49 | let encoder = JSONEncoder()
50 | encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
51 |
52 | guard let data = try? encoder.encode(observations) else {
53 | return
54 | }
55 |
56 | self.json = String(decoding: data, as: UTF8.self)
57 | }
58 | }
59 |
60 |
61 | struct ReadDataView_Previews: PreviewProvider {
62 | static var previews: some View {
63 | ReadDataView()
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HealthKit Extensions/HKSampleType+ResourceType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 | import ModelsR4
11 |
12 |
13 | extension HKSampleType {
14 | /// Converts an `HKSampleType` into the corresponding FHIR resource type, defined as a `ResourceType`
15 | public var resourceType: ResourceType {
16 | get throws {
17 | switch self {
18 | case is HKQuantityType, is HKCorrelationType, is HKCategoryType:
19 | return ResourceType.observation
20 | case let clinicalType as HKClinicalType:
21 | switch clinicalType {
22 | case HKClinicalType(.allergyRecord):
23 | return ResourceType.allergyIntolerance
24 | case HKClinicalType(.conditionRecord):
25 | return ResourceType.condition
26 | case HKClinicalType(.coverageRecord):
27 | return ResourceType.coverage
28 | case HKClinicalType(.immunizationRecord):
29 | return ResourceType.immunization
30 | case HKClinicalType(.labResultRecord):
31 | return ResourceType.observation
32 | case HKClinicalType(.medicationRecord):
33 | return ResourceType.medication
34 | case HKClinicalType(.procedureRecord):
35 | return ResourceType.procedure
36 | case HKClinicalType(.vitalSignRecord):
37 | return ResourceType.observation
38 | default:
39 | throw HealthKitOnFHIRError.notSupported
40 | }
41 | default:
42 | throw HealthKitOnFHIRError.notSupported
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HealthKit Extensions/HKQuantitySample+Observation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 | import ModelsR4
11 |
12 |
13 | extension HKQuantitySample: FHIRObservationBuildable {
14 | func build(_ observation: Observation, mapping: HKSampleMapping) throws {
15 | guard let mapping = mapping.quantitySampleMapping[self.quantityType] else {
16 | throw HealthKitOnFHIRError.notSupported
17 | }
18 | for code in mapping.codings {
19 | observation.appendCoding(code.coding)
20 | }
21 | observation.setValue(quantity.buildQuantity(mapping: mapping))
22 | }
23 | }
24 |
25 |
26 | extension HKQuantity {
27 | func buildObservationComponent(
28 | for quantityType: HKQuantityType,
29 | mappings: [HKQuantityType: HKQuantitySampleMapping] = HKQuantitySampleMapping.default
30 | ) throws -> ObservationComponent {
31 | guard let mapping = mappings[quantityType] else {
32 | throw HealthKitOnFHIRError.notSupported
33 | }
34 | return buildObservationComponent(mapping: mapping)
35 | }
36 |
37 | func buildObservationComponent(mapping: HKQuantitySampleMapping) -> ObservationComponent {
38 | ObservationComponent(
39 | code: CodeableConcept(coding: mapping.codings.map(\.coding)),
40 | value: .quantity(buildQuantity(mapping: mapping))
41 | )
42 | }
43 |
44 | func buildQuantity(mapping: HKQuantitySampleMapping) -> Quantity {
45 | Quantity(
46 | code: mapping.unit.code?.asFHIRStringPrimitive(),
47 | system: mapping.unit.system?.asFHIRURIPrimitive(),
48 | unit: mapping.unit.unit.asFHIRStringPrimitive(),
49 | value: self.doubleValue(for: mapping.unit.hkunit).asFHIRDecimalPrimitive()
50 | )
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/FHIR Extensions/FHIR+Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2025 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import Foundation
10 | import ModelsR4
11 |
12 |
13 | extension DateTime {
14 | /// Constructs a new `DateTime` from an `Instant`
15 | @inlinable
16 | public init(instant: Instant) throws {
17 | self.init(
18 | date: FHIRDate(instantDate: instant.date),
19 | time: instant.time,
20 | timezone: instant.timeZone
21 | )
22 | }
23 | }
24 |
25 |
26 | extension FHIRDate {
27 | /// Constructs a new `FHIRDate` from an `InstantDate`
28 | @inlinable
29 | init(instantDate: InstantDate) {
30 | self.init(
31 | year: instantDate.year,
32 | month: instantDate.month,
33 | day: instantDate.day
34 | )
35 | }
36 | }
37 |
38 |
39 | extension Decimal {
40 | /// Creates a `FHIRPrimitive` with the value of the `Decimal`.
41 | @inlinable
42 | public func asFHIRPrimitive() -> FHIRPrimitive {
43 | FHIRPrimitive(FHIRDecimal(self))
44 | }
45 | }
46 |
47 |
48 | extension FHIRPrimitive where PrimitiveType == FHIRURI {
49 | /// Creates a new `FHIRPrimitive`, by appending the specified component.
50 | @inlinable
51 | public func appending(component: some StringProtocol) -> Self {
52 | guard let value else {
53 | return self
54 | }
55 | return Self(FHIRURI(value.url.appending(component: component)))
56 | }
57 |
58 | /// Creates a new `FHIRPrimitive`, by appending the specified components.
59 | @inlinable
60 | public func appending(components: [some StringProtocol]) -> Self {
61 | guard let value else {
62 | return self
63 | }
64 | var url = value.url
65 | for component in components {
66 | url = url.appending(component: component)
67 | }
68 | return Self(FHIRURI(url))
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/.github/workflows/build-and-test.yml:
--------------------------------------------------------------------------------
1 | #
2 | # This source file is part of the Stanford Spezi open source project
3 | #
4 | # SPDX-FileCopyrightText: 2025 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | #
6 | # SPDX-License-Identifier: MIT
7 | #
8 |
9 | name: Build and Test
10 |
11 | on:
12 | push:
13 | branches:
14 | - main
15 | pull_request:
16 | workflow_dispatch:
17 |
18 | concurrency:
19 | group: Build-and-Test-${{ github.ref }}
20 | cancel-in-progress: true
21 |
22 | jobs:
23 | package_tests:
24 | name: Build and Test Swift Package ${{ matrix.platform.name }} (${{ matrix.config }})
25 | uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
26 | strategy:
27 | matrix:
28 | config: [Debug, Release]
29 | platform:
30 | - name: iOS
31 | destination: 'platform=iOS Simulator,name=iPhone 17 Pro'
32 | - name: macOS
33 | destination: 'platform=macOS,name=My Mac'
34 | - name: watchOS
35 | destination: 'platform=watchOS Simulator,name=Apple Watch Series 11 (46mm)'
36 | with:
37 | runsonlabels: '["macOS", "self-hosted"]'
38 | scheme: HealthKitOnFHIR
39 | destination: ${{ matrix.platform.destination }}
40 | buildConfig: ${{ matrix.config }}
41 | resultBundle: ${{ format('HealthKitOnFHIR-{0}-{1}.xcresult', matrix.platform.name, matrix.config) }}
42 | artifactname: ${{ format('HealthKitOnFHIR-{0}-{1}.xcresult', matrix.platform.name, matrix.config) }}
43 | ui_tests:
44 | name: Build and Test UI Tests ${{ matrix.platform.name }} (${{ matrix.config }})
45 | uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
46 | strategy:
47 | matrix:
48 | config: [Debug, Release]
49 | platform:
50 | - name: iOS
51 | destination: 'platform=iOS Simulator,name=iPhone 17 Pro'
52 | with:
53 | runsonlabels: '["macOS", "self-hosted"]'
54 | path: 'Tests/UITests'
55 | scheme: TestApp
56 | destination: ${{ matrix.platform.destination }}
57 | resultBundle: ${{ format('TestApp-{0}-{1}.xcresult', matrix.platform.name, matrix.config) }}
58 | artifactname: ${{ format('TestApp-{0}-{1}.xcresult', matrix.platform.name, matrix.config) }}
59 | uploadcoveragereport:
60 | name: Upload Coverage Report
61 | needs: [package_tests, ui_tests]
62 | uses: StanfordBDHG/.github/.github/workflows/create-and-upload-coverage-report.yml@v2
63 | with:
64 | coveragereports: HealthKitOnFHIR-*.xcresult TestApp-*.xcresult
65 | secrets:
66 | token: ${{ secrets.CODECOV_TOKEN }}
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HKSampleMapping/HKStateOfMindSampleMapping.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2025 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 |
11 |
12 | /// An ``HKStateOfMindMapping`` allows developers to customize the mapping of `HKStateOfMind` samples to FHIR Observations.
13 | public struct HKStateOfMindMapping: Decodable, Sendable {
14 | /// A default instance of an ``HKStateOfMindMapping`` allowing developers to customize the ``HKStateOfMindMapping``
15 | /// The default values are loaded from the `HKSampleMapping.json` resource in the ``HealthKitOnFHIR`` Swift Package.
16 | public static let `default` = HKSampleMapping.default.stateOfMindMapping
17 |
18 | /// The FHIR codings defined as ``MappedCode``s to be used for `HKStateOfMind` samples
19 | public var codings: [MappedCode]
20 | /// The FHIR categories defined as ``MappedCode``s to be used for `HKStateOfMind` samples
21 | public var categories: [MappedCode]
22 | /// The mapping for a `HKStateOfMind` sample's kind.
23 | public var kind: HKCategorySampleMapping
24 | /// The mapping for a `HKStateOfMind` sample's valence.
25 | public var valence: HKCategorySampleMapping
26 | /// The mapping for a `HKStateOfMind` sample's valence classification.
27 | public var valenceClassification: HKCategorySampleMapping
28 | /// The mapping for a `HKStateOfMind` sample's label.
29 | public var label: HKCategorySampleMapping
30 | /// The mapping for a `HKStateOfMind` sample's association.
31 | public var association: HKCategorySampleMapping
32 |
33 |
34 | /// An ``HKWorkoutSampleMapping`` allows developers to customize the mapping of `HKStateOfMind`s to FHIR observations.
35 | /// - Parameters:
36 | /// - codings: The FHIR codings defined as ``MappedCode``s used for the `HKStateOfMind` sample
37 | /// - categories: The FHIR categories defined as ``MappedCode``s used for the `HKStateOfMind` sample
38 | public init(
39 | codings: [MappedCode] = Self.default.codings,
40 | categories: [MappedCode] = Self.default.categories,
41 | kind: HKCategorySampleMapping = Self.default.kind,
42 | valence: HKCategorySampleMapping = Self.default.valence,
43 | valenceClassification: HKCategorySampleMapping = Self.default.valenceClassification,
44 | label: HKCategorySampleMapping = Self.default.label,
45 | association: HKCategorySampleMapping = Self.default.association
46 | ) {
47 | self.codings = codings
48 | self.categories = categories
49 | self.kind = kind
50 | self.valence = valence
51 | self.valenceClassification = valenceClassification
52 | self.label = label
53 | self.association = association
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Tests/UITests/TestApp/Views/CreateWorkoutView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open-source project
3 | //
4 | // SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 | import SwiftUI
11 |
12 | struct CreateWorkoutView: View {
13 | @State private var manager = HealthKitManager()
14 |
15 | @State private var json = ""
16 | @State private var showingSheet = false
17 |
18 | var body: some View {
19 | Form {
20 | Section {
21 | Button("Create Sample Workout") {
22 | Task {
23 | await createWorkout()
24 | showingSheet.toggle()
25 | }
26 | }
27 | }
28 | }
29 | .sheet(isPresented: $showingSheet) {
30 | JSONView(json: $json)
31 | }
32 | .navigationBarTitle("Create Workout")
33 | }
34 |
35 | /// Uses `HKWorkoutBuilder` to build an `HKWorkout`
36 | private func buildWorkout(
37 | startDate: Date,
38 | endDate: Date,
39 | activityType: HKWorkoutActivityType
40 | ) async throws -> HKWorkout {
41 | guard let healthStore = self.manager.healthStore else {
42 | throw HKError(.errorHealthDataUnavailable)
43 | }
44 | let configuration = HKWorkoutConfiguration()
45 | configuration.activityType = activityType
46 | configuration.locationType = .indoor
47 | let workoutBuilder = HKWorkoutBuilder(
48 | healthStore: healthStore,
49 | configuration: configuration,
50 | device: nil
51 | )
52 | try await workoutBuilder.beginCollection(at: startDate)
53 | try await workoutBuilder.endCollection(at: endDate)
54 | if let workout = try await workoutBuilder.finishWorkout() {
55 | return workout
56 | } else {
57 | throw HKError(.errorHealthDataUnavailable)
58 | }
59 | }
60 |
61 | private func createWorkout() async {
62 | do {
63 | try await manager.requestWorkoutAuthorization()
64 |
65 | /// Use `HKWorkoutBuilder` to create the workout
66 | let workout = try await buildWorkout(
67 | startDate: Date(),
68 | endDate: Date().addingTimeInterval(3600),
69 | activityType: .running
70 | )
71 |
72 | let observation = try workout.resource().get()
73 |
74 | let encoder = JSONEncoder()
75 | encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
76 | let data = try encoder.encode(observation)
77 | self.json = String(decoding: data, as: UTF8.self)
78 | } catch {
79 | print(error)
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Tests/UITests/TestApp/Views/HealthRecordsTestView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open-source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | @preconcurrency import HealthKit
10 | import ModelsR4
11 | import SwiftUI
12 |
13 |
14 | struct HealthRecordsTestView: View {
15 | @State private var manager = HealthKitManager()
16 |
17 | @State private var json = ""
18 | @State private var showingSheet = false
19 |
20 | let recordTypes = [
21 | "HKClinicalTypeIdentifierAllergyRecord": "Allergies",
22 | "HKClinicalTypeIdentifierConditionRecord": "Conditions",
23 | "HKClinicalTypeIdentifierCoverageRecord": "Coverage",
24 | "HKClinicalTypeIdentifierImmunizationRecord": "Immunizations",
25 | "HKClinicalTypeIdentifierLabResultRecord": "Lab Results",
26 | "HKClinicalTypeIdentifierMedicationRecord": "Medications",
27 | "HKClinicalTypeIdentifierProcedureRecord": "Procedures",
28 | "HKClinicalTypeIdentifierVitalSignRecord": "Vital Signs"
29 | ]
30 |
31 |
32 | var body: some View {
33 | Form {
34 | Section {
35 | ForEach(recordTypes.sorted(by: <), id: \.key) { key, value in
36 | Button("Read \(value)") {
37 | _Concurrency.Task { // Models.R4 also has a `Task`
38 | do {
39 | let type = HKClinicalTypeIdentifier(rawValue: key)
40 | try await readHealthRecords(type: type)
41 | } catch {
42 | print(error)
43 | }
44 | showingSheet.toggle()
45 | }
46 | }
47 | .sheet(isPresented: $showingSheet) {
48 | JSONView(json: $json)
49 | }
50 | }
51 | }
52 | }
53 | .navigationBarTitle("Read Data")
54 | }
55 |
56 |
57 | private func readHealthRecords(type: HKClinicalTypeIdentifier) async throws {
58 | try await manager.requestHealthRecordsAuthorization()
59 |
60 | let resources: [Resource] = try await manager.readHealthRecords(type: type)
61 | .compactMap { sample in
62 | do {
63 | return try sample.resource().get()
64 | } catch {
65 | print(error.localizedDescription)
66 | }
67 | return nil
68 | }
69 |
70 | let encoder = JSONEncoder()
71 | encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
72 |
73 | guard let data = try? encoder.encode(resources) else {
74 | return
75 | }
76 |
77 | self.json = String(decoding: data, as: UTF8.self)
78 | }
79 | }
80 |
81 |
82 | struct HealthRecordsTestView_Previews: PreviewProvider {
83 | static var previews: some View {
84 | HealthRecordsTestView()
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HKSampleMapping/MappedUnit.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 |
11 |
12 | /// A ``MappedUnit`` instance is used to specify a unit mapping for FHIR observations mapped from HealthKit's `HKUnit`s.
13 | public struct MappedUnit: Decodable, Sendable {
14 | private enum CodingKeys: String, CodingKey {
15 | case hkunit
16 | case unit
17 | case system
18 | case code
19 | }
20 |
21 |
22 | /// The specified `HKUnit` that should be mapped.
23 | public var hkunit: HKUnit
24 | /// Unit representation.
25 | public var unit: String
26 | /// Identity of the terminology system.
27 | public private(set) var system: URL?
28 | /// Representation defined by the system.
29 | public private(set) var code: String?
30 |
31 |
32 | public init(from decoder: any Decoder) throws {
33 | let values = try decoder.container(keyedBy: CodingKeys.self)
34 | let hkunit = try HKUnit(from: values.decode(String.self, forKey: .hkunit))
35 | let unit = try values.decode(String.self, forKey: .unit)
36 | guard let system = try values.decodeIfPresent(URL.self, forKey: .system),
37 | let code = try values.decodeIfPresent(String.self, forKey: .code) else {
38 | self.init(
39 | hkunit: hkunit,
40 | unit: unit
41 | )
42 | return
43 | }
44 |
45 | self.init(
46 | hkunit: hkunit,
47 | unit: unit,
48 | system: system,
49 | code: code
50 | )
51 | }
52 |
53 | /// A ``MappedUnit`` instance is used to specify a unit mapping for FHIR observations mapped from HealthKit's `HKUnit`s.
54 | /// - Parameters:
55 | /// - hkunit: The specified `HKUnit` that should be mapped.
56 | /// - unit: Unit representation.
57 | public init(
58 | hkunit: HKUnit,
59 | unit: String
60 | ) {
61 | self.hkunit = hkunit
62 | self.unit = unit
63 | }
64 |
65 | /// A ``MappedUnit`` instance is used to specify a unit mapping for FHIR observations mapped from HealthKit's `HKUnit`s.
66 | /// - Parameters:
67 | /// - hkunit: The specified `HKUnit` that should be mapped.
68 | /// - unit: Unit representation.
69 | /// - system: Identity of the terminology system.
70 | /// - code: Representation defined by the system.
71 | public init(
72 | hkunit: HKUnit,
73 | unit: String,
74 | system: URL,
75 | code: String
76 | ) {
77 | self.hkunit = hkunit
78 | self.unit = unit
79 | self.system = system
80 | self.code = code
81 | }
82 |
83 |
84 | /// Update the system and code from the ``MappedUnit`` instance.
85 | /// - Parameters:
86 | /// - system: Identity of the terminology system.
87 | /// - code: Representation defined by the system.
88 | mutating func update(system: URL, code: String) {
89 | self.system = system
90 | self.code = code
91 | }
92 |
93 | /// Remove the system and code from the ``MappedUnit`` instance.
94 | mutating func removeSystemAndCode() {
95 | system = nil
96 | code = nil
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Tests/UITests/TestApp/HKElectrocardiogram+Context.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open-source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 | import HealthKitOnFHIR
11 | import SwiftUI
12 |
13 |
14 | extension HKElectrocardiogram {
15 | static let correlatedSymptomTypes: [HKCategoryType] = {
16 | // We disable the SwiftLint force unwrap rule here as all initializers use Apple's constants.
17 | // swiftlint:disable force_unwrapping
18 | [
19 | HKSampleType.categoryType(forIdentifier: HKCategoryTypeIdentifier.rapidPoundingOrFlutteringHeartbeat)!,
20 | HKSampleType.categoryType(forIdentifier: HKCategoryTypeIdentifier.skippedHeartbeat)!,
21 | HKSampleType.categoryType(forIdentifier: HKCategoryTypeIdentifier.fatigue)!,
22 | HKSampleType.categoryType(forIdentifier: HKCategoryTypeIdentifier.shortnessOfBreath)!,
23 | HKSampleType.categoryType(forIdentifier: HKCategoryTypeIdentifier.chestTightnessOrPain)!,
24 | HKSampleType.categoryType(forIdentifier: HKCategoryTypeIdentifier.fainting)!,
25 | HKSampleType.categoryType(forIdentifier: HKCategoryTypeIdentifier.dizziness)!
26 | ]
27 | // swiftlint:enable force_unwrapping
28 | }()
29 |
30 |
31 | func symptoms(from healthStore: HKHealthStore) async throws -> Symptoms {
32 | let predicate = HKQuery.predicateForObjectsAssociated(electrocardiogram: self)
33 |
34 | try await healthStore.requestAuthorization(toShare: [], read: Set(HKElectrocardiogram.correlatedSymptomTypes))
35 |
36 | var symptoms: Symptoms = [:]
37 |
38 | if symptomsStatus == .present {
39 | for sampleType in HKElectrocardiogram.correlatedSymptomTypes {
40 | // Create the descriptor.
41 | let sampleQueryDescriptor = HKSampleQueryDescriptor(
42 | predicates: [
43 | .sample(type: sampleType, predicate: predicate)
44 | ],
45 | sortDescriptors: [
46 | SortDescriptor(\.endDate, order: .reverse)
47 | ]
48 | )
49 |
50 | guard let sample = try await sampleQueryDescriptor.result(for: healthStore).first,
51 | let categorySample = sample as? HKCategorySample else {
52 | continue
53 | }
54 | symptoms[categorySample.categoryType] = HKCategoryValueSeverity(rawValue: categorySample.value)
55 | }
56 | }
57 |
58 | return symptoms
59 | }
60 |
61 | func voltageMeasurements(from healthStore: HKHealthStore) async throws -> VoltageMeasurements {
62 | var voltageMeasurements: VoltageMeasurements = []
63 | voltageMeasurements.reserveCapacity(numberOfVoltageMeasurements)
64 |
65 | let electrocardiogramQueryDescriptor = HKElectrocardiogramQueryDescriptor(self)
66 |
67 | for try await measurement in electrocardiogramQueryDescriptor.results(for: healthStore) {
68 | if let voltageQuantity = measurement.quantity(for: .appleWatchSimilarToLeadI) {
69 | voltageMeasurements.append((measurement.timeSinceSampleStart, voltageQuantity))
70 | }
71 | }
72 |
73 | return voltageMeasurements
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Tests/UITests/TestApp/Views/CheckMappingCompletenessView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open-source project
3 | //
4 | // SPDX-FileCopyrightText: 2025 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import Foundation
10 | import HealthKitOnFHIR
11 | import SpeziHealthKit
12 | import SwiftUI
13 |
14 |
15 | struct CheckMappingCompletenessView: View {
16 | private struct TestResult {
17 | struct Entry: Hashable {
18 | let typeIdentifier: String
19 | let unitString: String?
20 | }
21 | var missingQuantityTypes = Set()
22 | var missingCategoryTypes = Set()
23 | var missingCorrelationTypes = Set()
24 |
25 | var isEmpty: Bool {
26 | missingQuantityTypes.isEmpty && missingCategoryTypes.isEmpty && missingCorrelationTypes.isEmpty
27 | }
28 | }
29 |
30 |
31 | var body: some View {
32 | Form {
33 | let testResult = runCheck()
34 | Section {
35 | Text(testResult.isEmpty ? "All Fine!" : "Missing Mapping Entries!")
36 | }
37 | makeSection(title: "Missing Quantity Types", for: testResult.missingQuantityTypes)
38 | makeSection(title: "Missing Category Types", for: testResult.missingCategoryTypes)
39 | makeSection(title: "Missing Correlation Types", for: testResult.missingCorrelationTypes)
40 | }
41 | }
42 |
43 | @ViewBuilder
44 | private func makeSection(title: String, for types: some Collection) -> some View {
45 | if !types.isEmpty {
46 | Section(title) {
47 | ForEach(types.sorted(by: { $0.typeIdentifier < $1.typeIdentifier }), id: \.self) { entry in
48 | HStack {
49 | Text(entry.typeIdentifier)
50 | if let unitString = entry.unitString {
51 | Spacer()
52 | Text(unitString)
53 | .font(.caption)
54 | .foregroundStyle(.secondary)
55 | .monospaced()
56 | }
57 | }
58 | }
59 | }
60 | }
61 | }
62 |
63 | private func runCheck() -> TestResult {
64 | var result = TestResult()
65 | let mappings = HKSampleMapping.default
66 | for type in HKQuantityType.allKnownQuantities {
67 | guard mappings.quantitySampleMapping[type] == nil else {
68 | continue
69 | }
70 | result.missingQuantityTypes.insert(.init(
71 | typeIdentifier: type.identifier,
72 | unitString: type.sampleType.flatMap { $0 as? SampleType }?.displayUnit.unitString
73 | ))
74 | }
75 | for type in HKCategoryType.allKnownCategories {
76 | guard mappings.categorySampleMapping[type] == nil else {
77 | continue
78 | }
79 | result.missingCategoryTypes.insert(.init(typeIdentifier: type.identifier, unitString: nil))
80 | }
81 | for type in HKCorrelationType.allKnownCorrelations {
82 | guard mappings.correlationMapping[type] == nil else {
83 | continue
84 | }
85 | result.missingCorrelationTypes.insert(.init(typeIdentifier: type.identifier, unitString: nil))
86 | }
87 | return result
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/FHIR Extensions/Resource+Mutation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import Foundation
10 | import HealthKit
11 | import ModelsR4
12 |
13 |
14 | /// we need this as a protocol extension, bc we can't use `Self` in the KeyPath type if it's a normal extension on eg Resource.
15 | @_marker
16 | @_documentation(visibility: internal)
17 | public protocol FHIRResourceMutationExtensions {}
18 |
19 | extension ModelsR4.FHIRAbstractResource: FHIRResourceMutationExtensions {}
20 | extension ModelsR4.Element: FHIRResourceMutationExtensions {}
21 |
22 |
23 | extension FHIRResourceMutationExtensions {
24 | /// Appends an element to a `Collection`-typed property.
25 | @inlinable
26 | public func appendElement(_ element: C.Element, to keyPath: ReferenceWritableKeyPath) {
27 | appendElements(CollectionOfOne(element), to: keyPath)
28 | }
29 |
30 | /// Appends multiple elements to a `Collection`-typed property.
31 | @inlinable
32 | public func appendElements(
33 | _ elements: some Collection,
34 | to keyPath: ReferenceWritableKeyPath
35 | ) {
36 | if self[keyPath: keyPath] == nil {
37 | self[keyPath: keyPath] = C()
38 | self[keyPath: keyPath]!.reserveCapacity(elements.count) // swiftlint:disable:this force_unwrapping
39 | } else {
40 | self[keyPath: keyPath]!.reserveCapacity(self[keyPath: keyPath]!.count + elements.count) // swiftlint:disable:this force_unwrapping
41 | }
42 | self[keyPath: keyPath]?.append(contentsOf: elements)
43 | }
44 |
45 | /// Removes the first element of the property that matches the predicate.
46 | ///
47 | /// Also sets the property to `nil` if there are no elements remaining after the removal.
48 | ///
49 | /// - returns: the removed element, if any.
50 | @inlinable
51 | public func removeFirstElement(
52 | of keyPath: ReferenceWritableKeyPath,
53 | where predicate: (C.Element) -> Bool
54 | ) -> C.Element? {
55 | guard var elements = self[keyPath: keyPath], let idx = elements.firstIndex(where: predicate) else {
56 | return nil
57 | }
58 | let element = elements.remove(at: idx)
59 | self[keyPath: keyPath] = elements.isEmpty ? nil : elements
60 | return element
61 | }
62 |
63 | /// Removes all elements of the property that matche the predicate.
64 | ///
65 | /// Also sets the property to `nil` if there are no elements remaining after the removal.
66 | ///
67 | /// - returns: the removed elements, if any.
68 | @inlinable
69 | public func removeAllElements(
70 | of keyPath: ReferenceWritableKeyPath,
71 | where predicate: (C.Element) -> Bool
72 | ) -> [C.Element]? { // swiftlint:disable:this discouraged_optional_collection
73 | guard var elements = self[keyPath: keyPath] else {
74 | return nil
75 | }
76 | let indices = elements.indices.filter { predicate(elements[$0]) }
77 | let removedElements = indices.map { elements[$0] }
78 | elements.removeElements(at: indices)
79 | self[keyPath: keyPath] = elements.isEmpty ? nil : elements
80 | return removedElements
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Tests/HealthKitOnFHIRTests/CustomMappingsTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 | @testable import HealthKitOnFHIR
11 | import ModelsR4
12 | import Testing
13 |
14 |
15 | @MainActor // to work around https://github.com/apple/FHIRModels/issues/36
16 | struct CustomMappingsTests {
17 | @Test
18 | func customMappings() throws {
19 | // swiftlint:disable:previous function_body_length
20 | // We disable the function body length as this is a test case
21 | let quantitySample = HKQuantitySample(
22 | type: HKQuantityType(.bodyMass),
23 | quantity: HKQuantity(unit: .gramUnit(with: .kilo), doubleValue: 60),
24 | start: Date(),
25 | end: Date()
26 | )
27 |
28 | let ucumSystem = try #require(URL(string: "http://unitsofmeasure.org"))
29 | let stanfordURL = try #require(URL(string: "http://stanford.edu"))
30 |
31 | var customMapping = [
32 | HKQuantityType(.bodyMass):
33 | HKQuantitySampleMapping(
34 | codings: [
35 | MappedCode(
36 | code: "SU-01",
37 | display: "Stanford University",
38 | system: stanfordURL
39 | )
40 | ],
41 | unit: MappedUnit(
42 | hkunit: .ounce(),
43 | unit: "oz",
44 | system: ucumSystem,
45 | code: "[oz_av]"
46 | )
47 | )
48 | ]
49 | customMapping[HKQuantityType(.bodyMass)]?.unit.removeSystemAndCode()
50 | #expect(customMapping[HKQuantityType(.bodyMass)]?.unit.system == nil)
51 | #expect(customMapping[HKQuantityType(.bodyMass)]?.unit.code == nil)
52 |
53 | customMapping[HKQuantityType(.bodyMass)]?.unit.update(system: ucumSystem, code: "[oz_av]")
54 | #expect(customMapping[HKQuantityType(.bodyMass)]?.unit.system == ucumSystem)
55 | #expect(customMapping[HKQuantityType(.bodyMass)]?.unit.code == "[oz_av]")
56 |
57 | let hkSampleMapping = HKSampleMapping(quantitySampleMapping: customMapping)
58 |
59 | let observation = try quantitySample
60 | .resource(withMapping: hkSampleMapping)
61 | .get(if: Observation.self)
62 |
63 | #expect(quantitySample.quantityType.codes == [
64 | Coding(
65 | code: "29463-7",
66 | display: "Body weight",
67 | system: FHIRPrimitive(FHIRURI(stringLiteral: "http://loinc.org"))
68 | ),
69 | Coding(
70 | code: "HKQuantityTypeIdentifierBodyMass",
71 | display: "Body Mass",
72 | system: FHIRPrimitive(FHIRURI(stringLiteral: "http://developer.apple.com/documentation/healthkit"))
73 | )
74 | ])
75 |
76 | #expect(observation?.code.coding == [
77 | Coding(
78 | code: "SU-01",
79 | display: "Stanford University",
80 | system: FHIRPrimitive(FHIRURI(stanfordURL))
81 | )
82 | ])
83 |
84 | #expect(observation?.value == .quantity(Quantity(
85 | code: "[oz_av]",
86 | system: "http://unitsofmeasure.org",
87 | unit: "oz",
88 | value: 2116.43771697482496.asFHIRDecimalPrimitive()
89 | )))
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:6.2
2 |
3 | //
4 | // This source file is part of the HealthKitOnFHIR open source project
5 | //
6 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
7 | //
8 | // SPDX-License-Identifier: MIT
9 | //
10 |
11 | import CompilerPluginSupport
12 | import class Foundation.ProcessInfo
13 | import PackageDescription
14 |
15 |
16 | let package = Package(
17 | name: "HealthKitOnFHIR",
18 | defaultLocalization: "en",
19 | platforms: [
20 | .iOS(.v17),
21 | .macOS(.v14),
22 | .watchOS(.v10)
23 | ],
24 | products: [
25 | .library(name: "HealthKitOnFHIR", targets: ["HealthKitOnFHIR"])
26 | ],
27 | dependencies: [
28 | .package(url: "https://github.com/apple/FHIRModels.git", .upToNextMajor(from: "0.7.0")),
29 | .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "602.0.0"),
30 | .package(url: "https://github.com/apple/swift-algorithms.git", from: "1.2.1")
31 | ] + swiftLintPackage(),
32 | targets: [
33 | .macro(
34 | name: "HealthKitOnFHIRMacrosImpl",
35 | dependencies: [
36 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
37 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
38 | .product(name: "SwiftDiagnostics", package: "swift-syntax"),
39 | .product(name: "Algorithms", package: "swift-algorithms")
40 | ],
41 | swiftSettings: [.enableUpcomingFeature("ExistentialAny")]
42 | ),
43 | .target(
44 | name: "HealthKitOnFHIRMacros",
45 | dependencies: [
46 | .target(name: "HealthKitOnFHIRMacrosImpl")
47 | ]
48 | ),
49 | .target(
50 | name: "HealthKitOnFHIR",
51 | dependencies: [
52 | .target(name: "HealthKitOnFHIRMacros"),
53 | .product(name: "ModelsR4", package: "FHIRModels")
54 | ],
55 | resources: [
56 | .process("Resources")
57 | ],
58 | swiftSettings: [.enableUpcomingFeature("ExistentialAny")],
59 | plugins: [] + swiftLintPlugin()
60 | ),
61 | .testTarget(
62 | name: "HealthKitOnFHIRTests",
63 | dependencies: [
64 | .target(name: "HealthKitOnFHIR")
65 | ],
66 | swiftSettings: [.enableUpcomingFeature("ExistentialAny")],
67 | plugins: [] + swiftLintPlugin()
68 | ),
69 | .testTarget(
70 | name: "HealthKitOnFHIRMacrosTests",
71 | dependencies: [
72 | .target(name: "HealthKitOnFHIRMacros"),
73 | .target(name: "HealthKitOnFHIRMacrosImpl"),
74 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
75 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax")
76 | ]
77 | )
78 | ]
79 | )
80 |
81 |
82 | func swiftLintPlugin() -> [Target.PluginUsage] {
83 | // Fully quit Xcode and open again with `open --env SPEZI_DEVELOPMENT_SWIFTLINT /Applications/Xcode.app`
84 | if ProcessInfo.processInfo.environment["SPEZI_DEVELOPMENT_SWIFTLINT"] != nil {
85 | [.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLint")]
86 | } else {
87 | []
88 | }
89 | }
90 |
91 | func swiftLintPackage() -> [PackageDescription.Package.Dependency] {
92 | if ProcessInfo.processInfo.environment["SPEZI_DEVELOPMENT_SWIFTLINT"] != nil {
93 | [.package(url: "https://github.com/realm/SwiftLint.git", from: "0.55.1")]
94 | } else {
95 | []
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/Tests/HealthKitOnFHIRTests/HKCorrelationTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 | @testable import HealthKitOnFHIR
11 | import ModelsR4
12 | import Testing
13 |
14 |
15 | @MainActor // to work around https://github.com/apple/FHIRModels/issues/36
16 | struct HKCorrelationTests {
17 | var startDate: Date {
18 | get throws {
19 | let dateComponents = DateComponents(year: 1891, month: 10, day: 1, hour: 12, minute: 0, second: 0) // Date Stanford University opened (https://www.stanford.edu/about/history/)
20 | return try #require(Calendar.current.date(from: dateComponents))
21 | }
22 | }
23 |
24 | var endDate: Date {
25 | get throws {
26 | let dateComponents = DateComponents(year: 1891, month: 10, day: 1, hour: 12, minute: 0, second: 42)
27 | return try #require(Calendar.current.date(from: dateComponents))
28 | }
29 | }
30 |
31 | @Test
32 | func bloodPressureCorrelation() throws {
33 | let systolicBloodPressure = HKQuantitySample(
34 | type: HKQuantityType(.bloodPressureSystolic),
35 | quantity: HKQuantity(unit: .millimeterOfMercury(), doubleValue: 120),
36 | start: try startDate,
37 | end: try endDate
38 | )
39 |
40 | let diastolicBloodPressure = HKQuantitySample(
41 | type: HKQuantityType(.bloodPressureDiastolic),
42 | quantity: HKQuantity(unit: .millimeterOfMercury(), doubleValue: 80),
43 | start: try startDate,
44 | end: try endDate
45 | )
46 |
47 | let correlation = HKCorrelation(
48 | type: HKCorrelationType(.bloodPressure),
49 | start: try startDate,
50 | end: try endDate,
51 | objects: [systolicBloodPressure, diastolicBloodPressure]
52 | )
53 |
54 | let observation = try #require(correlation.resource().get(if: Observation.self))
55 |
56 | #expect(1 == observation.component?.filter {
57 | $0.value == .quantity(
58 | Quantity(
59 | code: "mm[Hg]",
60 | system: "http://unitsofmeasure.org".asFHIRURIPrimitive(),
61 | unit: "mmHg",
62 | value: 120.asFHIRDecimalPrimitive()
63 | )
64 | )
65 | }.count)
66 |
67 | #expect(1 == observation.component?.filter {
68 | $0.value == .quantity(
69 | Quantity(
70 | code: "mm[Hg]",
71 | system: "http://unitsofmeasure.org".asFHIRURIPrimitive(),
72 | unit: "mmHg",
73 | value: 80.asFHIRDecimalPrimitive()
74 | )
75 | )
76 | }.count)
77 | }
78 |
79 | @Test(.disabled())
80 | func unsupportedCorrelation() throws {
81 | // Food correlations are not currently supported
82 | let vitaminC = HKQuantitySample(
83 | type: HKQuantityType(.dietaryVitaminC),
84 | quantity: HKQuantity(unit: .gram(), doubleValue: 1),
85 | start: try startDate,
86 | end: try endDate
87 | )
88 |
89 | let correlation = HKCorrelation(
90 | type: HKCorrelationType(.food),
91 | start: try startDate,
92 | end: try endDate,
93 | objects: [vitaminC]
94 | )
95 | #expect(throws: HealthKitOnFHIRError.self) {
96 | try correlation.resource()
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIRMacrosImpl/AddDisplayPropertyMacro.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2025 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import Algorithms
10 | import SwiftDiagnostics
11 | import SwiftSyntax
12 | import SwiftSyntaxMacros
13 |
14 |
15 | /// The `@SynthesizeDisplayPropertyMacro` macro.
16 | public struct SynthesizeDisplayPropertyMacro {}
17 |
18 | extension SynthesizeDisplayPropertyMacro: MemberMacro {
19 | public static func expansion(
20 | of node: AttributeSyntax,
21 | providingMembersOf declaration: some DeclGroupSyntax,
22 | conformingTo protocols: [TypeSyntax],
23 | in context: some MacroExpansionContext
24 | ) throws -> [DeclSyntax] {
25 | let caseNames: [String] = try { () -> [String] in
26 | guard let argsList = node.arguments?.as(LabeledExprListSyntax.self) else {
27 | throw MacroExpansionErrorMessage("missing arguments?")
28 | }
29 | return try argsList.dropFirst().map { syntax in
30 | if let syntax = syntax.expression.as((MemberAccessExprSyntax.self)) {
31 | return syntax.declName.baseName.text
32 | } else if let syntax = syntax.expression.as(StringLiteralExprSyntax.self) {
33 | return try syntax.segments.reduce(into: "") { partialResult, segment in
34 | switch segment {
35 | case .stringSegment(let segment):
36 | partialResult.append(contentsOf: segment.content.text)
37 | case .expressionSegment:
38 | throw MacroExpansionErrorMessage("Argument String isn't allowed to contain interpolations!")
39 | }
40 | }
41 | } else {
42 | throw MacroExpansionErrorMessage("Arhument must be an enum case expression!")
43 | }
44 | }
45 | }()
46 | let displayProperty = try VariableDeclSyntax("var display: String?") {
47 | SwitchExprSyntax(subject: "self" as ExprSyntax) {
48 | for name in caseNames {
49 | .switchCase(SwitchCaseSyntax("case .\(raw: name):") {
50 | #""\#(raw: displayText(for: name))""#
51 | })
52 | }
53 | SwitchCaseListSyntax.Element.switchCase(SwitchCaseSyntax("@unknown default:") {
54 | "nil" as ExprSyntax
55 | })
56 | }
57 | }
58 | return [DeclSyntax(fromProtocol: displayProperty)]
59 | }
60 |
61 | private static func displayText(for enumCaseName: String) -> String {
62 | let separatorIndices: [String.Index] = enumCaseName.indices.adjacentPairs().compactMap { lhsIdx, rhsIdx in
63 | enumCaseName[lhsIdx].isLowercase && enumCaseName[rhsIdx].isUppercase ? rhsIdx : nil
64 | }
65 | guard !separatorIndices.isEmpty else {
66 | return enumCaseName
67 | }
68 | let components = chain(CollectionOfOne(enumCaseName.startIndex), chain(separatorIndices, CollectionOfOne(enumCaseName.endIndex)))
69 | .adjacentPairs().map { enumCaseName[$0..<$1] }
70 | return components
71 | .map { component -> String in
72 | if component.allSatisfy(\.isUppercase) {
73 | // eg: we want "asleepREM" to become "asleep REM", i.e. the "REM" part should remain uppercase
74 | String(component)
75 | } else if component.allSatisfy(\.isLowercase) {
76 | component.lowercased()
77 | } else {
78 | String(component.lowercased())
79 | }
80 | }
81 | .joined(separator: " ")
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Tests/UITests/TestApp/Views/ExportDataView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open-source project
3 | //
4 | // SPDX-FileCopyrightText: 2025 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import Foundation
10 | import HealthKit
11 | import HealthKitOnFHIR
12 | import SwiftUI
13 |
14 |
15 | struct ExportDataView: View {
16 | private enum ViewState {
17 | case idle
18 | case processing
19 | case failed(any Error)
20 |
21 | var isIdle: Bool {
22 | switch self {
23 | case .idle:
24 | true
25 | case .processing, .failed:
26 | false
27 | }
28 | }
29 | }
30 |
31 | private let healthStore = HKHealthStore()
32 |
33 | @State private var viewState: ViewState = .idle
34 | @State private var generateResourcesDuration: TimeInterval?
35 |
36 | var body: some View {
37 | Form {
38 | Section("Actions") {
39 | actionsSectionContent
40 | }
41 | if let generateResourcesDuration {
42 | Section {
43 | LabeledContent("genResoueces", value: "\(generateResourcesDuration) sec")
44 | }
45 | }
46 | }
47 | }
48 |
49 | @ViewBuilder private var actionsSectionContent: some View {
50 | Button("Ask for Authorization") {
51 | runAsync {
52 | try await healthStore.requestAuthorization(
53 | toShare: [],
54 | read: [HKQuantityType(.activeEnergyBurned), HKQuantityType(.stepCount), HKQuantityType(.appleExerciseTime)]
55 | )
56 | }
57 | }
58 | .disabled(!viewState.isIdle)
59 | Button("Query Samples") {
60 | runAsync {
61 | let fetchStartTS = CACurrentMediaTime()
62 | let samples = try await healthStore.query(.init(.appleExerciseTime))
63 | let fetchEndTS = CACurrentMediaTime()
64 | print("did fetch samples (#=\(samples.count)) (took \(fetchEndTS - fetchStartTS) sec)")
65 | let mapResourcesStartTS = CACurrentMediaTime()
66 | _ = try samples.mapIntoResourceProxies()
67 | let mapResourcesEndTS = CACurrentMediaTime()
68 | print("did turn into resources (took \(mapResourcesEndTS - mapResourcesStartTS) sec)")
69 | await MainActor.run {
70 | generateResourcesDuration = mapResourcesEndTS - mapResourcesStartTS
71 | }
72 | }
73 | }
74 | .disabled(!viewState.isIdle)
75 | }
76 |
77 | private func runAsync(_ operation: @Sendable @escaping () async throws -> Void) {
78 | precondition(viewState.isIdle)
79 | Task {
80 | viewState = .processing
81 | do {
82 | try await operation()
83 | viewState = .idle
84 | } catch {
85 | viewState = .failed(error)
86 | }
87 | }
88 | }
89 | }
90 |
91 |
92 | extension HKHealthStore {
93 | func query(_ sampleType: HKQuantityType) async throws -> [HKQuantitySample] {
94 | let cal = Calendar.current
95 | let predicate = HKSamplePredicate.quantitySample(
96 | type: sampleType
97 | // predicate: HKQuery.predicateForSamples(
98 | // withStart: cal.startOfDay(for: .now),
99 | // end: cal.date(byAdding: .day, value: 1, to: cal.startOfDay(for: .now))!
100 | // )
101 | )
102 | let descriptor = HKSampleQueryDescriptor(
103 | predicates: [predicate],
104 | sortDescriptors: [SortDescriptor(\.startDate, order: .forward)]
105 | )
106 | return try await descriptor.result(for: self)
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/Tests/UITests/TestAppUITests/TestAppUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open-source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import XCTest
10 | import XCTestExtensions
11 | import XCTHealthKit
12 |
13 |
14 | class TestAppUITests: XCTestCase {
15 | override func setUpWithError() throws {
16 | try super.setUpWithError()
17 |
18 | continueAfterFailure = false
19 | }
20 |
21 |
22 | @MainActor
23 | func testHealthKitOnFHIR() throws {
24 | let app = XCUIApplication()
25 | app.launch()
26 |
27 | // Write Data
28 | XCTAssert(app.collectionViews.buttons["Write Data"].waitForExistence(timeout: 5))
29 | app.collectionViews.buttons["Write Data"].tap()
30 |
31 | XCTAssert(app.collectionViews.textFields["Number of steps..."].waitForExistence(timeout: 5))
32 | try app.collectionViews.textFields["Number of steps..."].enter(value: "2")
33 |
34 | app.collectionViews.buttons["Write Step Count"].tap()
35 |
36 | // Enable Apple Health Access if needed
37 | app.handleHealthKitAuthorization()
38 |
39 | // Check that the data is written
40 | XCTAssert(app.collectionViews.staticTexts["Data successfully written!"].waitForExistence(timeout: 5))
41 |
42 | // Return back to the main view
43 | app.navigationBars["Write Data"].buttons["HealthKitOnFHIR Tests"].tap()
44 |
45 | // Check that the data can be read
46 | app.collectionViews.buttons["Read Data"].tap()
47 | app.collectionViews.buttons["Read Step Count"].tap()
48 |
49 | // Dismiss results view
50 | app.swipeDown(velocity: XCUIGestureVelocity.fast)
51 | }
52 |
53 | @MainActor
54 | func testECGHealthKitMapping() throws {
55 | let app = XCUIApplication()
56 | app.launch()
57 |
58 | try launchAndAddSample(healthApp: .healthApp, .electrocardiogram())
59 |
60 | app.launch()
61 | XCTAssert(app.wait(for: .runningForeground, timeout: 6.0))
62 |
63 | XCTAssert(app.staticTexts["Electrocardiogram"].waitForExistence(timeout: 5))
64 | app.staticTexts["Electrocardiogram"].tap()
65 | XCTAssert(app.buttons["Read Electrocardiogram"].waitForExistence(timeout: 5))
66 | app.buttons["Read Electrocardiogram"].tap()
67 |
68 | // Enable Apple Health Access if needed
69 | app.handleHealthKitAuthorization()
70 |
71 | XCTAssert(app.staticTexts["Passed"].waitForExistence(timeout: 10))
72 |
73 | app.collectionViews.buttons["See JSON"].tap()
74 |
75 | // Dismiss results view
76 | app.swipeDown(velocity: XCUIGestureVelocity.fast)
77 | }
78 |
79 | @MainActor
80 | func testWorkoutMapping() throws {
81 | let app = XCUIApplication()
82 | app.launch()
83 |
84 | // Create Workout
85 | XCTAssert(app.collectionViews.buttons["Create Workout"].waitForExistence(timeout: 5))
86 | app.collectionViews.buttons["Create Workout"].tap()
87 | XCTAssert(app.collectionViews.buttons["Create Sample Workout"].waitForExistence(timeout: 5))
88 | app.collectionViews.buttons["Create Sample Workout"].tap()
89 |
90 | // Enable Apple Health Access if needed
91 | app.handleHealthKitAuthorization()
92 |
93 | // Dismiss results view
94 | app.swipeDown(velocity: XCUIGestureVelocity.fast)
95 | }
96 |
97 | @MainActor
98 | func testMappingCompleteness() throws {
99 | let app = XCUIApplication()
100 | app.launch()
101 |
102 | app.buttons["Mapping Completeness"].tap()
103 | XCTAssertTrue(app.staticTexts["All Fine!"].waitForExistence(timeout: 2))
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HealthKit Extensions/HKMetadataEnum+Coding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2025 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 | import HealthKitOnFHIRMacros
11 |
12 |
13 | @SynthesizeDisplayProperty(
14 | HKAppleECGAlgorithmVersion.self,
15 | .version1, .version2
16 | )
17 | extension HKAppleECGAlgorithmVersion: FHIRCodingConvertibleHKEnum {}
18 |
19 | @SynthesizeDisplayProperty(
20 | HKBloodGlucoseMealTime.self,
21 | .preprandial, .postprandial
22 | )
23 | extension HKBloodGlucoseMealTime: FHIRCodingConvertibleHKEnum {}
24 |
25 | @SynthesizeDisplayProperty(
26 | HKBodyTemperatureSensorLocation.self,
27 | .other, .armpit, .body, .ear, .finger, .gastroIntestinal,
28 | .mouth, .rectum, .toe, .earDrum, .temporalArtery, .forehead
29 | )
30 | extension HKBodyTemperatureSensorLocation: FHIRCodingConvertibleHKEnum {}
31 |
32 | @SynthesizeDisplayProperty(
33 | HKCyclingFunctionalThresholdPowerTestType.self,
34 | .maxExercise60Minute, .maxExercise20Minute, .rampTest, .predictionExercise)
35 | extension HKCyclingFunctionalThresholdPowerTestType: FHIRCodingConvertibleHKEnum {}
36 |
37 | @SynthesizeDisplayProperty(
38 | HKDevicePlacementSide.self,
39 | .unknown, .left, .right, .central
40 | )
41 | extension HKDevicePlacementSide: FHIRCodingConvertibleHKEnum {}
42 |
43 | @SynthesizeDisplayProperty(
44 | HKHeartRateMotionContext.self,
45 | .notSet, .sedentary, .active
46 | )
47 | extension HKHeartRateMotionContext: FHIRCodingConvertibleHKEnum {}
48 |
49 | @SynthesizeDisplayProperty(
50 | HKHeartRateRecoveryTestType.self,
51 | .maxExercise, .predictionSubMaxExercise, .predictionNonExercise
52 | )
53 | extension HKHeartRateRecoveryTestType: FHIRCodingConvertibleHKEnum {}
54 |
55 | @SynthesizeDisplayProperty(
56 | HKHeartRateSensorLocation.self,
57 | .other, .chest, .wrist, .finger, .hand, .earLobe, .foot
58 | )
59 | extension HKHeartRateSensorLocation: FHIRCodingConvertibleHKEnum {}
60 |
61 | @SynthesizeDisplayProperty(
62 | HKInsulinDeliveryReason.self,
63 | .basal, .bolus
64 | )
65 | extension HKInsulinDeliveryReason: FHIRCodingConvertibleHKEnum {}
66 |
67 | @SynthesizeDisplayProperty(
68 | HKPhysicalEffortEstimationType.self,
69 | .activityLookup, .deviceSensed
70 | )
71 | extension HKPhysicalEffortEstimationType: FHIRCodingConvertibleHKEnum {}
72 |
73 | @SynthesizeDisplayProperty(
74 | HKSwimmingStrokeStyle.self,
75 | .unknown, .mixed, .freestyle, .backstroke, .breaststroke, .butterfly, .kickboard
76 | )
77 | extension HKSwimmingStrokeStyle: FHIRCodingConvertibleHKEnum {}
78 |
79 | @SynthesizeDisplayProperty(
80 | HKUserMotionContext.self,
81 | .notSet, .stationary, .active
82 | )
83 | extension HKUserMotionContext: FHIRCodingConvertibleHKEnum {}
84 |
85 | @SynthesizeDisplayProperty(
86 | HKVO2MaxTestType.self,
87 | .maxExercise, .predictionSubMaxExercise, .predictionNonExercise,
88 | additionalCases: "predictionStepTest"
89 | )
90 | extension HKVO2MaxTestType: FHIRCodingConvertibleHKEnum {}
91 |
92 | @SynthesizeDisplayProperty(
93 | HKWaterSalinity.self,
94 | .freshWater, .saltWater
95 | )
96 | extension HKWaterSalinity: FHIRCodingConvertibleHKEnum {}
97 |
98 | @SynthesizeDisplayProperty(
99 | HKWeatherCondition.self,
100 | .none, .clear, .fair, .partlyCloudy, .mostlyCloudy, .cloudy, .foggy, .haze,
101 | .windy, .blustery, .smoky, .dust, .snow, .hail, .sleet, .freezingDrizzle,
102 | .freezingRain, .mixedRainAndHail, .mixedRainAndSnow, .mixedRainAndSleet, .mixedSnowAndSleet,
103 | .drizzle, .scatteredShowers, .showers, .thunderstorms, .tropicalStorm, .hurricane, .tornado
104 | )
105 | extension HKWeatherCondition: FHIRCodingConvertibleHKEnum {}
106 |
107 | @SynthesizeDisplayProperty(
108 | HKWorkoutSwimmingLocationType.self,
109 | .unknown, .pool, .openWater
110 | )
111 | extension HKWorkoutSwimmingLocationType: FHIRCodingConvertibleHKEnum {}
112 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/FHIR Extensions/FHIR Extension Builder/FHIRExtensionBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2025 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import Foundation
10 | import HealthKit
11 | import ModelsR4
12 |
13 |
14 | /// Namespace containing URLs of some custom FHIR Extensions.
15 | public enum FHIRExtensionUrls {}
16 |
17 |
18 | /// Type-erased version of a ``FHIRExtensionBuilder``
19 | public protocol FHIRExtensionBuilderProtocol {
20 | /// The extension builder's input type.
21 | associatedtype Input
22 |
23 | /// Applies the extension builder to an `Observation`, using the specified input.
24 | func apply(input: Input, to observation: Observation) throws
25 | }
26 |
27 |
28 | /// Defines a custom Extension Builder that can be applied to a FHIR `Observation` representing a HeathKit sample.
29 | ///
30 | /// ## Topics
31 | ///
32 | /// ### Creating an Extension Builder
33 | /// - ``init(_:)-((Input,Observation)->Void)``
34 | /// - ``init(_:)-((Observation)->Void)``
35 | ///
36 | /// ### Applying Extensions
37 | /// - ``apply(input:to:)``
38 | /// - ``apply(to:)``
39 | /// - ``apply(typeErasedInput:to:)``
40 | ///
41 | /// ### Supporting Types
42 | /// - ``FHIRExtensionUrls``
43 | /// - ``FHIRExtensionBuilderProtocol``
44 | ///
45 | /// ### Other
46 | /// - ``ModelsR4/Observation/apply(_:input:)``
47 | /// - ``ModelsR4/Observation/apply(_:)``
48 | public struct FHIRExtensionBuilder: FHIRExtensionBuilderProtocol, Sendable {
49 | private let impl: @Sendable (_ input: Input, _ observation: Observation) throws -> Void
50 |
51 | /// Creates a new Extension Builder.
52 | public init(_ action: @escaping @Sendable (_ input: Input, _ observation: Observation) throws -> Void) {
53 | self.impl = action
54 | }
55 |
56 | /// Creates a new Extension Builder.
57 | public init(_ action: @escaping @Sendable (_ observation: Observation) throws -> Void) where Input == Void {
58 | self.init { _, observation in
59 | try action(observation)
60 | }
61 | }
62 |
63 | public func apply(input: Input, to observation: Observation) throws {
64 | try impl(input, observation)
65 | }
66 | }
67 |
68 |
69 | extension FHIRExtensionBuilderProtocol {
70 | /// Applies the extension builder to an `Observation`.
71 | public func apply(to observation: Observation) throws where Input == Void {
72 | try apply(input: (), to: observation)
73 | }
74 |
75 | /// Attempts to apply the extension builder to an `Observation`, using the specified input.
76 | ///
77 | /// This function will have no effect if `typeErasedInput` doesn't match the extension builder's input type.
78 | /// An exception is if the extension builder's input type is `Void`; in this case any input is allowed, and will simply be discarded.
79 | ///
80 | /// - returns: A boolean value indicating whether the input was able to be coerced to the expected input type, and the builder was invoked.
81 | @discardableResult
82 | public func apply(typeErasedInput input: Any, to observation: Observation) throws -> Bool {
83 | if let input = input as? Input {
84 | try apply(input: input, to: observation)
85 | return true
86 | } else if let self = self as? any FHIRExtensionBuilderProtocol {
87 | // if the observation builder takes Void
88 | try self.apply(to: observation)
89 | return true
90 | } else {
91 | return false
92 | }
93 | }
94 | }
95 |
96 |
97 | extension Observation {
98 | /// Applies a ``FHIRExtensionBuilder`` to the `Observation`.
99 | public func apply(_ builder: FHIRExtensionBuilder, input: Input) throws {
100 | try builder.apply(input: input, to: self)
101 | }
102 |
103 | /// Applies a ``FHIRExtensionBuilder`` to the `Observation`.
104 | public func apply(_ builder: FHIRExtensionBuilder) throws {
105 | try builder.apply(to: self)
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/Tests/UITests/TestApp/Views/ElectrocardiogramTestView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open-source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import Foundation
10 | @preconcurrency import HealthKit
11 | import HealthKitOnFHIR
12 | import ModelsR4
13 | import SwiftUI
14 |
15 |
16 | struct ElectrocardiogramTestView: View {
17 | @State private var manager = HealthKitManager()
18 |
19 | @State private var observation: Observation?
20 | @State private var passed = false
21 | @State private var json = ""
22 | @State private var showingSheet = false
23 |
24 |
25 | var body: some View {
26 | Form {
27 | Section {
28 | Button("Read Electrocardiogram") {
29 | _Concurrency.Task {
30 | try await readElectrocardiogramTest()
31 | }
32 | }
33 | if passed {
34 | Text("Passed")
35 | }
36 | if observation != nil {
37 | Button("See JSON") {
38 | _Concurrency.Task {
39 | try readCreateJSON()
40 | showingSheet.toggle()
41 | }
42 | }
43 | .sheet(isPresented: $showingSheet) {
44 | JSONView(json: $json)
45 | }
46 | }
47 | }
48 | }
49 | .navigationBarTitle("Read Electrocardiogram")
50 | }
51 |
52 |
53 | private func readElectrocardiogramTest() async throws {
54 | try await manager.requestElectrocardiogramAuthorization()
55 |
56 | guard let electrocardiogram = try await manager.readElectrocardiogram() else {
57 | return
58 | }
59 | let symptoms = try await manager.readSymptoms(for: electrocardiogram)
60 | let voltageMeasurements = try await manager.readVoltageMeasurements(for: electrocardiogram)
61 |
62 | self.observation = try electrocardiogram.observation(
63 | symptoms: symptoms,
64 | voltageMeasurements: voltageMeasurements
65 | )
66 |
67 | let expectedCoding = Coding(
68 | code: "procedure".asFHIRStringPrimitive(),
69 | display: "Procedure".asFHIRStringPrimitive(),
70 | system: "http://terminology.hl7.org/CodeSystem/observation-category".asFHIRURIPrimitive()
71 | )
72 | guard observation?.category?.count == 1,
73 | observation?.category?.first?.coding == [expectedCoding] else {
74 | return
75 | }
76 |
77 | let expectedCodes = [
78 | Coding(
79 | code: "HKElectrocardiogram".asFHIRStringPrimitive(),
80 | display: "Electrocardiogram".asFHIRStringPrimitive(),
81 | system: "http://developer.apple.com/documentation/healthkit".asFHIRURIPrimitive()
82 | ),
83 | Coding(
84 | code: "131328".asFHIRStringPrimitive(),
85 | display: "MDC_ECG_ELEC_POTL".asFHIRStringPrimitive(),
86 | system: "urn:oid:2.16.840.1.113883.6.24".asFHIRURIPrimitive()
87 | )
88 | ]
89 | guard observation?.code.coding == expectedCodes else {
90 | return
91 | }
92 |
93 | guard observation?.component?.count == 12 else {
94 | return
95 | }
96 |
97 | self.passed = true
98 | }
99 |
100 | private func readCreateJSON() throws {
101 | guard let observation else {
102 | return
103 | }
104 |
105 | let encoder = JSONEncoder()
106 | encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
107 |
108 | guard let data = try? encoder.encode(observation) else {
109 | return
110 | }
111 |
112 | self.json = String(decoding: data, as: UTF8.self)
113 | }
114 | }
115 |
116 |
117 | struct ElectrocardiogramTestView_Previews: PreviewProvider {
118 | static var previews: some View {
119 | ElectrocardiogramTestView()
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/Tests/HealthKitOnFHIRTests/HKStateOfMindTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 | @testable import HealthKitOnFHIR
11 | import ModelsR4
12 | import Testing
13 |
14 |
15 | @Suite
16 | struct HKStateOfMindTests {
17 | @Test
18 | @available(iOS 18.0, watchOS 11.0, macCatalyst 18.0, macOS 15.0, visionOS 2.0, *)
19 | func stateOfMind1() throws {
20 | let cal = Calendar.current
21 | let yesterday = try #require(cal.date(byAdding: .day, value: -1, to: cal.startOfDay(for: .now)))
22 | let sample = HKStateOfMind(
23 | date: yesterday,
24 | kind: .dailyMood,
25 | valence: 0.27,
26 | labels: [.indifferent],
27 | associations: [.work]
28 | )
29 | let observation = try #require(sample.resource().get(if: Observation.self))
30 | #expect(observation.effective == .dateTime(try FHIRPrimitive(.init(date: yesterday))))
31 | #expect(observation.category?.first?.coding?.first?.code == "survey")
32 | #expect(observation.status == .final)
33 | let components = try #require(observation.component)
34 | #expect(components.count == 5)
35 | components.expectContainsComponent(withCode: "HKStateOfMindKind", value: .string("daily mood"))
36 | components.expectContainsComponent(withCode: "HKStateOfMindValence", value: .quantity(.init(value: 0.27)))
37 | components.expectContainsComponent(withCode: "HKStateOfMindValenceClassification", value: .string("slightly pleasant"))
38 | components.expectContainsComponent(withCode: "HKStateOfMindLabel", value: .string("indifferent"))
39 | components.expectContainsComponent(withCode: "HKStateOfMindAssociation", value: .string("work"))
40 | }
41 |
42 |
43 | @Test
44 | @available(iOS 18.0, watchOS 11.0, macCatalyst 18.0, macOS 15.0, visionOS 2.0, *)
45 | func stateOfMind2() throws {
46 | let cal = Calendar.current
47 | let yesterday = try #require(cal.date(byAdding: .day, value: -1, to: cal.startOfDay(for: .now)))
48 | let sample = HKStateOfMind(
49 | date: yesterday,
50 | kind: .momentaryEmotion,
51 | valence: -0.52,
52 | labels: [.brave, .confident, .lonely],
53 | associations: [.dating, .community, .friends]
54 | )
55 | let observation = try #require(sample.resource().get(if: Observation.self))
56 | #expect(observation.effective == .dateTime(try FHIRPrimitive(.init(date: yesterday))))
57 | #expect(observation.category?.first?.coding?.first?.code == "survey")
58 | #expect(observation.status == .final)
59 | let components = try #require(observation.component)
60 | #expect(components.count == 9)
61 | components.expectContainsComponent(withCode: "HKStateOfMindKind", value: .string("momentary emotion"))
62 | components.expectContainsComponent(withCode: "HKStateOfMindValence", value: .quantity(.init(value: -0.52)))
63 | components.expectContainsComponent(withCode: "HKStateOfMindValenceClassification", value: .string("unpleasant"))
64 | components.expectContainsComponent(withCode: "HKStateOfMindLabel", value: .string("brave"))
65 | components.expectContainsComponent(withCode: "HKStateOfMindLabel", value: .string("confident"))
66 | components.expectContainsComponent(withCode: "HKStateOfMindLabel", value: .string("lonely"))
67 | components.expectContainsComponent(withCode: "HKStateOfMindAssociation", value: .string("dating"))
68 | components.expectContainsComponent(withCode: "HKStateOfMindAssociation", value: .string("community"))
69 | components.expectContainsComponent(withCode: "HKStateOfMindAssociation", value: .string("friends"))
70 | }
71 | }
72 |
73 |
74 | extension Array where Element == ObservationComponent {
75 | func expectContainsComponent(withCode code: String, value: ObservationComponent.ValueX?) {
76 | let candidates = self.filter { $0.code.coding?.contains { $0.code?.value?.string == code } == true }
77 | guard !candidates.isEmpty else {
78 | Issue.record("Unable to find a component for code '\(code)'.")
79 | return
80 | }
81 | if candidates.count == 1 {
82 | #expect(candidates[0].value == value, "Mismatching component values for code '\(code)'")
83 | } else {
84 | #expect(candidates.contains { $0.value == value }, "No component with matching value.")
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/FHIR Extensions/Resource+Collections.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | // swiftlint:disable discouraged_optional_collection
10 |
11 | import Foundation
12 | import HealthKit
13 | import ModelsR4
14 |
15 | extension Observation {
16 | /// Appends an `Identifier` to the `Observation`
17 | @inlinable
18 | public func appendIdentifier(_ identifier: Identifier) {
19 | appendElement(identifier, to: \.identifier)
20 | }
21 |
22 | /// Appends multiple `Identifier`s to the `Observation`
23 | @inlinable
24 | public func appendIdentifiers(_ identifiers: some Collection) {
25 | appendElements(identifiers, to: \.identifier)
26 | }
27 |
28 | /// Appends a `CodeableConcept` to the `Observation`
29 | @inlinable
30 | public func appendCategory(_ category: CodeableConcept) {
31 | appendElement(category, to: \.category)
32 | }
33 |
34 | /// Appends multiple `CodeableConcept`s to the `Observation`
35 | @inlinable
36 | public func appendCategories(_ categories: some Collection) {
37 | appendElements(categories, to: \.category)
38 | }
39 |
40 | /// Appends a `Coding` to the `Observation`
41 | @inlinable
42 | public func appendCoding(_ coding: Coding) {
43 | appendElement(coding, to: \.code.coding)
44 | }
45 |
46 | /// Appends multiple `Coding`s to the `Observation`
47 | @inlinable
48 | public func appendCodings(_ codings: some Collection) {
49 | appendElements(codings, to: \.code.coding)
50 | }
51 |
52 | /// Appends an `ObservationComponent` to the `Observation`
53 | @inlinable
54 | public func appendComponent(_ component: ObservationComponent) {
55 | appendElement(component, to: \.component)
56 | }
57 |
58 | /// Appends multiple `ObservationComponent`s to the `Observation`
59 | @inlinable
60 | public func appendComponents(_ components: some Collection) {
61 | appendElements(components, to: \.component)
62 | }
63 | }
64 |
65 |
66 | /// A FHIR Type that supports extensions.
67 | ///
68 | /// - Note: Types outside this package shouldn't declare conformances to this protocol; rather, it is used to provide a range of FHIR Extension-related operations on several FHIR types.
69 | public protocol FHIRTypeWithExtensions: AnyObject {
70 | var `extension`: [Extension]? { get set }
71 | }
72 |
73 | extension ModelsR4.Element: FHIRTypeWithExtensions {}
74 | extension ModelsR4.DomainResource: FHIRTypeWithExtensions {}
75 |
76 |
77 | extension FHIRTypeWithExtensions {
78 | /// Retrieves all FHIR Extensions for the specified url.
79 | @inlinable
80 | public func extensions(for url: FHIRPrimitive) -> [Extension] {
81 | `extension`.map { $0.filter { $0.url == url } } ?? []
82 | }
83 | }
84 |
85 |
86 | extension FHIRTypeWithExtensions where Self: FHIRResourceMutationExtensions {
87 | /// Appends an `Extension` to the `DomainResource`
88 | @inlinable
89 | public func appendExtension(_ extension: Extension, replaceAllExistingWithSameUrl: Bool) {
90 | appendExtensions(CollectionOfOne(`extension`), replaceAllExistingWithSameUrl: replaceAllExistingWithSameUrl)
91 | }
92 |
93 | /// Appends multiple `Extension`s to the `DomainResource`
94 | @inlinable
95 | public func appendExtensions(_ extensions: some Collection, replaceAllExistingWithSameUrl: Bool) {
96 | if replaceAllExistingWithSameUrl {
97 | for element in extensions {
98 | removeAllExtensions(withUrl: element.url)
99 | }
100 | }
101 | appendElements(extensions, to: \.extension)
102 | }
103 |
104 | /// Removes the first extension element that matches the specified url.
105 | ///
106 | /// - returns: the removed extension element, if any.
107 | @inlinable
108 | @discardableResult
109 | public func removeFirstExtension(withUrl url: FHIRPrimitive) -> Extension? {
110 | removeFirstElement(of: \.extension) { $0.url == url }
111 | }
112 |
113 | /// Removes all extension elements that matches the specified url.
114 | ///
115 | /// - returns: the removed extension elements, if any.
116 | @inlinable
117 | @discardableResult
118 | public func removeAllExtensions(withUrl url: FHIRPrimitive) -> [Extension]? {
119 | removeAllElements(of: \.extension) { $0.url == url }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/Tests/HealthKitOnFHIRTests/HKWorkoutTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 | @testable import HealthKitOnFHIR
11 | import ModelsR4
12 | import Testing
13 |
14 |
15 | @Suite
16 | struct HKWorkoutTests {
17 | static let supportedWorkoutActivityTypes: [HKWorkoutActivityType] = [
18 | .americanFootball,
19 | .archery,
20 | .australianFootball,
21 | .badminton,
22 | .barre,
23 | .baseball,
24 | .basketball,
25 | .bowling,
26 | .boxing,
27 | .cardioDance,
28 | .climbing,
29 | .cooldown,
30 | .coreTraining,
31 | .cricket,
32 | .crossCountrySkiing,
33 | .crossTraining,
34 | .curling,
35 | .cycling,
36 | .socialDance,
37 | .cardioDance,
38 | .barre,
39 | .pilates,
40 | .discSports,
41 | .downhillSkiing,
42 | .elliptical,
43 | .equestrianSports,
44 | .fencing,
45 | .fishing,
46 | .fitnessGaming,
47 | .flexibility,
48 | .functionalStrengthTraining,
49 | .golf,
50 | .gymnastics,
51 | .handCycling,
52 | .handball,
53 | .highIntensityIntervalTraining,
54 | .hiking,
55 | .hockey,
56 | .hunting,
57 | .jumpRope,
58 | .kickboxing,
59 | .lacrosse,
60 | .martialArts,
61 | .mindAndBody,
62 | .mixedCardio,
63 | .mixedCardio,
64 | .highIntensityIntervalTraining,
65 | .other,
66 | .paddleSports,
67 | .pickleball,
68 | .pilates,
69 | .play,
70 | .preparationAndRecovery,
71 | .racquetball,
72 | .rowing,
73 | .rugby,
74 | .running,
75 | .sailing,
76 | .skatingSports,
77 | .snowboarding,
78 | .snowSports,
79 | .soccer,
80 | .socialDance,
81 | .softball,
82 | .squash,
83 | .stairs,
84 | .stairClimbing,
85 | .stepTraining,
86 | .surfingSports,
87 | .swimBikeRun,
88 | .swimming,
89 | .tableTennis,
90 | .taiChi,
91 | .tennis,
92 | .trackAndField,
93 | .traditionalStrengthTraining,
94 | .transition,
95 | .volleyball,
96 | .walking,
97 | .waterFitness,
98 | .waterPolo,
99 | .waterSports,
100 | .wheelchairRunPace,
101 | .wheelchairWalkPace,
102 | .wrestling,
103 | .yoga
104 | ]
105 |
106 | var startDate: Date {
107 | get throws {
108 | let dateComponents = DateComponents(year: 1891, month: 10, day: 1, hour: 12, minute: 0, second: 0) // Date Stanford University opened (https://www.stanford.edu/about/history/)
109 | return try #require(Calendar.current.date(from: dateComponents))
110 | }
111 | }
112 |
113 | var endDate: Date {
114 | get throws {
115 | let dateComponents = DateComponents(year: 1891, month: 10, day: 1, hour: 12, minute: 0, second: 42)
116 | return try #require(Calendar.current.date(from: dateComponents))
117 | }
118 | }
119 |
120 | func createCodeableConcept(
121 | code: String,
122 | system: String
123 | ) -> CodeableConcept {
124 | CodeableConcept(
125 | coding: [
126 | Coding(
127 | code: code.asFHIRStringPrimitive(),
128 | system: system.asFHIRURIPrimitive()
129 | )
130 | ]
131 | )
132 | }
133 |
134 |
135 | @Test(arguments: Self.supportedWorkoutActivityTypes)
136 | func hkWorkoutToObservation(activityType: HKWorkoutActivityType) throws {
137 | // The HKWorkout initializers are deprecated as of iOS 17 in favor of using `HKWorkoutBuilder`, but there
138 | // is currently no mechanism to use `HKWorkoutBuilder` inside unit tests without an authenticated
139 | // `HKHealthStore`, so we use this approach.
140 | let workoutSample = HKWorkout(
141 | activityType: activityType,
142 | start: try startDate,
143 | end: try endDate
144 | )
145 | let observation = try #require(workoutSample.resource().get(if: Observation.self))
146 | let expectedValue = createCodeableConcept(
147 | code: try activityType.fhirWorkoutTypeValue,
148 | system: "http://developer.apple.com/documentation/healthkit"
149 | )
150 | #expect(observation.value == .codeableConcept(expectedValue))
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/FHIR Extensions/FHIR Extension Builder/FHIRExtensionBuilder+AbsoluteTimeRange.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2025 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import Foundation
10 | import HealthKit
11 | import ModelsR4
12 |
13 |
14 | extension FHIRExtensionUrls {
15 | // SAFETY: this is in fact safe, since the FHIRPrimitive's `extension` property is empty.
16 | // As a result, the actual instance doesn't contain any mutable state, and since this is a let,
17 | // it also never can be mutated to contain any.
18 | /// Url of a FHIR Extension containing, if applicable, the absolute start date timestamp of a FHIR `Observation`.
19 | nonisolated(unsafe) public static let absoluteTimeRangeStart = "https://bdh.stanford.edu/fhir/defs/absoluteTimeRangeStart".asFHIRURIPrimitive()!
20 | // swiftlint:disable:previous force_unwrapping
21 |
22 | // SAFETY: this is in fact safe, since the FHIRPrimitive's `extension` property is empty.
23 | // As a result, the actual instance doesn't contain any mutable state, and since this is a let,
24 | // it also never can be mutated to contain any.
25 | /// Url of a FHIR Extension containing, if applicable, the absolute end date timestamp of a FHIR `Observation`.
26 | nonisolated(unsafe) public static let absoluteTimeRangeEnd = "https://bdh.stanford.edu/fhir/defs/absoluteTimeRangeEnd".asFHIRURIPrimitive()!
27 | // swiftlint:disable:previous force_unwrapping
28 | }
29 |
30 |
31 | extension FHIRExtensionBuilderProtocol where Self == FHIRExtensionBuilder {
32 | /// A FHIR Extension Builder that writes the absolute time range (i.e., start and end date) of a HealthKit sample into a FHIR `Observation` created from the sample.
33 | public static var includeAbsoluteTimeRange: FHIRExtensionBuilder {
34 | .init { (sample: HKSample, observation) in
35 | let timeRangeExtensions = [
36 | Extension(
37 | url: FHIRExtensionUrls.absoluteTimeRangeStart,
38 | value: .decimal(sample.startDate.timeIntervalSince1970.asFHIRDecimalPrimitive())
39 | ),
40 | Extension(
41 | url: FHIRExtensionUrls.absoluteTimeRangeEnd,
42 | value: .decimal(sample.endDate.timeIntervalSince1970.asFHIRDecimalPrimitive())
43 | )
44 | ]
45 | observation.appendExtensions(timeRangeExtensions, replaceAllExistingWithSameUrl: true)
46 | }
47 | }
48 | }
49 |
50 |
51 | extension Observation {
52 | /// Writes the Observation's absolute effective start and end date into a FHIR Extension.
53 | ///
54 | /// The absolute timestamps (decimals representing the time interval since 1970) are stored using the ``FHIRExtensionUrls/absoluteTimeRangeStart`` and ``FHIRExtensionUrls/absoluteTimeRangeEnd`` urls.
55 | ///
56 | /// - throws: If an error was encountered when converting the effective time range into the extension values. If the Observation's effecrive time uses an unsupported format (eg: `Timing`), ``HealthKitOnFHIRError/notSupported`` is thrown.
57 | public func encodeAbsoluteTimeRangeIntoExtension() throws {
58 | removeAllExtensions(withUrl: FHIRExtensionUrls.absoluteTimeRangeStart)
59 | removeAllExtensions(withUrl: FHIRExtensionUrls.absoluteTimeRangeEnd)
60 | let startDate, endDate: DateTime?
61 | switch effective {
62 | case nil:
63 | return
64 | case .dateTime(let dateTime):
65 | startDate = dateTime.value
66 | endDate = dateTime.value
67 | case .period(let period):
68 | startDate = period.start?.value
69 | endDate = period.end?.value
70 | case .instant(let instant):
71 | startDate = try instant.value.flatMap { try DateTime(instant: $0) }
72 | endDate = startDate
73 | case .timing:
74 | throw HealthKitOnFHIRError.notSupported
75 | }
76 | if let startDate = try startDate?.asNSDate() {
77 | appendExtension(
78 | Extension(
79 | url: FHIRExtensionUrls.absoluteTimeRangeStart,
80 | value: .decimal(startDate.timeIntervalSince1970.asFHIRDecimalPrimitive())
81 | ),
82 | replaceAllExistingWithSameUrl: true
83 | )
84 | }
85 | if let endDate = try endDate?.asNSDate() {
86 | appendExtension(
87 | Extension(
88 | url: FHIRExtensionUrls.absoluteTimeRangeEnd,
89 | value: .decimal(endDate.timeIntervalSince1970.asFHIRDecimalPrimitive())
90 | ),
91 | replaceAllExistingWithSameUrl: true
92 | )
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
45 |
46 |
52 |
53 |
54 |
55 |
58 |
59 |
60 |
61 |
63 |
69 |
70 |
71 |
72 |
73 |
83 |
85 |
91 |
92 |
93 |
94 |
100 |
102 |
108 |
109 |
110 |
111 |
113 |
114 |
117 |
118 |
119 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HKSampleMapping/HKElectrocardiogramMapping.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 |
10 | /// An ``HKElectrocardiogramMapping`` allows developers to customize the mapping of an`HKElectrocardiogram` to an FHIR observation.
11 | public struct HKElectrocardiogramMapping: Decodable, Sendable {
12 | /// A default instance of an ``HKElectrocardiogramMapping`` instance allowing developers to customize the ``HKElectrocardiogramMapping``.
13 | ///
14 | /// The default values are loaded from the `HKSampleMapping.json` resource in the ``HealthKitOnFHIR`` Swift Package.
15 | public static let `default` = HKSampleMapping.default.electrocardiogramMapping
16 |
17 |
18 | /// The FHIR codings defined as ``MappedCode``s used for the `HKElectrocardiogram`.
19 | public var codings: [MappedCode]
20 | /// The FHIR categories defined as ``MappedCode``s used for the `HKElectrocardiogram`.
21 | public var categories: [MappedCode]
22 | /// Defines the mapping of the `classification` category sample of an `HKElectrocardiogram` to an FHIR observation.
23 | public var classification: HKCategorySampleMapping
24 | /// Defines the mapping of the `symptomsStatus` category sample of an `HKElectrocardiogram` to an FHIR observation.
25 | public var symptomsStatus: HKCategorySampleMapping
26 | /// Defines the mapping of the `numberOfVoltageMeasurements` quantity property of an `HKElectrocardiogram` to an FHIR observation.
27 | public var numberOfVoltageMeasurements: HKQuantitySampleMapping
28 | /// Defines the mapping of the `samplingFrequency` quantity property of an `HKElectrocardiogram` to an FHIR observation.
29 | public var samplingFrequency: HKQuantitySampleMapping
30 | /// Defines the mapping of the `averageHeartRate` quantity property of an `HKElectrocardiogram` to an FHIR observation.
31 | public var averageHeartRate: HKQuantitySampleMapping
32 | /// Defines the mapping of the `voltageMeasurements` of an `HKElectrocardiogram` to an FHIR observation.
33 | public var voltageMeasurements: HKQuantitySampleMapping
34 | /// Defines the precision represented as the number of decimal values for the voltage measurement mapping of an `HKElectrocardiogram` to an FHIR observation.
35 | public var voltagePrecision: UInt
36 |
37 |
38 | /// An ``HKCorrelationMapping`` allows developers to customize the mapping of an`HKElectrocardiogram` to an FHIR observation.
39 | /// - Parameters:
40 | /// - codings: The FHIR codings defined as ``MappedCode``s used for the specified `HKElectrocardiogram`
41 | /// - categories: The FHIR categories defined as ``MappedCode``s used for the specified `HKElectrocardiogram`
42 | /// - classification: Defines the mapping of the `classification` category sample of an `HKElectrocardiogram` to an FHIR observation.
43 | /// - symptomsStatus: Defines the mapping of the `symptomsStatus` category sample of an `HKElectrocardiogram` to an FHIR observation.
44 | /// - numberOfVoltageMeasurements: Defines the mapping of the `numberOfVoltageMeasurements` quantity property of an `HKElectrocardiogram` to an FHIR observation.
45 | /// - samplingFrequency: Defines the mapping of the `samplingFrequency` quantity property of an `HKElectrocardiogram` to an FHIR observation.
46 | /// - averageHeartRate: Defines the mapping of the `averageHeartRate` quantity property of an `HKElectrocardiogram` to an FHIR observation.
47 | /// - voltageMeasurements: Defines the mapping of the `voltageMeasurements` of an `HKElectrocardiogram` to an FHIR observation.
48 | /// - voltagePrecision: Defines the precision represented as the number of decimal values for the voltage measurement mapping of an `HKElectrocardiogram` to an FHIR observation.
49 | public init(
50 | codings: [MappedCode] = Self.default.codings,
51 | categories: [MappedCode] = Self.default.categories,
52 | classification: HKCategorySampleMapping = Self.default.classification,
53 | symptomsStatus: HKCategorySampleMapping = Self.default.symptomsStatus,
54 | numberOfVoltageMeasurements: HKQuantitySampleMapping = Self.default.numberOfVoltageMeasurements,
55 | samplingFrequency: HKQuantitySampleMapping = Self.default.samplingFrequency,
56 | averageHeartRate: HKQuantitySampleMapping = Self.default.averageHeartRate,
57 | voltageMeasurements: HKQuantitySampleMapping = Self.default.voltageMeasurements,
58 | voltagePrecision: UInt = Self.default.voltagePrecision
59 | ) {
60 | self.codings = codings
61 | self.categories = categories
62 | self.classification = classification
63 | self.symptomsStatus = symptomsStatus
64 | self.numberOfVoltageMeasurements = numberOfVoltageMeasurements
65 | self.samplingFrequency = samplingFrequency
66 | self.averageHeartRate = averageHeartRate
67 | self.voltageMeasurements = voltageMeasurements
68 | self.voltagePrecision = voltagePrecision
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HealthKitOnFHIR.docc/HealthKitOnFHIR.md:
--------------------------------------------------------------------------------
1 | # ``HealthKitOnFHIR``
2 |
3 |
12 |
13 | Extensions that convert supported HealthKit samples to FHIR resources.
14 |
15 | ## Overview
16 |
17 | The HealthKitOnFHIR framework provides extensions that convert supported HealthKit samples to FHIR resources.
18 |
19 | HealthKitOnFHIR supports:
20 | - Extensions to convert data from Apple HealthKit to HL7® FHIR® R4.
21 | - Customizable mappings between HealthKit data types and standardized codes (e.g., LOINC)
22 |
23 | Please refer to the [HKObject Support Table]() for a complete list of supported types.
24 |
25 | ## HealthKit Extensions
26 |
27 | The HealthKitOnFHIR framework provides extensions that convert supported HealthKit samples to FHIR resources using [FHIRModels](https://github.com/apple/FHIRModels) encapsulated in a [ResourceProxy](https://github.com/apple/FHIRModels/blob/main/HowTo/Instantiation.md#1-use-resourceproxy).
28 |
29 | ```swift
30 | let sample: HKSample = // ...
31 | let resource = try sample.resource()
32 | ```
33 |
34 | ### Observations
35 |
36 | `HKQuantitySample`, `HKCategorySample`, `HKCorrelationSample`, and `HKElectrocardiogram` will be converted into FHIR [Observation](https://hl7.org/fhir/R4/observation.html) resources encapsulated in a [ResourceProxy](https://github.com/apple/FHIRModels/blob/main/HowTo/Instantiation.md#1-use-resourceproxy).
37 |
38 | ```swift
39 | let sample: HKQuantitySample = // ...
40 | let observation = try sample.resource().get(if: Observation.self)
41 | ```
42 |
43 | Codes and units can be customized by passing in a custom `HKSampleMapping` instance to the `resource(withMapping:)` method.
44 |
45 | ```swift
46 | let sample: HKQuantitySample = // ...
47 | let sampleMapping: HKSampleMapping = // ...
48 | let observation = try sample.resource(withMapping: sampleMapping).get(if: Observation.self)
49 | ```
50 |
51 | ### Clinical Records
52 |
53 | `HKClinicalRecord` will be converted to FHIR resources based on the type of its underlying data. Only records encoded in FHIR R4 are supported at this time.
54 |
55 | ```swift
56 | let allergyRecord: HKClinicalRecord = // ...
57 | let allergyIntolerance = try allergyRecord.resource().get(if: AllergyIntolerance.self)
58 | ```
59 |
60 | ## Example
61 |
62 | In the following example, we will query the HealthKit store for step count data, convert the resulting samples to FHIR observations, and encode them into JSON.
63 |
64 | ```swift
65 | import HealthKitOnFHIR
66 |
67 | // Initialize an HKHealthStore instance and request permissions with it
68 | // ...
69 |
70 | let date = ISO8601DateFormatter().date(from: "1885-11-11T00:00:00-08:00") ?? .now
71 | let sample = HKQuantitySample(
72 | type: HKQuantityType(.heartRate),
73 | quantity: HKQuantity(unit: HKUnit.count().unitDivided(by: .minute()), doubleValue: 42.0),
74 | start: date,
75 | end: date
76 | )
77 |
78 | // Convert the results to FHIR observations
79 | let observation: Observation?
80 | do {
81 | try observation = sample.resource().get(if: Observation.self)
82 | } catch {
83 | // Handle any mapping errors here.
84 | // ...
85 | }
86 |
87 | // Encode FHIR observations as JSON
88 | let encoder = JSONEncoder()
89 | encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes, .sortedKeys]
90 |
91 | guard let observation,
92 | let data = try? encoder.encode(observation) else {
93 | // Handle any encoding errors here.
94 | // ...
95 | }
96 |
97 | // Print the resulting JSON
98 | let json = String(decoding: data, as: UTF8.self)
99 | print(json)
100 | ```
101 |
102 | The following example generates the following FHIR observation:
103 |
104 | ```json
105 | {
106 | "code" : {
107 | "coding" : [
108 | {
109 | "code" : "8867-4",
110 | "display" : "Heart rate",
111 | "system" : "http://loinc.org"
112 | }
113 | ]
114 | },
115 | "effectiveDateTime" : "1885-11-11T00:00:00-08:00",
116 | "identifier" : [
117 | {
118 | "id" : "8BA093D9-B99B-4A3C-8C9E-98C86F49F5D8"
119 | }
120 | ],
121 | "issued" : "2023-01-01T00:00:00-08:00",
122 | "resourceType" : "Observation",
123 | "status" : "final",
124 | "valueQuantity" : {
125 | "code": "/min",
126 | "unit": "beats/minute",
127 | "system": "http://unitsofmeasure.org",
128 | "value" : 42
129 | }
130 | }
131 | ```
132 |
133 | ## Topics
134 |
135 | ### Mapping HealthKit Samples into FHIR Observations
136 | - ``HealthKit/HKSample/resource(withMapping:issuedDate:extensions:)``
137 | - ``HealthKit/HKSampleType/resourceType``
138 | - ``HealthKit/HKElectrocardiogram/observation(symptoms:voltageMeasurements:withMapping:issuedDate:extensions:)``
139 |
140 | ### Supported HealthKit HKSample Types
141 | -
142 | -
143 | -
144 | -
145 | -
146 |
147 | ### Working with FHIR Extensions
148 | - ``FHIRExtensionBuilder``
149 | - ``FHIRTypeWithExtensions``
150 |
--------------------------------------------------------------------------------
/Tests/HealthKitOnFHIRMacrosTests/HealthKitOnFHIRMacrosTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2025 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | #if os(macOS) // macro tests can only be run on the host machine
10 | import HealthKit
11 | import HealthKitOnFHIRMacros
12 | import HealthKitOnFHIRMacrosImpl
13 | import SwiftSyntaxMacroExpansion
14 | import SwiftSyntaxMacros
15 | import SwiftSyntaxMacrosGenericTestSupport
16 | import Testing
17 |
18 | let testMacrosSpecs: [String: MacroSpec] = [
19 | "SynthesizeDisplayProperty": MacroSpec(type: SynthesizeDisplayPropertyMacro.self)
20 | ]
21 |
22 | @Suite
23 | struct HealthKitOnFHIRMacrosTests {
24 | @Test
25 | func macro0() {
26 | assertMacroExpansion(
27 | """
28 | @SynthesizeDisplayProperty(
29 | HKCategoryValueSleepAnalysis.self,
30 | .inBed, .asleepUnspecified, .awake, .asleepCore, .asleepDeep, .asleepREM
31 | )
32 | extension HKCategoryValueSleepAnalysis: FHIRCodingConvertibleHKEnum {}
33 | """,
34 | expandedSource:
35 | """
36 | extension HKCategoryValueSleepAnalysis: FHIRCodingConvertibleHKEnum {
37 |
38 | var display: String? {
39 | switch self {
40 | case .inBed:
41 | "in bed"
42 | case .asleepUnspecified:
43 | "asleep unspecified"
44 | case .awake:
45 | "awake"
46 | case .asleepCore:
47 | "asleep core"
48 | case .asleepDeep:
49 | "asleep deep"
50 | case .asleepREM:
51 | "asleep REM"
52 | @unknown default:
53 | nil
54 | }
55 | }
56 | }
57 | """,
58 | macroSpecs: testMacrosSpecs,
59 | failureHandler: { Issue.record("\($0.message)") }
60 | )
61 | }
62 |
63 | @Test
64 | func macro1() {
65 | assertMacroExpansion(
66 | """
67 | @SynthesizeDisplayProperty(
68 | HKCategoryValueSleepAnalysis.self,
69 | .inBed, .asleepUnspecified, .awake, .asleepCore, .asleepDeep, .asleepREM
70 | )
71 | @available(iOS 18.0, macOS 15.0, watchOS 11.0, *)
72 | extension HKCategoryValueSleepAnalysis: FHIRCodingConvertibleHKEnum {}
73 | """,
74 | expandedSource:
75 | """
76 | @available(iOS 18.0, macOS 15.0, watchOS 11.0, *)
77 | extension HKCategoryValueSleepAnalysis: FHIRCodingConvertibleHKEnum {
78 |
79 | var display: String? {
80 | switch self {
81 | case .inBed:
82 | "in bed"
83 | case .asleepUnspecified:
84 | "asleep unspecified"
85 | case .awake:
86 | "awake"
87 | case .asleepCore:
88 | "asleep core"
89 | case .asleepDeep:
90 | "asleep deep"
91 | case .asleepREM:
92 | "asleep REM"
93 | @unknown default:
94 | nil
95 | }
96 | }
97 | }
98 | """,
99 | macroSpecs: testMacrosSpecs,
100 | failureHandler: { Issue.record("\($0.message)") }
101 | )
102 | }
103 |
104 | @Test
105 | func macro2() {
106 | assertMacroExpansion(
107 | """
108 | @SynthesizeDisplayProperty(
109 | HKCategoryValueSleepAnalysis.self,
110 | .inBed, .asleepUnspecified, .awake, .asleepCore,
111 | additionalCases: "asleepDeep", "asleepREM"
112 | )
113 | extension HKCategoryValueSleepAnalysis: FHIRCodingConvertibleHKEnum {}
114 | """,
115 | expandedSource:
116 | """
117 | extension HKCategoryValueSleepAnalysis: FHIRCodingConvertibleHKEnum {
118 |
119 | var display: String? {
120 | switch self {
121 | case .inBed:
122 | "in bed"
123 | case .asleepUnspecified:
124 | "asleep unspecified"
125 | case .awake:
126 | "awake"
127 | case .asleepCore:
128 | "asleep core"
129 | case .asleepDeep:
130 | "asleep deep"
131 | case .asleepREM:
132 | "asleep REM"
133 | @unknown default:
134 | nil
135 | }
136 | }
137 | }
138 | """,
139 | macroSpecs: testMacrosSpecs,
140 | failureHandler: { Issue.record("\($0.message)") }
141 | )
142 | }
143 | }
144 | #endif
145 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HealthKit Extensions/HKCategoryValue+Coding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 | import HealthKitOnFHIRMacros
11 | import ModelsR4
12 |
13 |
14 | /// Models a value type used by a `HKCategoryType`.
15 | protocol FHIRCodingConvertible {
16 | static var system: FHIRPrimitive { get }
17 |
18 | var code: String { get }
19 | var display: String? { get }
20 |
21 | init?(rawValue: Int)
22 | }
23 |
24 | extension FHIRCodingConvertible {
25 | var asCoding: Coding {
26 | Coding(
27 | code: code.asFHIRStringPrimitive(),
28 | display: display?.asFHIRStringPrimitive(),
29 | system: Self.system
30 | )
31 | }
32 | }
33 |
34 |
35 | extension FHIRCodingConvertible where Self: RawRepresentable, RawValue == Int {
36 | var code: String {
37 | String(rawValue)
38 | }
39 | }
40 |
41 |
42 | protocol FHIRCodingConvertibleHKEnum: FHIRCodingConvertible {}
43 |
44 | extension FHIRCodingConvertibleHKEnum {
45 | static var system: FHIRPrimitive {
46 | let typename = String(describing: Self.self).lowercased()
47 | return "https://developer.apple.com/documentation/healthkit/\(typename)".asFHIRURIPrimitive()! // swiftlint:disable:this force_unwrapping
48 | }
49 | }
50 |
51 |
52 | // MARK: Extensions
53 |
54 | @available(iOS 18.0, macOS 15.0, watchOS 11.0, *)
55 | @SynthesizeDisplayProperty(
56 | HKCategoryValueVaginalBleeding.self,
57 | .unspecified, .light, .medium, .heavy, .none
58 | )
59 | extension HKCategoryValueVaginalBleeding: FHIRCodingConvertibleHKEnum {}
60 |
61 | @SynthesizeDisplayProperty(
62 | HKCategoryValueCervicalMucusQuality.self,
63 | .dry, .sticky, .creamy, .watery, .eggWhite
64 | )
65 | extension HKCategoryValueCervicalMucusQuality: FHIRCodingConvertibleHKEnum {}
66 |
67 | @SynthesizeDisplayProperty(
68 | HKCategoryValueMenstrualFlow.self,
69 | .unspecified, .light, .medium, .heavy, .none
70 | )
71 | extension HKCategoryValueMenstrualFlow: FHIRCodingConvertibleHKEnum {}
72 |
73 | @SynthesizeDisplayProperty(
74 | HKCategoryValueOvulationTestResult.self,
75 | .negative, .luteinizingHormoneSurge, .indeterminate, .estrogenSurge
76 | )
77 | extension HKCategoryValueOvulationTestResult: FHIRCodingConvertibleHKEnum {}
78 |
79 | @SynthesizeDisplayProperty(
80 | HKCategoryValueContraceptive.self,
81 | .unspecified, .implant, .injection, .intrauterineDevice, .intravaginalRing, .oral, .patch
82 | )
83 | extension HKCategoryValueContraceptive: FHIRCodingConvertibleHKEnum {}
84 |
85 | @SynthesizeDisplayProperty(
86 | HKCategoryValueSleepAnalysis.self,
87 | .inBed, .asleepUnspecified, .awake, .asleepCore, .asleepDeep, .asleepREM
88 | )
89 | extension HKCategoryValueSleepAnalysis: FHIRCodingConvertibleHKEnum {}
90 |
91 | @SynthesizeDisplayProperty(
92 | HKCategoryValueAppetiteChanges.self,
93 | .unspecified, .noChange, .decreased, .increased
94 | )
95 | extension HKCategoryValueAppetiteChanges: FHIRCodingConvertibleHKEnum {}
96 |
97 | @SynthesizeDisplayProperty(
98 | HKCategoryValueEnvironmentalAudioExposureEvent.self,
99 | .momentaryLimit
100 | )
101 | extension HKCategoryValueEnvironmentalAudioExposureEvent: FHIRCodingConvertibleHKEnum {}
102 |
103 | @SynthesizeDisplayProperty(
104 | HKCategoryValueHeadphoneAudioExposureEvent.self,
105 | .sevenDayLimit
106 | )
107 | extension HKCategoryValueHeadphoneAudioExposureEvent: FHIRCodingConvertibleHKEnum {}
108 |
109 | @SynthesizeDisplayProperty(
110 | HKCategoryValueLowCardioFitnessEvent.self,
111 | .lowFitness
112 | )
113 | extension HKCategoryValueLowCardioFitnessEvent: FHIRCodingConvertibleHKEnum {}
114 |
115 | @SynthesizeDisplayProperty(
116 | HKAppleWalkingSteadinessClassification.self,
117 | .ok, .low, .veryLow
118 | )
119 | extension HKAppleWalkingSteadinessClassification: FHIRCodingConvertibleHKEnum {}
120 |
121 | @SynthesizeDisplayProperty(
122 | HKCategoryValueAppleWalkingSteadinessEvent.self,
123 | .initialLow, .initialVeryLow, .repeatLow, .repeatVeryLow
124 | )
125 | extension HKCategoryValueAppleWalkingSteadinessEvent: FHIRCodingConvertibleHKEnum {}
126 |
127 | @SynthesizeDisplayProperty(
128 | HKCategoryValuePregnancyTestResult.self,
129 | .negative, .positive, .indeterminate
130 | )
131 | extension HKCategoryValuePregnancyTestResult: FHIRCodingConvertibleHKEnum {}
132 |
133 | @SynthesizeDisplayProperty(
134 | HKCategoryValueProgesteroneTestResult.self,
135 | .negative, .positive, .indeterminate
136 | )
137 | extension HKCategoryValueProgesteroneTestResult: FHIRCodingConvertibleHKEnum {}
138 |
139 | @SynthesizeDisplayProperty(
140 | HKCategoryValueAppleStandHour.self,
141 | .stood, .idle
142 | )
143 | extension HKCategoryValueAppleStandHour: FHIRCodingConvertibleHKEnum {}
144 |
145 | @SynthesizeDisplayProperty(
146 | HKCategoryValueSeverity.self,
147 | .unspecified, .notPresent, .mild, .moderate, .severe
148 | )
149 | extension HKCategoryValueSeverity: FHIRCodingConvertibleHKEnum {}
150 |
151 | @SynthesizeDisplayProperty(
152 | HKCategoryValuePresence.self,
153 | .present, .notPresent
154 | )
155 | extension HKCategoryValuePresence: FHIRCodingConvertibleHKEnum {}
156 |
--------------------------------------------------------------------------------
/Tests/UITests/TestApp/HealthKitManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open-source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 |
10 | import Foundation
11 | import HealthKit
12 | import Observation
13 |
14 |
15 | @Observable
16 | final class HealthKitManager: Sendable {
17 | let healthStore: HKHealthStore?
18 |
19 | init() {
20 | if HKHealthStore.isHealthDataAvailable() {
21 | healthStore = HKHealthStore()
22 | } else {
23 | healthStore = nil
24 | }
25 | }
26 |
27 |
28 | func requestStepAuthorization() async throws {
29 | guard let healthStore,
30 | let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount) else {
31 | throw HKError(.errorHealthDataUnavailable)
32 | }
33 | try await healthStore.requestAuthorization(toShare: [stepType], read: [stepType])
34 | }
35 |
36 | func readStepCount(sorted sortDescriptors: [SortDescriptor] = [], limit: Int? = nil) async throws -> [HKQuantitySample] {
37 | guard let healthStore else {
38 | throw HKError(.errorHealthDataUnavailable)
39 | }
40 | let query = HKSampleQueryDescriptor(
41 | predicates: [.quantitySample(type: HKQuantityType(.stepCount))],
42 | sortDescriptors: sortDescriptors,
43 | limit: limit ?? HKObjectQueryNoLimit
44 | )
45 | return try await query.result(for: healthStore)
46 | }
47 |
48 | func writeSteps(startDate: Date, endDate: Date, steps: Double) async throws {
49 | guard let healthStore,
50 | let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount) else {
51 | throw HKError(.errorHealthDataUnavailable)
52 | }
53 | let stepsSample = HKQuantitySample(
54 | type: stepType,
55 | quantity: HKQuantity(unit: HKUnit.count(), doubleValue: steps),
56 | start: startDate,
57 | end: endDate
58 | )
59 | try await healthStore.save(stepsSample)
60 | }
61 |
62 | // MARK: - Workouts
63 |
64 | func requestWorkoutAuthorization() async throws {
65 | guard let healthStore else {
66 | throw HKError(.errorHealthDataUnavailable)
67 | }
68 | let typesToWrite: Set = [HKObjectType.workoutType()]
69 | try await healthStore.requestAuthorization(toShare: typesToWrite, read: [])
70 | }
71 |
72 | // MARK: - Electrocardiogram
73 |
74 | func requestElectrocardiogramAuthorization() async throws {
75 | guard let healthStore else {
76 | throw HKError(.errorHealthDataUnavailable)
77 | }
78 | var readTypes: [HKObjectType] = HKElectrocardiogram.correlatedSymptomTypes
79 | readTypes.append(HKQuantityType.electrocardiogramType())
80 | try await healthStore.requestAuthorization(toShare: [], read: Set(readTypes))
81 | }
82 |
83 | func readElectrocardiogram() async throws -> HKElectrocardiogram? {
84 | guard let healthStore else {
85 | throw HKError(.errorHealthDataUnavailable)
86 | }
87 | let query = HKSampleQueryDescriptor(
88 | predicates: [.electrocardiogram()],
89 | sortDescriptors: [],
90 | limit: 1
91 | )
92 | return try await query.result(for: healthStore).first
93 | }
94 |
95 | func readSymptoms(for electrocardiogram: HKElectrocardiogram) async throws -> HKElectrocardiogram.Symptoms {
96 | guard let healthStore else {
97 | throw HKError(.errorHealthDataUnavailable)
98 | }
99 | return try await electrocardiogram.symptoms(from: healthStore)
100 | }
101 |
102 | func readVoltageMeasurements(for electrocardiogram: HKElectrocardiogram) async throws -> HKElectrocardiogram.VoltageMeasurements {
103 | guard let healthStore else {
104 | throw HKError(.errorHealthDataUnavailable)
105 | }
106 | return try await electrocardiogram.voltageMeasurements(from: healthStore)
107 | }
108 |
109 | // MARK: - Health Records
110 | func requestHealthRecordsAuthorization() async throws {
111 | guard let healthStore else {
112 | throw HKError(.errorHealthDataUnavailable)
113 | }
114 | // We disable the SwiftLint force unwrap rule here as all initializers use Apple's constants.
115 | // swiftlint:disable force_unwrapping
116 | let readTypes: Set = [
117 | HKObjectType.clinicalType(forIdentifier: .allergyRecord)!,
118 | HKObjectType.clinicalType(forIdentifier: .conditionRecord)!,
119 | HKObjectType.clinicalType(forIdentifier: .immunizationRecord)!,
120 | HKObjectType.clinicalType(forIdentifier: .labResultRecord)!,
121 | HKObjectType.clinicalType(forIdentifier: .medicationRecord)!,
122 | HKObjectType.clinicalType(forIdentifier: .procedureRecord)!,
123 | HKObjectType.clinicalType(forIdentifier: .vitalSignRecord)!
124 | ]
125 | try await healthStore.requestAuthorization(toShare: [], read: readTypes)
126 | }
127 |
128 | func readHealthRecords(type: HKClinicalTypeIdentifier) async throws -> [HKClinicalRecord] {
129 | guard let healthStore else {
130 | return []
131 | }
132 | let query = HKSampleQueryDescriptor(
133 | predicates: [.clinicalRecord(type: HKClinicalType(type))],
134 | sortDescriptors: [],
135 | limit: HKObjectQueryNoLimit
136 | )
137 | return try await query.result(for: healthStore)
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HealthKit Extensions/HKSample+ResourceProxy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 | import ModelsR4
11 |
12 |
13 | extension HKSample {
14 | /// A `ResourceProxy` containing an FHIR `Observation` based on the concrete subclass of `HKSample`.
15 | ///
16 | /// - parameter mapping: A mapping to map `HKSample`s to corresponding FHIR observations allowing the customization of, e.g., codings and units. See ``HKSampleMapping``.
17 | /// - parameter issuedDate: `Instant` specifying when this version of the resource was made available. Defaults to `Date.now`.
18 | /// - parameter extensions: Any ``FHIRExtensionBuilder``s that should be applied to each of the produced observations.
19 | /// The ``FHIRExtensionBuilder/sourceDevice-9m1t7``, ``FHIRExtensionBuilder/sourceRevision-8b3xb``, and ``FHIRExtensionBuilder/metadata`` extension builders are always enabled when creating a FHIR `Observation`s from a `HKSample`.
20 | /// - returns: A `ResourceProxy`containing an FHIR `Observation` based on the concrete subclass of `HKSample`.
21 | /// - throws: If a specific `HKSample` type is currently not supported the property returns an ``HealthKitOnFHIRError/notSupported`` error.
22 | ///
23 | /// - Important: When mapping an array of HKSample objects into ResourceProxies, for performance reasons always prefer ``Swift/Sequence/mapIntoResourceProxies(using:extensions:)`` or ``Swift/Sequence/mapIntoResourceProxies(using:extensions:)``.
24 | public func resource(
25 | withMapping mapping: HKSampleMapping = .default,
26 | issuedDate: FHIRPrimitive? = nil,
27 | extensions: [any FHIRExtensionBuilderProtocol] = []
28 | ) throws -> ResourceProxy {
29 | #if !os(watchOS)
30 | if let self = self as? HKClinicalRecord {
31 | return try self.resource()
32 | }
33 | #endif
34 | let observation = Observation(
35 | code: CodeableConcept(),
36 | status: FHIRPrimitive(.final)
37 | )
38 | // Set basic elements applicable to all observations
39 | observation.id = self.uuid.uuidString.asFHIRStringPrimitive()
40 | observation.appendIdentifier(Identifier(id: observation.id))
41 | try observation.setEffective(
42 | startDate: self.startDate,
43 | endDate: self.endDate,
44 | timeZone: self.timeZone ?? .current
45 | )
46 | if let issuedDate {
47 | observation.issued = issuedDate
48 | } else {
49 | try observation.setIssued(on: Date())
50 | }
51 | // Set specific data based on HealthKit type
52 | if let self = self as? any FHIRObservationBuildable {
53 | try self.build(observation, mapping: mapping)
54 | } else {
55 | throw HealthKitOnFHIRError.notSupported
56 | }
57 | let baseExtensions: [FHIRExtensionBuilder] = [
58 | .sourceDevice, .sourceRevision, .metadata
59 | ]
60 | for builder in baseExtensions + extensions {
61 | try builder.apply(typeErasedInput: self, to: observation)
62 | }
63 | return ResourceProxy(with: observation)
64 | }
65 | }
66 |
67 |
68 | extension Sequence where Element: HKSample {
69 | /// Produces an Array of FHIR `ResourceProxies`.
70 | ///
71 | /// - Note: This method provides significant performance improvements as compared to calling ``HealthKit/HKSample/resource(withMapping:issuedDate:extensions:)`` for each element in the collection.
72 | ///
73 | /// - parameter mapping: A mapping to map `HKSample`s to corresponding FHIR observations allowing the customization of, e.g., codings and units. See ``HKSampleMapping``.
74 | /// - parameter issuedDate: `Instant` specifying when this version of the resource was made available. Defaults to `Date.now`.
75 | /// - parameter extensions: Any ``FHIRExtensionBuilder``s that should be applied to each of the produced observations.
76 | /// The ``FHIRExtensionBuilder/sourceDevice-9m1t7``, ``FHIRExtensionBuilder/sourceRevision-8b3xb``, and ``FHIRExtensionBuilder/metadata`` extension builders are always enabled when creating a FHIR `Observation`s from a `HKSample`.
77 | public func mapIntoResourceProxies(
78 | using mapping: HKSampleMapping = .default,
79 | issuedDate: FHIRPrimitive? = nil,
80 | extensions: [any FHIRExtensionBuilderProtocol] = []
81 | ) throws -> [ResourceProxy] {
82 | let issuedDate = try issuedDate ?? FHIRPrimitive(try Instant(date: .now))
83 | return try map { try $0.resource(withMapping: mapping, issuedDate: issuedDate, extensions: extensions) }
84 | }
85 |
86 | /// Produces an Array of FHIR `ResourceProxies`.
87 | ///
88 | /// This function is equivalent to calling ``HealthKit/HKSample/resource(withMapping:issuedDate:extensions:)`` on every element in the sequence, andd filtering out those elememt for which the call raised an error.
89 | ///
90 | /// - Note: This method provides significant performance improvements as compared to calling ``HealthKit/HKSample/resource(withMapping:issuedDate:extensions:)`` for each element in the collection.
91 | ///
92 | /// - parameter mapping: A mapping to map `HKSample`s to corresponding FHIR observations allowing the customization of, e.g., codings and units. See ``HKSampleMapping``.
93 | /// - parameter issuedDate: `Instant` specifying when this version of the resource was made available. Defaults to `Date.now`.
94 | /// - parameter extensions: Any ``FHIRExtensionBuilder``s that should be applied to each of the produced observations.
95 | /// The ``FHIRExtensionBuilder/sourceDevice-9m1t7``, ``FHIRExtensionBuilder/sourceRevision-8b3xb``, and ``FHIRExtensionBuilder/metadata`` extension builders are always enabled when creating a FHIR `Observation`s from a `HKSample`.
96 | public func compactMapIntoResourceProxies(
97 | using mapping: HKSampleMapping = .default,
98 | issuedDate: FHIRPrimitive? = nil,
99 | extensions: [any FHIRExtensionBuilderProtocol] = []
100 | ) throws -> [ResourceProxy] {
101 | let issuedDate = try issuedDate ?? FHIRPrimitive(try Instant(date: .now))
102 | return compactMap { try? $0.resource(withMapping: mapping, issuedDate: issuedDate, extensions: extensions) }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HealthKit Extensions/HKWorkoutActivityType+String.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 |
11 |
12 | extension HKWorkoutActivityType {
13 | var fhirWorkoutTypeValue: String {
14 | get throws {
15 | switch self {
16 | case .americanFootball:
17 | return "americanFootball"
18 | case .archery:
19 | return "archery"
20 | case .australianFootball:
21 | return "australianFootball"
22 | case .badminton:
23 | return "badminton"
24 | case .barre:
25 | return "barre"
26 | case .baseball:
27 | return "baseball"
28 | case .basketball:
29 | return "basketball"
30 | case .bowling:
31 | return "bowling"
32 | case .boxing:
33 | return "boxing"
34 | case .cardioDance:
35 | return "cardioDance"
36 | case .climbing:
37 | return "climbing"
38 | case .cooldown:
39 | return "coolDown"
40 | case .coreTraining:
41 | return "coreTraining"
42 | case .cricket:
43 | return "cricket"
44 | case .crossCountrySkiing:
45 | return "crossCountrySkiing"
46 | case .crossTraining:
47 | return "crossTraining"
48 | case .curling:
49 | return "curling"
50 | case .cycling:
51 | return "cycling"
52 | case .dance:
53 | return "dance"
54 | case .danceInspiredTraining:
55 | return "danceInspiredTraining"
56 | case .discSports:
57 | return "discSports"
58 | case .downhillSkiing:
59 | return "downhillSkiing"
60 | case .elliptical:
61 | return "elliptical"
62 | case .equestrianSports:
63 | return "equestrianSports"
64 | case .fencing:
65 | return "fencing"
66 | case .fishing:
67 | return "fishing"
68 | case .fitnessGaming:
69 | return "fitnessGaming"
70 | case .flexibility:
71 | return "flexibility"
72 | case .functionalStrengthTraining:
73 | return "functionalStrengthTraining"
74 | case .golf:
75 | return "golf"
76 | case .gymnastics:
77 | return "gymnastics"
78 | case .handCycling:
79 | return "handCycling"
80 | case .handball:
81 | return "handball"
82 | case .highIntensityIntervalTraining:
83 | return "highIntensityIntervalTraining"
84 | case .hiking:
85 | return "hiking"
86 | case .hockey:
87 | return "hockey"
88 | case .hunting:
89 | return "hunting"
90 | case .jumpRope:
91 | return "jumpRope"
92 | case .kickboxing:
93 | return "kickboxing"
94 | case .lacrosse:
95 | return "lacrosse"
96 | case .martialArts:
97 | return "martialArts"
98 | case .mindAndBody:
99 | return "mindAndBody"
100 | case .mixedCardio:
101 | return "mixedCardio"
102 | case .mixedMetabolicCardioTraining:
103 | return "mixedMetabolicCardioTraining"
104 | case .other:
105 | return "other"
106 | case .paddleSports:
107 | return "paddleSports"
108 | case .pickleball:
109 | return "pickleball"
110 | case .pilates:
111 | return "pilates"
112 | case .play:
113 | return "play"
114 | case .preparationAndRecovery:
115 | return "preparationAndRecovery"
116 | case .racquetball:
117 | return "racquetball"
118 | case .rowing:
119 | return "rowing"
120 | case .rugby:
121 | return "rugby"
122 | case .running:
123 | return "running"
124 | case .sailing:
125 | return "sailing"
126 | case .skatingSports:
127 | return "skatingSports"
128 | case .snowboarding:
129 | return "snowboarding"
130 | case .snowSports:
131 | return "snowSports"
132 | case .soccer:
133 | return "soccer"
134 | case .socialDance:
135 | return "socialDance"
136 | case .softball:
137 | return "softball"
138 | case .squash:
139 | return "squash"
140 | case .stairClimbing:
141 | return "stairClimbing"
142 | case .stairs:
143 | return "stairs"
144 | case .stepTraining:
145 | return "stepTraining"
146 | case .surfingSports:
147 | return "surfingSports"
148 | case .swimBikeRun:
149 | return "swimBikeRun"
150 | case .swimming:
151 | return "swimming"
152 | case .tableTennis:
153 | return "tableTennis"
154 | case .taiChi:
155 | return "taiChi"
156 | case .tennis:
157 | return "tennis"
158 | case .trackAndField:
159 | return "trackAndField"
160 | case .traditionalStrengthTraining:
161 | return "traditionalStrengthTraining"
162 | case .transition:
163 | return "transition"
164 | case .underwaterDiving:
165 | return "underwaterDiving"
166 | case .volleyball:
167 | return "volleyball"
168 | case .walking:
169 | return "walking"
170 | case .waterFitness:
171 | return "waterFitness"
172 | case .waterPolo:
173 | return "waterPolo"
174 | case .waterSports:
175 | return "waterSports"
176 | case .wheelchairRunPace:
177 | return "wheelchairRunPace"
178 | case .wheelchairWalkPace:
179 | return "wheelchairWalkPace"
180 | case .wrestling:
181 | return "wrestling"
182 | case .yoga:
183 | return "yoga"
184 | @unknown default:
185 | throw HealthKitOnFHIRError.invalidValue
186 | }
187 | }
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/FHIR Extensions/FHIR Extension Builder/FHIRExtensionBuilder+Source.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2025 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import Foundation
10 | import HealthKit
11 | import ModelsR4
12 |
13 |
14 | extension FHIRExtensionUrls {
15 | // SAFETY: this is in fact safe, since the FHIRPrimitive's `extension` property is empty.
16 | // As a result, the actual instance doesn't contain any mutable state, and since this is a let,
17 | // it also never can be mutated to contain any.
18 | /// Url of a FHIR Extension containing, if applicable, encoded `HKDevice` of the `HKObject` from which a FHIR `Observation` was created.
19 | nonisolated(unsafe) public static let sourceDevice = "https://bdh.stanford.edu/fhir/defs/sourceDevice".asFHIRURIPrimitive()!
20 | // swiftlint:disable:previous force_unwrapping
21 |
22 | // SAFETY: this is in fact safe, since the FHIRPrimitive's `extension` property is empty.
23 | // As a result, the actual instance doesn't contain any mutable state, and since this is a let,
24 | // it also never can be mutated to contain any.
25 | /// Url of a FHIR Extension containing, if applicable, encoded `HKSourceRevision` of the `HKObject` from which a FHIR `Observation` was created.
26 | nonisolated(unsafe) public static let sourceRevision = "https://bdh.stanford.edu/fhir/defs/sourceRevision".asFHIRURIPrimitive()!
27 | // swiftlint:disable:previous force_unwrapping
28 | }
29 |
30 |
31 | extension FHIRExtensionBuilderProtocol where Self == FHIRExtensionBuilder {
32 | /// A FHIR Extension Builder that writes a `HKDevice` into a FHIR `Observation`.
33 | public static var sourceDevice: Self {
34 | .init { (device: HKDevice, observation) in
35 | let deviceInfo = Extension(url: FHIRExtensionUrls.sourceDevice)
36 | let appendDeviceInfoEntry = { (keyPath: KeyPath) in
37 | guard let name = keyPath._kvcKeyPathString else {
38 | print("Unable to obtain name for keyPath '\(keyPath)'. Skipping.")
39 | return
40 | }
41 | guard let value = device[keyPath: keyPath] else {
42 | return
43 | }
44 | let url = FHIRExtensionUrls.sourceDevice.appending(component: name)
45 | deviceInfo.appendExtension(
46 | Extension(url: url, value: .string(value.asFHIRStringPrimitive())),
47 | replaceAllExistingWithSameUrl: true
48 | )
49 | }
50 | appendDeviceInfoEntry(\.name)
51 | appendDeviceInfoEntry(\.manufacturer)
52 | appendDeviceInfoEntry(\.model)
53 | appendDeviceInfoEntry(\.hardwareVersion)
54 | appendDeviceInfoEntry(\.firmwareVersion)
55 | appendDeviceInfoEntry(\.softwareVersion)
56 | appendDeviceInfoEntry(\.localIdentifier)
57 | appendDeviceInfoEntry(\.udiDeviceIdentifier)
58 | observation.appendExtension(deviceInfo, replaceAllExistingWithSameUrl: true)
59 | }
60 | }
61 | }
62 |
63 |
64 | extension FHIRExtensionBuilderProtocol where Self == FHIRExtensionBuilder {
65 | /// A FHIR Extension Builder that writes a `HKSourceRevision` into a FHIR `Observation`.
66 | public static var sourceRevision: Self {
67 | .init { (revision: HKSourceRevision, observation) throws in // swiftlint:disable:this closure_body_length
68 | let deviceInfo = Extension(url: FHIRExtensionUrls.sourceRevision)
69 | let fieldUrl = { (components: String...) in
70 | FHIRExtensionUrls.sourceRevision.appending(components: components)
71 | }
72 | let appendDeviceInfoEntry = { (keyPath: KeyPath) in
73 | guard let name = keyPath._kvcKeyPathString else {
74 | print("Unable to obtain name for keyPath '\(keyPath)'. Skipping.")
75 | return
76 | }
77 | guard let value = revision[keyPath: keyPath] else {
78 | return
79 | }
80 | let url = fieldUrl(name)
81 | deviceInfo.appendExtension(
82 | Extension(url: url, value: .string(value.asFHIRStringPrimitive())),
83 | replaceAllExistingWithSameUrl: true
84 | )
85 | }
86 | deviceInfo.appendExtension(
87 | Extension(
88 | extension: [
89 | Extension(
90 | url: fieldUrl("source", "name"),
91 | value: .string(revision.source.name.asFHIRStringPrimitive())
92 | ),
93 | Extension(
94 | url: fieldUrl("source", "bundleIdentifier"),
95 | value: .string(revision.source.bundleIdentifier.asFHIRStringPrimitive())
96 | )
97 | ],
98 | url: fieldUrl("source")
99 | ),
100 | replaceAllExistingWithSameUrl: true
101 | )
102 | appendDeviceInfoEntry(\.version)
103 | appendDeviceInfoEntry(\.productType)
104 | appendDeviceInfoEntry(\.OSVersion)
105 | observation.appendExtension(deviceInfo, replaceAllExistingWithSameUrl: true)
106 | }
107 | }
108 | }
109 |
110 |
111 | extension FHIRExtensionBuilderProtocol where Self == FHIRExtensionBuilder {
112 | /// A FHIR Extension Builder that writes a HealthKit object's `HKSourceRevision` into a FHIR `Observation` created from the sample.
113 | public static var sourceRevision: Self {
114 | .init { object, observation in
115 | try observation.apply(.sourceRevision, input: object.sourceRevision)
116 | }
117 | }
118 |
119 | /// A FHIR Extension Builder that writes a HealthKit object's `HKDevice` into a FHIR `Observation` created from the sample.
120 | public static var sourceDevice: Self {
121 | .init { object, observation in
122 | if let device = object.device {
123 | try observation.apply(.sourceDevice, input: device)
124 | } else {
125 | observation.removeAllExtensions(withUrl: FHIRExtensionUrls.sourceDevice)
126 | }
127 | }
128 | }
129 | }
130 |
131 |
132 | extension HKSourceRevision {
133 | /// We define this as an optional String objc-compatible property, so that we can encode it into an Extension using the API we have above.
134 | @objc fileprivate var OSVersion: String? {
135 | let version = operatingSystemVersion
136 | return "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HealthKit Extensions/HKStateOfMind+Observation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2025 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 | import ModelsR4
11 |
12 |
13 | @available(iOS 18.0, watchOS 11.0, macCatalyst 18.0, macOS 15.0, visionOS 2.0, *)
14 | extension HKStateOfMind: FHIRObservationBuildable {
15 | func build(_ observation: Observation, mapping: HKSampleMapping) throws {
16 | let mapping = mapping.stateOfMindMapping
17 | for code in mapping.codings {
18 | observation.appendCoding(code.coding)
19 | }
20 | for category in mapping.categories {
21 | observation.appendCategory(CodeableConcept(coding: [category.coding]))
22 | }
23 | observation.appendComponent(.init(
24 | code: CodeableConcept(coding: mapping.kind.codings.map(\.coding)),
25 | value: .string(self.kind.stringValue.asFHIRStringPrimitive())
26 | ))
27 | observation.appendComponent(.init(
28 | code: CodeableConcept(coding: mapping.valence.codings.map(\.coding)),
29 | value: .quantity(.init(value: self.valence.asFHIRDecimalPrimitive()))
30 | ))
31 | observation.appendComponent(.init(
32 | code: CodeableConcept(coding: mapping.valenceClassification.codings.map(\.coding)),
33 | value: .string(self.valenceClassification.stringValue.asFHIRStringPrimitive())
34 | ))
35 | for label in self.labels {
36 | observation.appendComponent(.init(
37 | code: CodeableConcept(coding: mapping.label.codings.map(\.coding)),
38 | value: .string(label.stringValue.asFHIRStringPrimitive())
39 | ))
40 | }
41 | for association in self.associations {
42 | observation.appendComponent(.init(
43 | code: CodeableConcept(coding: mapping.association.codings.map(\.coding)),
44 | value: .string(association.stringValue.asFHIRStringPrimitive())
45 | ))
46 | }
47 | }
48 | }
49 |
50 |
51 | @available(iOS 18.0, watchOS 11.0, macCatalyst 18.0, macOS 15.0, visionOS 2.0, *)
52 | extension HKStateOfMind.Kind {
53 | var stringValue: String {
54 | switch self {
55 | case .momentaryEmotion:
56 | "momentary emotion"
57 | case .dailyMood:
58 | "daily mood"
59 | @unknown default:
60 | "unknown"
61 | }
62 | }
63 | }
64 |
65 |
66 | @available(iOS 18.0, watchOS 11.0, macCatalyst 18.0, macOS 15.0, visionOS 2.0, *)
67 | extension HKStateOfMind.ValenceClassification {
68 | var stringValue: String {
69 | switch self {
70 | case .veryUnpleasant:
71 | "very unpleasant"
72 | case .unpleasant:
73 | "unpleasant"
74 | case .slightlyUnpleasant:
75 | "slightly unpleasant"
76 | case .neutral:
77 | "neutral"
78 | case .slightlyPleasant:
79 | "slightly pleasant"
80 | case .pleasant:
81 | "pleasant"
82 | case .veryPleasant:
83 | "very pleasant"
84 | @unknown default:
85 | "unknown"
86 | }
87 | }
88 | }
89 |
90 |
91 | @available(iOS 18.0, watchOS 11.0, macCatalyst 18.0, macOS 15.0, visionOS 2.0, *)
92 | extension HKStateOfMind.Label {
93 | var stringValue: String {
94 | switch self {
95 | case .amazed:
96 | "amazed"
97 | case .amused:
98 | "amused"
99 | case .angry:
100 | "angry"
101 | case .anxious:
102 | "anxious"
103 | case .ashamed:
104 | "ashamed"
105 | case .brave:
106 | "brave"
107 | case .calm:
108 | "calm"
109 | case .content:
110 | "content"
111 | case .disappointed:
112 | "disappointed"
113 | case .discouraged:
114 | "discouraged"
115 | case .disgusted:
116 | "disgusted"
117 | case .embarrassed:
118 | "embarrassed"
119 | case .excited:
120 | "excited"
121 | case .frustrated:
122 | "frustrated"
123 | case .grateful:
124 | "grateful"
125 | case .guilty:
126 | "guilty"
127 | case .happy:
128 | "happy"
129 | case .hopeless:
130 | "hopeless"
131 | case .irritated:
132 | "irritated"
133 | case .jealous:
134 | "jealous"
135 | case .joyful:
136 | "joyful"
137 | case .lonely:
138 | "lonely"
139 | case .passionate:
140 | "passionate"
141 | case .peaceful:
142 | "peaceful"
143 | case .proud:
144 | "proud"
145 | case .relieved:
146 | "relieved"
147 | case .sad:
148 | "sad"
149 | case .scared:
150 | "scared"
151 | case .stressed:
152 | "stressed"
153 | case .surprised:
154 | "surprised"
155 | case .worried:
156 | "worried"
157 | case .annoyed:
158 | "annoyed"
159 | case .confident:
160 | "confident"
161 | case .drained:
162 | "drained"
163 | case .hopeful:
164 | "hopeful"
165 | case .indifferent:
166 | "indifferent"
167 | case .overwhelmed:
168 | "overwhelmed"
169 | case .satisfied:
170 | "satisfied"
171 | @unknown default:
172 | "unknown"
173 | }
174 | }
175 | }
176 |
177 |
178 | @available(iOS 18.0, watchOS 11.0, macCatalyst 18.0, macOS 15.0, visionOS 2.0, *)
179 | extension HKStateOfMind.Association {
180 | var stringValue: String {
181 | switch self {
182 | case .community:
183 | "community"
184 | case .currentEvents:
185 | "currentEvents"
186 | case .dating:
187 | "dating"
188 | case .education:
189 | "education"
190 | case .family:
191 | "family"
192 | case .fitness:
193 | "fitness"
194 | case .friends:
195 | "friends"
196 | case .health:
197 | "health"
198 | case .hobbies:
199 | "hobbies"
200 | case .identity:
201 | "identity"
202 | case .money:
203 | "money"
204 | case .partner:
205 | "partner"
206 | case .selfCare:
207 | "selfCare"
208 | case .spirituality:
209 | "spirituality"
210 | case .tasks:
211 | "tasks"
212 | case .travel:
213 | "travel"
214 | case .work:
215 | "work"
216 | case .weather:
217 | "weather"
218 | @unknown default:
219 | "unknown"
220 | }
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HKSampleMapping/HKSampleMapping.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This source file is part of the HealthKitOnFHIR open source project
3 | //
4 | // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
5 | //
6 | // SPDX-License-Identifier: MIT
7 | //
8 |
9 | import HealthKit
10 |
11 |
12 | /// A ``HKSampleMapping`` instance is used to specify the mapping of `HKSample`s to FHIR observations allowing the customization of, e.g., codings and units.
13 | public struct HKSampleMapping: Decodable, Sendable {
14 | private enum CodingKeys: String, CodingKey {
15 | case quantitySampleMapping = "HKQuantitySamples"
16 | case categorySampleMapping = "HKCategorySamples"
17 | case correlationMapping = "HKCorrelations"
18 | case electrocardiogramMapping = "HKElectrocardiogram"
19 | case workoutSampleMapping = "HKWorkout"
20 | case stateOfMindMapping = "HKStateOfMind"
21 | }
22 |
23 |
24 | /// A default instance of an ``HKSampleMapping`` instance allowing developers to customize the ``HKSampleMapping``.
25 | ///
26 | /// The default values are loaded from the `HKSampleMapping.json` resource in the ``HealthKitOnFHIR`` Swift Package.
27 | public static let `default`: HKSampleMapping = {
28 | Bundle.module.decode(HKSampleMapping.self, from: "HKSampleMapping.json")
29 | }()
30 |
31 |
32 | /// The mapping of `HKQuantityType`s to FHIR `Observation`s.
33 | public var quantitySampleMapping: [HKQuantityType: HKQuantitySampleMapping]
34 | /// The mapping of `HKCategoryType`s to FHIR `Observation`s.
35 | public var categorySampleMapping: [HKCategoryType: HKCategorySampleMapping]
36 | /// The mapping of `HKCorrelationType`s to FHIR `Observation`s.
37 | public var correlationMapping: [HKCorrelationType: HKCorrelationMapping]
38 | /// The mapping of `HKElectrocardiogramMapping`s to FHIR `Observation`s.
39 | public var electrocardiogramMapping: HKElectrocardiogramMapping
40 | /// The mapping of `HKWorkout`s to FHIR `Observation`s.
41 | public var workoutSampleMapping: HKWorkoutSampleMapping
42 | /// The mapping of `HKStateOfMind` samples to FHIR `Observation`s.
43 | public var stateOfMindMapping: HKStateOfMindMapping
44 |
45 |
46 | public init(from decoder: any Decoder) throws {
47 | let mappings = try decoder.container(keyedBy: CodingKeys.self)
48 | let quantityStringBasedSampleMapping = try mappings.decode(
49 | [String: HKQuantitySampleMapping].self,
50 | forKey: .quantitySampleMapping
51 | )
52 | let quantitySampleMapping = Dictionary(
53 | uniqueKeysWithValues: quantityStringBasedSampleMapping.map { mapping in
54 | guard let hkquantityType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier(rawValue: mapping.key)) else {
55 | fatalError("HKQuantityType for the String value \(mapping.key) does not exist. Please inspect your configuration.")
56 | }
57 | return (hkquantityType, mapping.value)
58 | }
59 | )
60 |
61 | let categoryStringBasedSampleMapping = try mappings.decode(
62 | [String: HKCategorySampleMapping].self,
63 | forKey: .categorySampleMapping
64 | )
65 |
66 | let categorySampleMapping = Dictionary(
67 | uniqueKeysWithValues: categoryStringBasedSampleMapping.map { mapping in
68 | guard let hkcategorytype = HKCategoryType.categoryType(forIdentifier: HKCategoryTypeIdentifier(rawValue: mapping.key)) else {
69 | fatalError("HKCategoryType for the String value \(mapping.key) does not exist. Please inspect your configuration.")
70 | }
71 | return (hkcategorytype, mapping.value)
72 | }
73 | )
74 |
75 | let correlationStringBasedMapping = try mappings.decode(
76 | [String: HKCorrelationMapping].self,
77 | forKey: .correlationMapping
78 | )
79 |
80 | let correlationMapping = Dictionary(
81 | uniqueKeysWithValues: correlationStringBasedMapping.map { mapping in
82 | let hkcorrelationTypeIdentifier = HKCorrelationTypeIdentifier(rawValue: mapping.key)
83 | guard let hkcorrelationType = HKCorrelationType.correlationType(forIdentifier: hkcorrelationTypeIdentifier) else {
84 | fatalError("HKCorrelationType for the String value \(mapping.key) does not exist. Please inspect your configuration.")
85 | }
86 | return (hkcorrelationType, mapping.value)
87 | }
88 | )
89 |
90 | let electrocardiogramMapping = try mappings.decode(HKElectrocardiogramMapping.self, forKey: .electrocardiogramMapping)
91 | let workoutSampleMapping = try mappings.decode(HKWorkoutSampleMapping.self, forKey: .workoutSampleMapping)
92 | let stateOfMindMapping = try mappings.decode(HKStateOfMindMapping.self, forKey: .stateOfMindMapping)
93 |
94 | self.init(
95 | quantitySampleMapping: quantitySampleMapping,
96 | categorySampleMapping: categorySampleMapping,
97 | correlationMapping: correlationMapping,
98 | electrocardiogramMapping: electrocardiogramMapping,
99 | workoutSampleMapping: workoutSampleMapping,
100 | stateOfMindMapping: stateOfMindMapping
101 | )
102 | }
103 |
104 | /// A ``HKSampleMapping`` instance is used to specify the mapping of `HKSample`s to FHIR observations allowing the customization of, e.g., codings and units.
105 | /// - Parameters:
106 | /// - quantitySampleMapping: The mapping of `HKQuantityType`s to FHIR Observations.
107 | /// - categorySampleMapping: The mapping of `HKCategoryType`s to FHIR `Observation`s.
108 | /// - correlationMapping: The mapping of `HKCorrelationType`s to FHIR Observations.
109 | /// - workoutSampleMapping: The mapping of `HKWorkout`s to FHIR `Observation`s.
110 | /// - electrocardiogramMapping: The mapping of `HKElectrocardiogramMapping`s to FHIR `Observation`s.
111 | /// - stateOfMindMapping: The mapping of `HKStateOfMind` samples to FHIR `Observation`s.
112 | public init(
113 | quantitySampleMapping: [HKQuantityType: HKQuantitySampleMapping] = HKQuantitySampleMapping.default,
114 | categorySampleMapping: [HKCategoryType: HKCategorySampleMapping] = HKCategorySampleMapping.default,
115 | correlationMapping: [HKCorrelationType: HKCorrelationMapping] = HKCorrelationMapping.default,
116 | electrocardiogramMapping: HKElectrocardiogramMapping = .default,
117 | workoutSampleMapping: HKWorkoutSampleMapping = .default,
118 | stateOfMindMapping: HKStateOfMindMapping = .default
119 | ) {
120 | self.quantitySampleMapping = quantitySampleMapping
121 | self.categorySampleMapping = categorySampleMapping
122 | self.correlationMapping = correlationMapping
123 | self.electrocardiogramMapping = electrocardiogramMapping
124 | self.workoutSampleMapping = workoutSampleMapping
125 | self.stateOfMindMapping = stateOfMindMapping
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
10 |
11 | # HealthKitOnFHIR
12 |
13 | [](https://github.com/StanfordBDHG/HealthKitOnFHIR/actions/workflows/build-and-test.yml)
14 | [](https://codecov.io/gh/StanfordBDHG/HealthKitOnFHIR)
15 | [](https://zenodo.org/badge/latestdoi/569837859)
16 | [](https://swiftpackageindex.com/StanfordBDHG/HealthKitOnFHIR)
17 | [](https://swiftpackageindex.com/StanfordBDHG/HealthKitOnFHIR)
18 |
19 | The HealthKitOnFHIR library provides extensions that convert supported HealthKit samples to corresponding FHIR resources using [FHIRModels](https://github.com/apple/FHIRModels) encapsulated in a [ResourceProxy](https://github.com/apple/FHIRModels/blob/main/HowTo/Instantiation.md#1-use-resourceproxy).
20 |
21 | For more information, please refer to the [API documentation](https://swiftpackageindex.com/StanfordBDHG/HealthKitOnFHIR/documentation).
22 |
23 | HealthKitOnFHIR supports:
24 | - Extensions to convert data from Apple HealthKit to HL7® FHIR® R4.
25 | - Customizable mappings between HealthKit data types and standardized codes (e.g., LOINC)
26 |
27 | Please refer to the [HKObject Support Table](Sources/HealthKitOnFHIR/HealthKitOnFHIR.docc/HKSampleSupportTables.md) for a complete list of supported types.
28 |
29 | > [!NOTE]
30 | > HealthKitOnFHIR will use the time zone specified in [HKMetadataKeyTimeZone](https://developer.apple.com/documentation/healthkit/hkmetadatakeytimezone) when creating FHIR Observations from HealthKit samples. If no time zone is specified, HealthKitOnFHIR will use the device's current time zone.
31 |
32 | ## Installation
33 |
34 | HealthKitOnFHIR can be installed into your Xcode project using [Swift Package Manager](https://github.com/apple/swift-package-manager).
35 |
36 | 1. In Xcode 14 and newer (requires Swift 5.7), go to “File” » “Add Packages...”
37 | 2. Enter the URL to this GitHub repository, then select the `HealthKitOnFHIR` package to install.
38 |
39 |
40 | ## Usage
41 |
42 | The HealthKitOnFHIR library provides extensions that convert supported HealthKit samples to corresponding FHIR resources using [FHIRModels](https://github.com/apple/FHIRModels) encapsulated in a [ResourceProxy](https://github.com/apple/FHIRModels/blob/main/HowTo/Instantiation.md#1-use-resourceproxy).
43 |
44 | ```swift
45 | let sample: HKSample = // ...
46 | let resource = try sample.resource()
47 | ```
48 |
49 | ### Observations
50 |
51 | `HKQuantitySample`, `HKCategorySample`, `HKCorrelationSample`, `HKElectrocardiogram`, and `HKWorkout` will be converted into FHIR [Observation](https://hl7.org/fhir/R4/observation.html) resources encapsulated in a [ResourceProxy](https://github.com/apple/FHIRModels/blob/main/HowTo/Instantiation.md#1-use-resourceproxy).
52 |
53 | ```swift
54 | let sample: HKQuantitySample = // ...
55 | let observation = try sample.resource().get(if: Observation.self)
56 | ```
57 |
58 | Codes and units can be customized by passing in a custom `HKSampleMapping` instance to the `resource(withMapping:)` method.
59 |
60 | ```swift
61 | let sample: HKQuantitySample = // ...
62 | let sampleMapping: HKSampleMapping = // ...
63 | let observation = try sample.resource(withMapping: sampleMapping).get(if: Observation.self)
64 | ```
65 |
66 | ### Clinical Records
67 |
68 | `HKClinicalRecord` will be converted to FHIR resources based on the type of its underlying data. Only records encoded in FHIR R4 are supported at this time.
69 |
70 | ```swift
71 | let allergyRecord: HKClinicalRecord = // ...
72 | let allergyIntolerance = try allergyRecord.resource().get(if: AllergyIntolerance.self)
73 | ```
74 |
75 |
76 | ## Example
77 |
78 | In the following example, we will query the HealthKit store for heart rate data, convert the resulting samples to FHIR observations, and encode them into JSON.
79 |
80 | ```swift
81 | import HealthKitOnFHIR
82 |
83 | // Initialize an HKHealthStore instance and request permissions with it
84 | // ...
85 |
86 | let date = ISO8601DateFormatter().date(from: "1885-11-11T00:00:00-08:00") ?? .now
87 | let sample = HKQuantitySample(
88 | type: HKQuantityType(.heartRate),
89 | quantity: HKQuantity(unit: HKUnit.count().unitDivided(by: .minute()), doubleValue: 42.0),
90 | start: date,
91 | end: date
92 | )
93 |
94 | // Convert the results to FHIR observations
95 | let observation: Observation?
96 | do {
97 | try observation = sample.resource().get(if: Observation.self)
98 | } catch {
99 | // Handle any mapping errors here.
100 | // ...
101 | }
102 |
103 | // Encode FHIR observations as JSON
104 | let encoder = JSONEncoder()
105 | encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes, .sortedKeys]
106 |
107 | guard let observation,
108 | let data = try? encoder.encode(observation) else {
109 | // Handle any encoding errors here.
110 | // ...
111 | }
112 |
113 | // Print the resulting JSON
114 | let json = String(decoding: data, as: UTF8.self)
115 | print(json)
116 | ```
117 |
118 | The following example generates the following FHIR observation:
119 |
120 | ```json
121 | {
122 | "code" : {
123 | "coding" : [
124 | {
125 | "code" : "8867-4",
126 | "display" : "Heart rate",
127 | "system" : "http://loinc.org"
128 | }
129 | ]
130 | },
131 | "effectiveDateTime" : "1885-11-11T00:00:00-08:00",
132 | "identifier" : [
133 | {
134 | "id" : "8BA093D9-B99B-4A3C-8C9E-98C86F49F5D8"
135 | }
136 | ],
137 | "issued" : "2023-01-01T00:00:00-08:00",
138 | "resourceType" : "Observation",
139 | "status" : "final",
140 | "valueQuantity" : {
141 | "code": "/min",
142 | "unit": "beats/minute",
143 | "system": "http://unitsofmeasure.org",
144 | "value" : 42
145 | }
146 | }
147 | ```
148 |
149 |
150 | ## License
151 |
152 | This project is licensed under the MIT License. See [Licenses](https://github.com/StanfordBDHG/HealthKitOnFHIR/tree/main/LICENSES) for more information.
153 |
154 |
155 | ## Contributors
156 |
157 | This project is developed as part of the Stanford Biodesign for Digital Health projects at Stanford.
158 | See [CONTRIBUTORS.md](https://github.com/StanfordBDHG/HealthKitOnFHIR/tree/main/CONTRIBUTORS.md) for a full list of all HealthKitOnFHIR contributors.
159 |
160 |
161 | ## Notices
162 |
163 | HealthKit is a registered trademark of Apple, Inc.
164 | FHIR is a registered trademark of Health Level Seven International.
165 |
166 | 
167 | 
168 |
--------------------------------------------------------------------------------
/Sources/HealthKitOnFHIR/HealthKitOnFHIR.docc/SupportedHKCategoryTypes.md:
--------------------------------------------------------------------------------
1 | # Supported HKCategoryTypes
2 |
11 |
12 | HealthKitOnFHIR supports 63 of 63 category types.
13 |
14 | |HKCategoryType|Supported|
15 | |----|----|
16 | |[AbdominalCramps](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierAbdominalCramps)|✅|
17 | |[Acne](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierAcne)|✅|
18 | |[AppetiteChanges](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierAppetiteChanges)|✅|
19 | |[AppleStandHour](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierAppleStandHour)|✅|
20 | |[AppleWalkingSteadinessEvent](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierAppleWalkingSteadinessEvent)|✅|
21 | |[AudioExposureEvent](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierAudioExposureEvent)|✅|
22 | |[BladderIncontinence](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierBladderIncontinence)|✅|
23 | |[Bloating](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierBloating)|✅|
24 | |[BreastPain](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierBreastPain)|✅|
25 | |[CervicalMucusQuality](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierCervicalMucusQuality)|✅|
26 | |[ChestTightnessOrPain](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierChestTightnessOrPain)|✅|
27 | |[Chills](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierChills)|✅|
28 | |[Constipation](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierConstipation)|✅|
29 | |[Contraceptive](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierContraceptive)|✅|
30 | |[Coughing](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierCoughing)|✅|
31 | |[Dizziness](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierDizziness)|✅|
32 | |[DrySkin](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierDrySkin)|✅|
33 | |[Fainting](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierFainting)|✅|
34 | |[Fatigue](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierFatigue)|✅|
35 | |[Fever](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierFever)|✅|
36 | |[GeneralizedBodyAche](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierGeneralizedBodyAche)|✅|
37 | |[HairLoss](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierHairLoss)|✅|
38 | |[HandwashingEvent](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierHandwashingEvent)|✅|
39 | |[Headache](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierHeadache)|✅|
40 | |[HeadphoneAudioExposureEvent](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierHeadphoneAudioExposureEvent)|✅|
41 | |[Heartburn](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierHeartburn)|✅|
42 | |[HighHeartRateEvent](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierHighHeartRateEvent)|✅|
43 | |[HotFlashes](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierHotFlashes)|✅|
44 | |[InfrequentMenstrualCycles](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierInfrequentMenstrualCycles)|✅|
45 | |[IntermenstrualBleeding](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierIntermenstrualBleeding)|✅|
46 | |[IrregularHeartRhythmEvent](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierIrregularHeartRhythmEvent)|✅|
47 | |[IrregularMenstrualCycles](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierIrregularMenstrualCycles)|✅|
48 | |[Lactation](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierLactation)|✅|
49 | |[LossOfSmell](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierLossOfSmell)|✅|
50 | |[LossOfTaste](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierLossOfTaste)|✅|
51 | |[LowCardioFitnessEvent](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierLowCardioFitnessEvent)|✅|
52 | |[LowHeartRateEvent](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierLowHeartRateEvent)|✅|
53 | |[LowerBackPain](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierLowerBackPain)|✅|
54 | |[MemoryLapse](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierMemoryLapse)|✅|
55 | |[MenstrualFlow](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierMenstrualFlow)|✅|
56 | |[MindfulSession](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierMindfulSession)|✅|
57 | |[MoodChanges](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierMoodChanges)|✅|
58 | |[Nausea](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierNausea)|✅|
59 | |[NightSweats](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierNightSweats)|✅|
60 | |[OvulationTestResult](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierOvulationTestResult)|✅|
61 | |[PelvicPain](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierPelvicPain)|✅|
62 | |[PersistentIntermenstrualBleeding](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierPersistentIntermenstrualBleeding)|✅|
63 | |[PregnancyTestResult](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierPregnancyTestResult)|✅|
64 | |[ProgesteroneTestResult](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierProgesteroneTestResult)|✅|
65 | |[ProlongedMenstrualPeriods](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierProlongedMenstrualPeriods)|✅|
66 | |[RapidPoundingOrFlutteringHeartbeat](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierRapidPoundingOrFlutteringHeartbeat)|✅|
67 | |[RunnyNose](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierRunnyNose)|✅|
68 | |[SexualActivity](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierSexualActivity)|✅|
69 | |[ShortnessOfBreath](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierShortnessOfBreath)|✅|
70 | |[SinusCongestion](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierSinusCongestion)|✅|
71 | |[SkippedHeartbeat](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierSkippedHeartbeat)|✅|
72 | |[SleepAnalysis](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierSleepAnalysis)|✅|
73 | |[SleepChanges](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierSleepChanges)|✅|
74 | |[SoreThroat](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierSoreThroat)|✅|
75 | |[ToothbrushingEvent](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierToothbrushingEvent)|✅|
76 | |[VaginalDryness](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierVaginalDryness)|✅|
77 | |[Vomiting](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierVomiting)|✅|
78 | |[Wheezing](https://developer.apple.com/documentation/healthkit/HKCategoryTypeIdentifierWheezing)|✅|
79 |
--------------------------------------------------------------------------------