├── 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 | [![Build and Test](https://github.com/StanfordBDHG/HealthKitOnFHIR/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/StanfordBDHG/HealthKitOnFHIR/actions/workflows/build-and-test.yml) 14 | [![codecov](https://codecov.io/gh/StanfordBDHG/HealthKitOnFHIR/branch/main/graph/badge.svg?token=17BMMYE3AC)](https://codecov.io/gh/StanfordBDHG/HealthKitOnFHIR) 15 | [![DOI](https://zenodo.org/badge/569837859.svg)](https://zenodo.org/badge/latestdoi/569837859) 16 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FStanfordBDHG%2FHealthKitOnFHIR%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/StanfordBDHG/HealthKitOnFHIR) 17 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FStanfordBDHG%2FHealthKitOnFHIR%2Fbadge%3Ftype%3Dplatforms)](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 | ![Stanford Byers Center for Biodesign Logo](https://raw.githubusercontent.com/StanfordBDHG/.github/main/assets/biodesign-footer-light.png#gh-light-mode-only) 167 | ![Stanford Byers Center for Biodesign Logo](https://raw.githubusercontent.com/StanfordBDHG/.github/main/assets/biodesign-footer-dark.png#gh-dark-mode-only) 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 | --------------------------------------------------------------------------------