├── .github └── FUNDING.yml ├── HealthLens ├── Assets.xcassets │ ├── Contents.json │ ├── Icon.imageset │ │ ├── healthlens-logo 1.png │ │ ├── healthlens-logo 2.png │ │ ├── healthlens-logo.png │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── healthlens-logo.png │ │ └── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── Excel.imageset │ │ ├── Contents.json │ │ └── excel.svg │ └── CSV.imageset │ │ ├── Contents.json │ │ └── csv-svgrepo-com.svg ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Logger.swift ├── HealthLensApp.swift ├── Info.plist ├── HealthLens.entitlements ├── ExportFile.swift ├── LaunchScreen.storyboard ├── Groups.swift ├── ContentView.swift └── ContentViewModel.swift ├── typos.toml ├── HealthLens.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── xcuserdata │ └── wkaiser.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── project.pbxproj ├── HealthLensUITests ├── HealthLensUITestsLaunchTests.swift ├── MarketingScreenShotView.swift └── HealthLensUITests.swift ├── HealthLensTests └── HealthLensTests.swift ├── .gitignore ├── PRIVACY.md ├── .swiftformat ├── README.md ├── CODE_OF_CONDUCT.md └── LICENSE /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [wkaisertexas] 2 | -------------------------------------------------------------------------------- /HealthLens/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /typos.toml: -------------------------------------------------------------------------------- 1 | [files] 2 | extend-exclude = [ 3 | "**/*.xcstrings", 4 | "**/*.pbxproj", 5 | ] 6 | 7 | [default.extend-words] 8 | -------------------------------------------------------------------------------- /HealthLens/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /HealthLens/Assets.xcassets/Icon.imageset/healthlens-logo 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wkaisertexas/HealthLens/HEAD/HealthLens/Assets.xcassets/Icon.imageset/healthlens-logo 1.png -------------------------------------------------------------------------------- /HealthLens/Assets.xcassets/Icon.imageset/healthlens-logo 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wkaisertexas/HealthLens/HEAD/HealthLens/Assets.xcassets/Icon.imageset/healthlens-logo 2.png -------------------------------------------------------------------------------- /HealthLens/Assets.xcassets/Icon.imageset/healthlens-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wkaisertexas/HealthLens/HEAD/HealthLens/Assets.xcassets/Icon.imageset/healthlens-logo.png -------------------------------------------------------------------------------- /HealthLens/Assets.xcassets/AppIcon.appiconset/healthlens-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wkaisertexas/HealthLens/HEAD/HealthLens/Assets.xcassets/AppIcon.appiconset/healthlens-logo.png -------------------------------------------------------------------------------- /HealthLens/Logger.swift: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | /// `logger` singleton used to remove print statements and streamline debugging experience 4 | /// 5 | /// Soo much better than print statements tbh 6 | let logger = Logger() 7 | -------------------------------------------------------------------------------- /HealthLens.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /HealthLens/HealthLensApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftData 2 | import SwiftUI 3 | 4 | @main 5 | struct HealthLensApp: App { 6 | var body: some Scene { 7 | WindowGroup { 8 | ContentView() 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /HealthLens/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 | -------------------------------------------------------------------------------- /HealthLens/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /HealthLens.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /HealthLens/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "healthlens-logo.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /HealthLens/HealthLens.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.healthkit 6 | 7 | com.apple.developer.healthkit.access 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /HealthLens/Assets.xcassets/Excel.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "excel.svg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /HealthLens/Assets.xcassets/CSV.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "csv-svgrepo-com.svg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /HealthLens.xcodeproj/xcuserdata/wkaiser.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | HealthLens.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /HealthLens.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "9df483d0011f9e646fbbd8b12b9f9bf8f42953d44971f6e9f18d8341d18f567b", 3 | "pins" : [ 4 | { 5 | "identity" : "libxlsxwriter", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/jmcnamara/libxlsxwriter", 8 | "state" : { 9 | "branch" : "main", 10 | "revision" : "14f13513cb140092a913a91fce719ff7dc36e332" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /HealthLens/Assets.xcassets/Icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "healthlens-logo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "healthlens-logo 1.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "healthlens-logo 2.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /HealthLensUITests/HealthLensUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class HealthLensUITestsLaunchTests: XCTestCase { 4 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 5 | return false 6 | } 7 | 8 | override func setUpWithError() throws { 9 | continueAfterFailure = false 10 | } 11 | 12 | let locales = ["ar", "fr_FR", "es_ES"] 13 | 14 | func testLaunch() throws { 15 | for locale in locales { 16 | let app = XCUIApplication() 17 | 18 | // 1) Set up the environment and arguments for the locale 19 | // AppleLanguages is an array; AppleLocale is a single string 20 | app.launchArguments += [ 21 | "-AppleLanguages", "(\(locale))", 22 | "-AppleLocale", locale, 23 | ] 24 | // Optional: Set measurement units, temperature units, etc. if needed 25 | // app.launchArguments += ["-AppleMeasurementUnits", "Centimeters", "-AppleTemperatureUnit", "Celsius"] 26 | 27 | // Launch the app 28 | app.launch() 29 | 30 | // Screenshot + add it to the test result 31 | let screenshot = app.screenshot() 32 | let attachment = XCTAttachment(screenshot: screenshot) 33 | 34 | attachment.name = "\(locale)-HomeScreen" 35 | attachment.lifetime = .keepAlways 36 | 37 | add(attachment) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /HealthLensTests/HealthLensTests.swift: -------------------------------------------------------------------------------- 1 | import HealthKit 2 | import XCTest 3 | 4 | @testable import HealthLens 5 | 6 | /// for testing, use healthkit's testing framework in swift: https://github.com/StanfordBDHG/XCTHealthKit 7 | final class HealthLensTests: XCTestCase { 8 | 9 | override func setUpWithError() throws { 10 | // Put setup code here. This method is called before the invocation of each test method in the class. 11 | } 12 | 13 | override func tearDownWithError() throws { 14 | // Put teardown code here. This method is called after the invocation of each test method in the class. 15 | } 16 | 17 | func testCSVSanitization() throws { 18 | let viewModel = ContentViewModel() 19 | 20 | let normal_string = "asdfabasdfasdf" 21 | let abnormal_string = "asdfabasdfasdf\n" 22 | 23 | XCTAssertEqual( 24 | viewModel.sanitizeForCSV(normal_string).count, normal_string.count, 25 | "sanitization should not have changed width") 26 | XCTAssertNotEqual( 27 | viewModel.sanitizeForCSV(abnormal_string).count, abnormal_string.count, 28 | "width should have changed") 29 | } 30 | 31 | func testPerformanceExample() throws { 32 | // This is an example of a performance test case. 33 | self.measure { 34 | // Put the code you want to measure the time of here. 35 | // we are going to measure the collecting of test information 36 | 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /HealthLensUITests/MarketingScreenShotView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MarketingScreenShotView.swift 3 | // HealthLens 4 | // 5 | // Created by William Kaiser on 1/31/25. 6 | // 7 | 8 | 9 | import SwiftUI 10 | 11 | struct MarketingScreenshotView: View { 12 | let screenshot: UIImage // the screenshot you captured programmatically 13 | 14 | // Example desired size 15 | var targetWidth: CGFloat = 460 16 | var targetHeight: CGFloat = 997 17 | 18 | var body: some View { 19 | ZStack { 20 | // 1) A background color or gradient 21 | Color("BackgroundColor") 22 | .ignoresSafeArea() 23 | 24 | // 2) Optionally add marketing text at the top 25 | VStack(alignment: .leading, spacing: 16) { 26 | Text("Export your health data as a CSV") 27 | .font(.system(size: 32, weight: .bold)) 28 | .foregroundColor(.white) 29 | .frame(maxWidth: .infinity, alignment: .leading) 30 | .padding(.horizontal) 31 | 32 | // 3) Show the screenshot in a phone frame or by itself 33 | Image(uiImage: screenshot) 34 | .resizable() 35 | .scaledToFit() 36 | .clipShape(RoundedRectangle(cornerRadius: 30)) 37 | .shadow(radius: 10) 38 | .padding() 39 | 40 | Spacer() 41 | } 42 | } 43 | // 4) Force the view to a specific “marketing” size 44 | .frame(width: targetWidth, height: targetHeight) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /HealthLensUITests/HealthLensUITests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftUI 3 | 4 | final class HealthLensUITests: XCTestCase { 5 | 6 | override func setUpWithError() throws { 7 | // Put setup code here. This method is called before the invocation of each test method in the class. 8 | 9 | // In UI tests it is usually best to stop immediately when a failure occurs. 10 | continueAfterFailure = false 11 | 12 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 13 | } 14 | 15 | override func tearDownWithError() throws { 16 | // Put teardown code here. This method is called after the invocation of each test method in the class. 17 | } 18 | 19 | @MainActor 20 | func testExample() throws { 21 | 22 | let app = XCUIApplication() 23 | app.launchEnvironment["UITestInterfaceStyle"] = "Light" 24 | app.launchArguments.append("--uitesting-lightmode") 25 | app.launch() 26 | 27 | let screenshot = app.screenshot() 28 | let marketingView = MarketingScreenshotView(screenshot: screenshot.image) 29 | let renderer = ImageRenderer(content: marketingView) 30 | 31 | renderer.scale = UIScreen.main.scale 32 | 33 | let attachment = XCTAttachment(image: renderer.uiImage!) 34 | 35 | attachment.name = "MarketingScreenshot" 36 | attachment.lifetime = .keepAlways 37 | 38 | add(attachment) 39 | } 40 | 41 | func testLaunchPerformance() throws { 42 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 43 | // This measures how long it takes to launch your application. 44 | measure(metrics: [XCTApplicationLaunchMetric()]) { 45 | XCUIApplication().launch() 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## Obj-C/Swift specific 9 | *.hmap 10 | 11 | ## App packaging 12 | *.ipa 13 | *.dSYM.zip 14 | *.dSYM 15 | 16 | ## Playgrounds 17 | timeline.xctimeline 18 | playground.xcworkspace 19 | 20 | # Swift Package Manager 21 | # 22 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 23 | # Packages/ 24 | # Package.pins 25 | # Package.resolved 26 | # *.xcodeproj 27 | # 28 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 29 | # hence it is not needed unless you have added a package configuration file to your project 30 | # .swiftpm 31 | 32 | .build/ 33 | 34 | # CocoaPods 35 | # 36 | # We recommend against adding the Pods directory to your .gitignore. However 37 | # you should judge for yourself, the pros and cons are mentioned at: 38 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 39 | # 40 | # Pods/ 41 | # 42 | # Add this line if you want to avoid checking in source code from the Xcode workspace 43 | # *.xcworkspace 44 | 45 | # Carthage 46 | # 47 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 48 | # Carthage/Checkouts 49 | 50 | Carthage/Build/ 51 | 52 | # fastlane 53 | # 54 | # It is recommended to not store the screenshots in the git repo. 55 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 56 | # For more information about the recommended setup visit: 57 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 58 | 59 | fastlane/report.xml 60 | fastlane/Preview.html 61 | fastlane/screenshots/**/*.png 62 | fastlane/test_output 63 | -------------------------------------------------------------------------------- /HealthLens/ExportFile.swift: -------------------------------------------------------------------------------- 1 | import CoreData 2 | import SwiftUI 3 | import UniformTypeIdentifiers 4 | 5 | enum ExportFormat: String, CaseIterable { 6 | case csv 7 | case xlsx 8 | } 9 | 10 | struct CSVExportFile { 11 | typealias FileExportType = () async -> URL? 12 | typealias FileType = () -> String 13 | public var collectData: FileExportType? 14 | public var fileName: FileType? 15 | } 16 | 17 | extension CSVExportFile: Transferable { 18 | enum ShareError: Error { 19 | case failed 20 | } 21 | 22 | func shareURL() async -> URL? { 23 | guard let collectData = collectData else { 24 | return nil 25 | } 26 | 27 | guard let result = await collectData() else { 28 | return nil 29 | } 30 | 31 | return result 32 | } 33 | 34 | static var transferRepresentation: some TransferRepresentation { 35 | FileRepresentation(exportedContentType: .commaSeparatedText) { object in 36 | .init(await object.shareURL()!) 37 | }.suggestedFileName { $0.fileName?() ?? "healthData" } 38 | .visibility(.all) 39 | } 40 | } 41 | 42 | struct XLSXExportFile { 43 | typealias FileExportType = () async -> URL? 44 | typealias FileType = () -> String 45 | public var collectData: FileExportType? 46 | public var fileName: FileType? 47 | } 48 | 49 | extension UTType { 50 | static let xlsx = 51 | UTType("org.openxmlformats.spreadsheetml.sheet") 52 | ?? .spreadsheet 53 | } 54 | 55 | extension XLSXExportFile: Transferable { 56 | enum ShareError: Error { 57 | case failed 58 | } 59 | 60 | func shareURL() async -> URL? { 61 | guard let collectData = collectData else { 62 | return nil 63 | } 64 | 65 | guard let result = await collectData() else { 66 | return nil 67 | } 68 | 69 | return result 70 | } 71 | 72 | /// Creates a data representation transfer which is setup as a comma separated text 73 | static var transferRepresentation: some TransferRepresentation { 74 | FileRepresentation(exportedContentType: .xlsx) { object in 75 | .init(await object.shareURL()!) 76 | }.suggestedFileName { $0.fileName?() ?? "healthData.xlsx" } 77 | .visibility(.all) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | Privacy Policy for HealthLens 2 | 3 | Effective Date: July 22nd, 2024 4 | 5 | HealthLens, hereafter referred to as "we," "us," or "HealthLens," respects your privacy and is committed to protecting any personal information that you may provide while using our MacOS application, HealthLens, hereafter referred to as "the App." This Privacy Policy outlines our practices concerning the collection, use, and disclosure of your information. 6 | 7 | Information We Do Not Collect: 8 | 9 | No Personal Information: The App does not collect any personally identifiable information. We do not request, access, or store your name, address, phone number, or any other personal details. 10 | 11 | No Internet Connection: The App operates entirely offline, and it does not require an internet connection for its core functionality. Consequently, we do not collect any data from online sources. 12 | 13 | No Usage Analytics: We do not employ any analytics tools to track your usage patterns within the App. We respect your privacy and do not monitor how you interact with the application. 14 | 15 | Information We Do Collect: 16 | 17 | None: The App does not collect any information from users. As a result, there is no data stored locally or remotely related to your use of the App. How We Use Your Information: 18 | 19 | Since we do not collect any personal information, we do not use your information for any purpose. The App is designed to operate without the need for user data. 20 | 21 | Third-Party Access: 22 | 23 | We do not share, sell, or transfer any information with third parties. As the App operates offline and does not collect data, there is no information to share with external entities. 24 | 25 | Security: 26 | 27 | We take reasonable steps to ensure that the information collected by the App is secure. However, due to the offline nature of the App and the absence of data collection, the risk of unauthorized access is minimal. 28 | 29 | Changes to this Privacy Policy: 30 | 31 | We reserve the right to update or modify this Privacy Policy at any time without prior notice. Any changes will be effective immediately upon posting the updated Privacy Policy on our website. 32 | 33 | Contact Us: 34 | 35 | If you have any questions or concerns about this Privacy Policy or the App's privacy practices, please contact us at wkaisertexas@gmail.com. 36 | 37 | By using the App, you agree to the terms and conditions outlined in this Privacy Policy. 38 | 39 | Thank you for choosing HealthLens! 40 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | { 2 | "fileScopedDeclarationPrivacy" : { 3 | "accessLevel" : "private" 4 | }, 5 | "indentConditionalCompilationBlocks" : true, 6 | "indentSwitchCaseLabels" : false, 7 | "indentation" : { 8 | "spaces" : 2 9 | }, 10 | "lineBreakAroundMultilineExpressionChainComponents" : false, 11 | "lineBreakBeforeControlFlowKeywords" : false, 12 | "lineBreakBeforeEachArgument" : false, 13 | "lineBreakBeforeEachGenericRequirement" : false, 14 | "lineLength" : 100, 15 | "maximumBlankLines" : 1, 16 | "multiElementCollectionTrailingCommas" : true, 17 | "noAssignmentInExpressions" : { 18 | "allowedFunctions" : [ 19 | "XCTAssertNoThrow" 20 | ] 21 | }, 22 | "prioritizeKeepingFunctionOutputTogether" : false, 23 | "respectsExistingLineBreaks" : true, 24 | "rules" : { 25 | "AllPublicDeclarationsHaveDocumentation" : false, 26 | "AlwaysUseLiteralForEmptyCollectionInit" : false, 27 | "AlwaysUseLowerCamelCase" : true, 28 | "AmbiguousTrailingClosureOverload" : true, 29 | "BeginDocumentationCommentWithOneLineSummary" : false, 30 | "DoNotUseSemicolons" : true, 31 | "DontRepeatTypeInStaticProperties" : true, 32 | "FileScopedDeclarationPrivacy" : true, 33 | "FullyIndirectEnum" : true, 34 | "GroupNumericLiterals" : true, 35 | "IdentifiersMustBeASCII" : true, 36 | "NeverForceUnwrap" : false, 37 | "NeverUseForceTry" : false, 38 | "NeverUseImplicitlyUnwrappedOptionals" : false, 39 | "NoAccessLevelOnExtensionDeclaration" : true, 40 | "NoAssignmentInExpressions" : true, 41 | "NoBlockComments" : true, 42 | "NoCasesWithOnlyFallthrough" : true, 43 | "NoEmptyTrailingClosureParentheses" : true, 44 | "NoLabelsInCasePatterns" : true, 45 | "NoLeadingUnderscores" : false, 46 | "NoParensAroundConditions" : true, 47 | "NoPlaygroundLiterals" : true, 48 | "NoVoidReturnOnFunctionSignature" : true, 49 | "OmitExplicitReturns" : false, 50 | "OneCasePerLine" : true, 51 | "OneVariableDeclarationPerLine" : true, 52 | "OnlyOneTrailingClosureArgument" : true, 53 | "OrderedImports" : true, 54 | "ReplaceForEachWithForLoop" : true, 55 | "ReturnVoidInsteadOfEmptyTuple" : true, 56 | "TypeNamesShouldBeCapitalized" : true, 57 | "UseEarlyExits" : false, 58 | "UseExplicitNilCheckInConditions" : true, 59 | "UseLetInEveryBoundCaseVariable" : true, 60 | "UseShorthandTypeNames" : true, 61 | "UseSingleLinePropertyGetter" : true, 62 | "UseSynthesizedInitializer" : true, 63 | "UseTripleSlashForDocumentationComments" : true, 64 | "UseWhereClausesInForLoops" : false, 65 | "ValidateDocumentationComments" : false 66 | }, 67 | "spacesAroundRangeFormationOperators" : false, 68 | "tabWidth" : 8, 69 | "version" : 1 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HealthLens 2 | 3 |

Health Lens - CSV Exporter

4 | 5 |

6 | Health's missing export button 7 |

8 | 9 |

10 | Introduction · 11 | Features · 12 | Installation · 13 | Roadmap · 14 | Contributing 15 |

16 |
17 | 18 | ## Introduction 19 | 20 |

21 | 22 | 23 | Health Lens' homescreen 24 | 25 |

26 | 27 |

28 | HealthLens is an open-source iOS application designed to be the missing "export" button for health-related data, offering data-obsessed users personalized analytics and visualization tools. 29 |

30 | 31 | ## Features 32 | 33 | - **Data Export**: Easily export your Apple Health data to common formats like CSV and XLSX. 34 | - **Personalized Analytics**: Analyze your health data with customizable visualizations. 35 | - **User-Friendly Design**: Intuitive interface for seamless data management and insights. 36 | - **Future-Proof**: Built with Swift Charts for future native visualizations. 37 | 38 | ## Installation 39 | 40 | The recommended way to install **HealthLens** is through the [App Store](https://apps.apple.com/app/health-lens-csv-exporter/id6578440958). 41 | 42 | ## Local Development 43 | 44 | To develop HealthLens locally, you will need to clone and open this repository in Xcode. 45 | 46 | Once that's done, you can use the following commands to run the app locally: 47 | 48 | ```bash 49 | git clone https://github.com/wkaisertexas/HealthLens 50 | cd HealthLens 51 | open HealthLens.xcodeproj 52 | ``` 53 | 54 | Ensure you have the necessary permissions for HealthKit and follow the Xcode prompts for local signing. 55 | 56 | ## Roadmap 57 | 58 | ### Short-Term Goals 59 | - **Initial Release**: Basic export functionality for health data in CSV and XLSX formats. 60 | - **User Interface Improvements**: Enhance the UI for better user experience. 61 | 62 | ### Long-Term Goals 63 | - **Advanced Visualizations**: Integrate Swift Charts for robust and interactive data visualizations. 64 | - **Data Analysis Showcase**: Provide meaningful health data analysis and insights within the app. 65 | 66 | ## Contributing 67 | 68 | We love our contributors! Here's how you can contribute: 69 | 70 | - [Open an issue](https://github.com/wkaisertexas/HealthLens/issues) if you believe you've encountered a bug. 71 | - Make a [pull request](https://github.com/wkaisertexas/HealthLens/pulls) to add new features, make quality-of-life improvements, or fix bugs. 72 | 73 | 74 | 75 | 76 | 77 | ![Alt](https://repobeats.axiom.co/api/embed/83ed202554b095482847f899de57ba51a493842c.svg "Repobeats analytics image") 78 | 79 | ## License 80 | 81 | HealthLens is open-source under the [CC-BY License](LICENSE). 82 | 83 | > [!IMPORTANT] 84 | > If you liked this project, consider giving the repository a star ⭐️! 85 | -------------------------------------------------------------------------------- /HealthLens/Assets.xcassets/Excel.imageset/excel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ]> 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 26 | 27 | 29 | 31 | 33 | 35 | 36 | 37 | 38 | 39 | 40 | 42 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /HealthLens/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /HealthLens/Assets.xcassets/CSV.imageset/csv-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | 40 | 41 | 42 | 46 | 47 | 48 | 49 | 50 | 52 | 53 | 54 | 55 | 56 | 58 | 59 | 60 | 61 | 62 | 64 | 65 | 66 | 67 | 68 | 70 | 71 | 72 | 73 | 74 | 76 | 77 | 78 | 79 | 80 | 82 | 83 | 84 | 85 | 86 | 88 | 89 | 90 | 91 | 92 | 94 | 95 | 96 | 97 | 98 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | wkaisertexas@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /HealthLens/Groups.swift: -------------------------------------------------------------------------------- 1 | import HealthKit 2 | 3 | /// A `CategoryGroup` links together quantity and category types into an object used to represent a user interface menu 4 | struct CategoryGroup: Identifiable, Hashable { 5 | let id: UUID = UUID() // prevents swiftui complaining about rending the same groups 6 | let name: String 7 | let quantities: [HKQuantityTypeIdentifier] 8 | let categories: [HKCategoryTypeIdentifier] 9 | 10 | var hasBoth: Bool { 11 | quantities.count != 0 && categories.count != 0 12 | } 13 | } 14 | 15 | let bodyMeasurementsGroup = CategoryGroup( 16 | name: "Body Measurements", 17 | quantities: [ 18 | .bodyMass, .bodyMassIndex, .leanBodyMass, .height, .waistCircumference, .bodyFatPercentage, 19 | .electrodermalActivity, 20 | ], 21 | categories: [] 22 | ) 23 | 24 | let fitnessGroup = CategoryGroup( 25 | name: "Fitness", 26 | quantities: [ 27 | .activeEnergyBurned, .appleExerciseTime, .appleMoveTime, .appleStandTime, .basalEnergyBurned, 28 | .cyclingCadence, 29 | .cyclingFunctionalThresholdPower, .cyclingPower, .cyclingSpeed, .distanceCycling, 30 | .distanceDownhillSnowSports, 31 | .distanceSwimming, .distanceWalkingRunning, .distanceWheelchair, .flightsClimbed, 32 | .physicalEffort, .pushCount, 33 | .runningPower, .runningSpeed, .stepCount, .swimmingStrokeCount, .underwaterDepth, 34 | ], 35 | categories: [] 36 | ) 37 | 38 | let hearingHealthGroup = CategoryGroup( 39 | name: "Hearing Health", 40 | quantities: [ 41 | .environmentalAudioExposure, .environmentalSoundReduction, .headphoneAudioExposure, 42 | ], 43 | categories: [ 44 | .environmentalAudioExposureEvent, .headphoneAudioExposureEvent, 45 | ] 46 | ) 47 | 48 | let heartGroup = CategoryGroup( 49 | name: "Heart", 50 | quantities: [ 51 | .atrialFibrillationBurden, .heartRate, .heartRateRecoveryOneMinute, .heartRateVariabilitySDNN, 52 | .peripheralPerfusionIndex, 53 | .restingHeartRate, .vo2Max, .walkingHeartRateAverage, 54 | ], 55 | categories: [ 56 | .highHeartRateEvent, .irregularHeartRhythmEvent, .lowCardioFitnessEvent, .lowHeartRateEvent, 57 | ] 58 | ) 59 | 60 | let mobilityGroup = CategoryGroup( 61 | name: "Mobility", 62 | quantities: [ 63 | .appleWalkingSteadiness, .runningGroundContactTime, .runningStrideLength, 64 | .runningVerticalOscillation, 65 | .sixMinuteWalkTestDistance, .stairAscentSpeed, .stairDescentSpeed, .walkingAsymmetryPercentage, 66 | .walkingDoubleSupportPercentage, .walkingSpeed, .walkingStepLength, 67 | ], 68 | categories: [ 69 | .appleWalkingSteadinessEvent 70 | ] 71 | ) 72 | 73 | let nutritionGroup = CategoryGroup( 74 | name: "Nutrition", 75 | quantities: [ 76 | .dietaryBiotin, .dietaryCaffeine, .dietaryCalcium, .dietaryCarbohydrates, .dietaryChloride, 77 | .dietaryCholesterol, 78 | .dietaryChromium, .dietaryCopper, .dietaryEnergyConsumed, .dietaryFatMonounsaturated, 79 | .dietaryFatPolyunsaturated, 80 | .dietaryFatSaturated, .dietaryFatTotal, .dietaryFiber, .dietaryFolate, .dietaryIodine, 81 | .dietaryIron, .dietaryMagnesium, 82 | .dietaryManganese, .dietaryMolybdenum, .dietaryNiacin, .dietaryPantothenicAcid, 83 | .dietaryPhosphorus, .dietaryPotassium, 84 | .dietaryProtein, .dietaryRiboflavin, .dietarySelenium, .dietarySodium, .dietarySugar, 85 | .dietaryThiamin, .dietaryVitaminA, 86 | .dietaryVitaminB12, .dietaryVitaminB6, .dietaryVitaminC, .dietaryVitaminD, .dietaryVitaminE, 87 | .dietaryVitaminK, .dietaryWater, 88 | .dietaryZinc, 89 | ], 90 | categories: [] 91 | ) 92 | 93 | let otherGroup = CategoryGroup( 94 | name: "Other", 95 | quantities: [ 96 | .bloodAlcoholContent, .insulinDelivery, 97 | .numberOfAlcoholicBeverages, 98 | .numberOfTimesFallen, 99 | .appleSleepingWristTemperature, .basalBodyTemperature, 100 | // .uvExposure, 101 | // .timeInDaylight, .waterTemperature, 102 | // .bloodPressureDiastolic, .bloodPressureSystolic, 103 | ], 104 | categories: [ 105 | .handwashingEvent, .toothbrushingEvent, 106 | ] 107 | ) 108 | 109 | let reproductiveHealthGroup = CategoryGroup( 110 | name: "Reproductive Health", 111 | quantities: [ 112 | // .basalBodyTemperature 113 | ], 114 | categories: [ 115 | .cervicalMucusQuality, .contraceptive, .infrequentMenstrualCycles, .intermenstrualBleeding, 116 | .irregularMenstrualCycles, 117 | .lactation, .menstrualFlow, .ovulationTestResult, .persistentIntermenstrualBleeding, .pregnancy, 118 | .pregnancyTestResult, 119 | .progesteroneTestResult, .prolongedMenstrualPeriods, .sexualActivity, 120 | ] 121 | ) 122 | 123 | let respiratoryGroup = CategoryGroup( 124 | name: "Respiratory", 125 | quantities: [ 126 | .forcedExpiratoryVolume1, .forcedVitalCapacity, .inhalerUsage, .oxygenSaturation, 127 | .peakExpiratoryFlowRate, .respiratoryRate, 128 | ], 129 | categories: [] 130 | ) 131 | 132 | let sleepGroup = CategoryGroup( 133 | name: "Sleep", 134 | quantities: [], 135 | categories: [ 136 | .sleepAnalysis 137 | ] 138 | ) 139 | 140 | let symptomsGroup = CategoryGroup( 141 | name: "Symptoms", 142 | quantities: [], 143 | categories: [ 144 | .abdominalCramps, .acne, .appetiteChanges, .bladderIncontinence, .bloating, .breastPain, 145 | .chestTightnessOrPain, 146 | .chills, .constipation, .coughing, .diarrhea, .dizziness, .drySkin, .fainting, .fatigue, .fever, 147 | .generalizedBodyAche, 148 | .hairLoss, .headache, .heartburn, .hotFlashes, .lossOfSmell, .lossOfTaste, .lowerBackPain, 149 | .memoryLapse, .moodChanges, 150 | .nausea, .nightSweats, .pelvicPain, .rapidPoundingOrFlutteringHeartbeat, .runnyNose, 151 | .shortnessOfBreath, 152 | .sinusCongestion, .skippedHeartbeat, .sleepChanges, .soreThroat, .vaginalDryness, .vomiting, 153 | .wheezing, 154 | ] 155 | ) 156 | 157 | let vitalSignsGroup = CategoryGroup( 158 | name: "Vital Signs", 159 | quantities: [ 160 | .bloodGlucose, .bodyTemperature, 161 | ], 162 | categories: [] 163 | ) 164 | -------------------------------------------------------------------------------- /HealthLens/ContentView.swift: -------------------------------------------------------------------------------- 1 | import HealthKit 2 | import SwiftData 3 | import SwiftUI 4 | 5 | /// Represents the main content which is present in the application 6 | struct ContentView: View { 7 | @ObservedObject private var contentViewModel = ContentViewModel() 8 | 9 | @Environment(\.requestReview) private var requestReview 10 | 11 | var body: some View { 12 | NavigationStack { 13 | List { 14 | if contentViewModel.searchText.isEmpty { 15 | above_the_fold() 16 | .transition(.move(edge: .top).combined(with: .opacity)) 17 | } 18 | make_groups_section() 19 | } 20 | .animation(.easeInOut, value: contentViewModel.searchText) 21 | .navigationTitle( 22 | Text( 23 | contentViewModel.selectedQuantityTypes.count > 0 24 | ? "Exporting \(contentViewModel.selectedQuantityTypes.count) item\(contentViewModel.selectedQuantityTypes.count == 1 ? "": "s")" 25 | : "") 26 | ) 27 | .navigationBarTitleDisplayMode(.inline) 28 | .toolbar { 29 | ToolbarItem(placement: .automatic) { 30 | make_share_link() 31 | } 32 | 33 | ToolbarItem(placement: .topBarLeading) { 34 | Button(action: { 35 | withAnimation { 36 | contentViewModel.clearExportQueue() 37 | } 38 | }) { 39 | Image(systemName: "clear") 40 | } 41 | .accessibilityHint("Clear the selected HealthKit types") 42 | .disabled(contentViewModel.selectedQuantityTypes.count == 0) 43 | } 44 | }.searchable( 45 | text: $contentViewModel.searchText, 46 | prompt: "Search Health Data" 47 | ) 48 | } 49 | } 50 | 51 | @ViewBuilder 52 | func above_the_fold() -> some View { 53 | createHeader() 54 | make_export_format() 55 | make_data_range_selector() 56 | } 57 | 58 | func createHeader() -> some View { 59 | Section { 60 | VStack { 61 | Text("HealthLens").font(.largeTitle).fontWeight(.bold).frame( 62 | maxWidth: .infinity, alignment: .leading) 63 | Text("Export your health data as a CSV or XLSX file").font(.subheadline).frame( 64 | maxWidth: .infinity, alignment: .leading) 65 | } 66 | 67 | Link( 68 | destination: URL( 69 | string: "https://raw.githubusercontent.com/wkaisertexas/HealthLens/main/PRIVACY.md")! 70 | ) { 71 | HStack { 72 | Image(systemName: "lock.shield.fill") 73 | .foregroundColor(.blue) 74 | Text("Privacy Policy") 75 | } 76 | } 77 | 78 | Link(destination: URL(string: "https://github.com/wkaisertexas/healthlens")!) { 79 | HStack { 80 | Image(systemName: "chevron.left.slash.chevron.right") 81 | .foregroundColor(.green) 82 | Text("Contribute on GitHub") 83 | } 84 | } 85 | 86 | Button(action: { requestReview() }) { 87 | HStack { 88 | Image(systemName: "star.fill") 89 | .foregroundColor(.yellow) 90 | Text("Leave a Review") 91 | } 92 | } 93 | } 94 | } 95 | 96 | @ViewBuilder 97 | func make_share_link() -> some View { 98 | let excelPreviewIcon = Image("Excel") 99 | let csvPreviewIcon = Image("CSV") 100 | 101 | switch contentViewModel.selectedExportFormat { 102 | case .csv: 103 | ShareLink( 104 | item: contentViewModel.csvShareTarget, 105 | preview: SharePreview( 106 | "Exporting \(contentViewModel.makeSelectedStringDescription())", 107 | icon: csvPreviewIcon) 108 | ).disabled(contentViewModel.selectedQuantityTypes.count == 0) 109 | case .xlsx: 110 | ShareLink( 111 | item: contentViewModel.xlsxShareTarget, 112 | preview: SharePreview( 113 | "Exporting \(contentViewModel.makeSelectedStringDescription())", 114 | icon: excelPreviewIcon 115 | ) 116 | ).disabled(contentViewModel.selectedQuantityTypes.count == 0) 117 | } 118 | } 119 | 120 | func make_export_format() -> some View { 121 | Section { 122 | Picker("Export Format", selection: $contentViewModel.selectedExportFormat) { 123 | ForEach(ExportFormat.allCases, id: \.self) { format in 124 | Text(format.rawValue.uppercased()) 125 | } 126 | } 127 | } header: { 128 | Text("Export Format") 129 | } 130 | } 131 | 132 | func make_data_range_selector() -> some View { 133 | Section { 134 | VStack { 135 | Text("Select a date range to export health data").fontWeight(.bold).frame( 136 | maxWidth: .infinity, alignment: .leading) 137 | DatePicker( 138 | "Start Date", selection: $contentViewModel.startDate, in: ...contentViewModel.endDate, 139 | displayedComponents: [.date]) 140 | DatePicker( 141 | "End Date", selection: $contentViewModel.endDate, 142 | in: contentViewModel.startDate...contentViewModel.currentDate, 143 | displayedComponents: [.date]) 144 | } 145 | } header: { 146 | Text("Export Range") 147 | } 148 | } 149 | 150 | @ViewBuilder 151 | func make_groups_section() -> some View { 152 | let groups = contentViewModel.filteredCategoryGroups 153 | 154 | groups.count == 0 155 | ? Section { 156 | Text("No results for \"\(contentViewModel.searchText)\"") 157 | .font(.callout) 158 | .foregroundColor(.secondary) 159 | .padding(.vertical, 8) 160 | } : nil 161 | 162 | ForEach(contentViewModel.filteredCategoryGroups, id: \.self) { category in 163 | Section(category.name) { 164 | ForEach( 165 | category.quantities.sorted(by: { 166 | contentViewModel.quantityMapping[$1]! > contentViewModel.quantityMapping[$0]! 167 | }), id: \.self 168 | ) { quant in 169 | Button(action: { 170 | withAnimation { 171 | contentViewModel.toggleTypeIdentifier(quant) 172 | } 173 | }) { 174 | HStack { 175 | Text(contentViewModel.quantityMapping[quant]!).foregroundStyle(Color.primary) 176 | Spacer() 177 | contentViewModel.selectedQuantityTypes.contains(quant) 178 | ? Image(systemName: "checkmark").foregroundColor(.blue) : nil 179 | } 180 | } 181 | } 182 | } 183 | } 184 | } 185 | 186 | } 187 | 188 | #Preview { 189 | ContentView() 190 | } 191 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Attribution-NonCommercial 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution-NonCommercial 4.0 International Public 58 | License 59 | 60 | By exercising the Licensed Rights (defined below), You accept and agree 61 | to be bound by the terms and conditions of this Creative Commons 62 | Attribution-NonCommercial 4.0 International Public License ("Public 63 | License"). To the extent this Public License may be interpreted as a 64 | contract, You are granted the Licensed Rights in consideration of Your 65 | acceptance of these terms and conditions, and the Licensor grants You 66 | such rights in consideration of benefits the Licensor receives from 67 | making the Licensed Material available under these terms and 68 | conditions. 69 | 70 | 71 | Section 1 -- Definitions. 72 | 73 | a. Adapted Material means material subject to Copyright and Similar 74 | Rights that is derived from or based upon the Licensed Material 75 | and in which the Licensed Material is translated, altered, 76 | arranged, transformed, or otherwise modified in a manner requiring 77 | permission under the Copyright and Similar Rights held by the 78 | Licensor. For purposes of this Public License, where the Licensed 79 | Material is a musical work, performance, or sound recording, 80 | Adapted Material is always produced where the Licensed Material is 81 | synched in timed relation with a moving image. 82 | 83 | b. Adapter's License means the license You apply to Your Copyright 84 | and Similar Rights in Your contributions to Adapted Material in 85 | accordance with the terms and conditions of this Public License. 86 | 87 | c. Copyright and Similar Rights means copyright and/or similar rights 88 | closely related to copyright including, without limitation, 89 | performance, broadcast, sound recording, and Sui Generis Database 90 | Rights, without regard to how the rights are labeled or 91 | categorized. For purposes of this Public License, the rights 92 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 93 | Rights. 94 | d. Effective Technological Measures means those measures that, in the 95 | absence of proper authority, may not be circumvented under laws 96 | fulfilling obligations under Article 11 of the WIPO Copyright 97 | Treaty adopted on December 20, 1996, and/or similar international 98 | agreements. 99 | 100 | e. Exceptions and Limitations means fair use, fair dealing, and/or 101 | any other exception or limitation to Copyright and Similar Rights 102 | that applies to Your use of the Licensed Material. 103 | 104 | f. Licensed Material means the artistic or literary work, database, 105 | or other material to which the Licensor applied this Public 106 | License. 107 | 108 | g. Licensed Rights means the rights granted to You subject to the 109 | terms and conditions of this Public License, which are limited to 110 | all Copyright and Similar Rights that apply to Your use of the 111 | Licensed Material and that the Licensor has authority to license. 112 | 113 | h. Licensor means the individual(s) or entity(ies) granting rights 114 | under this Public License. 115 | 116 | i. NonCommercial means not primarily intended for or directed towards 117 | commercial advantage or monetary compensation. For purposes of 118 | this Public License, the exchange of the Licensed Material for 119 | other material subject to Copyright and Similar Rights by digital 120 | file-sharing or similar means is NonCommercial provided there is 121 | no payment of monetary compensation in connection with the 122 | exchange. 123 | 124 | j. Share means to provide material to the public by any means or 125 | process that requires permission under the Licensed Rights, such 126 | as reproduction, public display, public performance, distribution, 127 | dissemination, communication, or importation, and to make material 128 | available to the public including in ways that members of the 129 | public may access the material from a place and at a time 130 | individually chosen by them. 131 | 132 | k. Sui Generis Database Rights means rights other than copyright 133 | resulting from Directive 96/9/EC of the European Parliament and of 134 | the Council of 11 March 1996 on the legal protection of databases, 135 | as amended and/or succeeded, as well as other essentially 136 | equivalent rights anywhere in the world. 137 | 138 | l. You means the individual or entity exercising the Licensed Rights 139 | under this Public License. Your has a corresponding meaning. 140 | 141 | 142 | Section 2 -- Scope. 143 | 144 | a. License grant. 145 | 146 | 1. Subject to the terms and conditions of this Public License, 147 | the Licensor hereby grants You a worldwide, royalty-free, 148 | non-sublicensable, non-exclusive, irrevocable license to 149 | exercise the Licensed Rights in the Licensed Material to: 150 | 151 | a. reproduce and Share the Licensed Material, in whole or 152 | in part, for NonCommercial purposes only; and 153 | 154 | b. produce, reproduce, and Share Adapted Material for 155 | NonCommercial purposes only. 156 | 157 | 2. Exceptions and Limitations. For the avoidance of doubt, where 158 | Exceptions and Limitations apply to Your use, this Public 159 | License does not apply, and You do not need to comply with 160 | its terms and conditions. 161 | 162 | 3. Term. The term of this Public License is specified in Section 163 | 6(a). 164 | 165 | 4. Media and formats; technical modifications allowed. The 166 | Licensor authorizes You to exercise the Licensed Rights in 167 | all media and formats whether now known or hereafter created, 168 | and to make technical modifications necessary to do so. The 169 | Licensor waives and/or agrees not to assert any right or 170 | authority to forbid You from making technical modifications 171 | necessary to exercise the Licensed Rights, including 172 | technical modifications necessary to circumvent Effective 173 | Technological Measures. For purposes of this Public License, 174 | simply making modifications authorized by this Section 2(a) 175 | (4) never produces Adapted Material. 176 | 177 | 5. Downstream recipients. 178 | 179 | a. Offer from the Licensor -- Licensed Material. Every 180 | recipient of the Licensed Material automatically 181 | receives an offer from the Licensor to exercise the 182 | Licensed Rights under the terms and conditions of this 183 | Public License. 184 | 185 | b. No downstream restrictions. You may not offer or impose 186 | any additional or different terms or conditions on, or 187 | apply any Effective Technological Measures to, the 188 | Licensed Material if doing so restricts exercise of the 189 | Licensed Rights by any recipient of the Licensed 190 | Material. 191 | 192 | 6. No endorsement. Nothing in this Public License constitutes or 193 | may be construed as permission to assert or imply that You 194 | are, or that Your use of the Licensed Material is, connected 195 | with, or sponsored, endorsed, or granted official status by, 196 | the Licensor or others designated to receive attribution as 197 | provided in Section 3(a)(1)(A)(i). 198 | 199 | b. Other rights. 200 | 201 | 1. Moral rights, such as the right of integrity, are not 202 | licensed under this Public License, nor are publicity, 203 | privacy, and/or other similar personality rights; however, to 204 | the extent possible, the Licensor waives and/or agrees not to 205 | assert any such rights held by the Licensor to the limited 206 | extent necessary to allow You to exercise the Licensed 207 | Rights, but not otherwise. 208 | 209 | 2. Patent and trademark rights are not licensed under this 210 | Public License. 211 | 212 | 3. To the extent possible, the Licensor waives any right to 213 | collect royalties from You for the exercise of the Licensed 214 | Rights, whether directly or through a collecting society 215 | under any voluntary or waivable statutory or compulsory 216 | licensing scheme. In all other cases the Licensor expressly 217 | reserves any right to collect such royalties, including when 218 | the Licensed Material is used other than for NonCommercial 219 | purposes. 220 | 221 | 222 | Section 3 -- License Conditions. 223 | 224 | Your exercise of the Licensed Rights is expressly made subject to the 225 | following conditions. 226 | 227 | a. Attribution. 228 | 229 | 1. If You Share the Licensed Material (including in modified 230 | form), You must: 231 | 232 | a. retain the following if it is supplied by the Licensor 233 | with the Licensed Material: 234 | 235 | i. identification of the creator(s) of the Licensed 236 | Material and any others designated to receive 237 | attribution, in any reasonable manner requested by 238 | the Licensor (including by pseudonym if 239 | designated); 240 | 241 | ii. a copyright notice; 242 | 243 | iii. a notice that refers to this Public License; 244 | 245 | iv. a notice that refers to the disclaimer of 246 | warranties; 247 | 248 | v. a URI or hyperlink to the Licensed Material to the 249 | extent reasonably practicable; 250 | 251 | b. indicate if You modified the Licensed Material and 252 | retain an indication of any previous modifications; and 253 | 254 | c. indicate the Licensed Material is licensed under this 255 | Public License, and include the text of, or the URI or 256 | hyperlink to, this Public License. 257 | 258 | 2. You may satisfy the conditions in Section 3(a)(1) in any 259 | reasonable manner based on the medium, means, and context in 260 | which You Share the Licensed Material. For example, it may be 261 | reasonable to satisfy the conditions by providing a URI or 262 | hyperlink to a resource that includes the required 263 | information. 264 | 265 | 3. If requested by the Licensor, You must remove any of the 266 | information required by Section 3(a)(1)(A) to the extent 267 | reasonably practicable. 268 | 269 | 4. If You Share Adapted Material You produce, the Adapter's 270 | License You apply must not prevent recipients of the Adapted 271 | Material from complying with this Public License. 272 | 273 | 274 | Section 4 -- Sui Generis Database Rights. 275 | 276 | Where the Licensed Rights include Sui Generis Database Rights that 277 | apply to Your use of the Licensed Material: 278 | 279 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 280 | to extract, reuse, reproduce, and Share all or a substantial 281 | portion of the contents of the database for NonCommercial purposes 282 | only; 283 | 284 | b. if You include all or a substantial portion of the database 285 | contents in a database in which You have Sui Generis Database 286 | Rights, then the database in which You have Sui Generis Database 287 | Rights (but not its individual contents) is Adapted Material; and 288 | 289 | c. You must comply with the conditions in Section 3(a) if You Share 290 | all or a substantial portion of the contents of the database. 291 | 292 | For the avoidance of doubt, this Section 4 supplements and does not 293 | replace Your obligations under this Public License where the Licensed 294 | Rights include other Copyright and Similar Rights. 295 | 296 | 297 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 298 | 299 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 300 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 301 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 302 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 303 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 304 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 305 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 306 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 307 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 308 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 309 | 310 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 311 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 312 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 313 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 314 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 315 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 316 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 317 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 318 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 319 | 320 | c. The disclaimer of warranties and limitation of liability provided 321 | above shall be interpreted in a manner that, to the extent 322 | possible, most closely approximates an absolute disclaimer and 323 | waiver of all liability. 324 | 325 | 326 | Section 6 -- Term and Termination. 327 | 328 | a. This Public License applies for the term of the Copyright and 329 | Similar Rights licensed here. However, if You fail to comply with 330 | this Public License, then Your rights under this Public License 331 | terminate automatically. 332 | 333 | b. Where Your right to use the Licensed Material has terminated under 334 | Section 6(a), it reinstates: 335 | 336 | 1. automatically as of the date the violation is cured, provided 337 | it is cured within 30 days of Your discovery of the 338 | violation; or 339 | 340 | 2. upon express reinstatement by the Licensor. 341 | 342 | For the avoidance of doubt, this Section 6(b) does not affect any 343 | right the Licensor may have to seek remedies for Your violations 344 | of this Public License. 345 | 346 | c. For the avoidance of doubt, the Licensor may also offer the 347 | Licensed Material under separate terms or conditions or stop 348 | distributing the Licensed Material at any time; however, doing so 349 | will not terminate this Public License. 350 | 351 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 352 | License. 353 | 354 | 355 | Section 7 -- Other Terms and Conditions. 356 | 357 | a. The Licensor shall not be bound by any additional or different 358 | terms or conditions communicated by You unless expressly agreed. 359 | 360 | b. Any arrangements, understandings, or agreements regarding the 361 | Licensed Material not stated herein are separate from and 362 | independent of the terms and conditions of this Public License. 363 | 364 | 365 | Section 8 -- Interpretation. 366 | 367 | a. For the avoidance of doubt, this Public License does not, and 368 | shall not be interpreted to, reduce, limit, restrict, or impose 369 | conditions on any use of the Licensed Material that could lawfully 370 | be made without permission under this Public License. 371 | 372 | b. To the extent possible, if any provision of this Public License is 373 | deemed unenforceable, it shall be automatically reformed to the 374 | minimum extent necessary to make it enforceable. If the provision 375 | cannot be reformed, it shall be severed from this Public License 376 | without affecting the enforceability of the remaining terms and 377 | conditions. 378 | 379 | c. No term or condition of this Public License will be waived and no 380 | failure to comply consented to unless expressly agreed to by the 381 | Licensor. 382 | 383 | d. Nothing in this Public License constitutes or may be interpreted 384 | as a limitation upon, or waiver of, any privileges and immunities 385 | that apply to the Licensor or You, including from the legal 386 | processes of any jurisdiction or authority. 387 | 388 | ======================================================================= 389 | 390 | Creative Commons is not a party to its public 391 | licenses. Notwithstanding, Creative Commons may elect to apply one of 392 | its public licenses to material it publishes and in those instances 393 | will be considered the “Licensor.” The text of the Creative Commons 394 | public licenses is dedicated to the public domain under the CC0 Public 395 | Domain Dedication. Except for the limited purpose of indicating that 396 | material is shared under a Creative Commons public license or as 397 | otherwise permitted by the Creative Commons policies published at 398 | creativecommons.org/policies, Creative Commons does not authorize the 399 | use of the trademark "Creative Commons" or any other trademark or logo 400 | of Creative Commons without its prior written consent including, 401 | without limitation, in connection with any unauthorized modifications 402 | to any of its public licenses or any other arrangements, 403 | understandings, or agreements concerning use of licensed material. For 404 | the avoidance of doubt, this paragraph does not form part of the 405 | public licenses. 406 | 407 | Creative Commons may be contacted at creativecommons.org. -------------------------------------------------------------------------------- /HealthLens/ContentViewModel.swift: -------------------------------------------------------------------------------- 1 | import HealthKit 2 | import StoreKit 3 | import SwiftUI 4 | import libxlsxwriter 5 | 6 | let export_count = 2 //< How many unique exports must be asked for before a review 7 | let categories_exported = 10 //< Categories exported before a review is asked for 8 | let time_difference_large_enough: TimeInterval = 1 * 24 * 60 * 60 //< 1 Day in seconds 9 | let sample_cap = 50_000 //< Max number of samples to export 10 | 11 | /// Contains all of the data to store the necessary health records 12 | class ContentViewModel: ObservableObject { 13 | private let healthStore = HKHealthStore() 14 | typealias ExportContinuation = UnsafeContinuation 15 | 16 | @Published public var searchText: String = "" 17 | 18 | @AppStorage("exportFormat") public var selectedExportFormat: ExportFormat = .csv 19 | 20 | let header_datetime = String(localized: "Datetime") 21 | let header_category = String(localized: "Category") 22 | let header_unit = String(localized: "Unit") 23 | let header_value = String(localized: "Value") 24 | 25 | var xlsx_headers: [String] { 26 | return [ 27 | header_datetime, 28 | header_category, 29 | header_unit, 30 | header_value, 31 | ] 32 | } 33 | var csv_headers: [String] { 34 | return [ 35 | header_datetime, 36 | header_category, 37 | header_unit, 38 | header_value, 39 | ] 40 | } 41 | 42 | private let itemFormatter: DateFormatter = { 43 | let formatter = DateFormatter() 44 | 45 | formatter.dateStyle = .short 46 | formatter.timeStyle = .short 47 | formatter.locale = Locale.current 48 | 49 | return formatter 50 | }() 51 | 52 | private let numberFormatter: NumberFormatter = { 53 | let formatter = NumberFormatter() 54 | 55 | formatter.numberStyle = .decimal 56 | formatter.maximumFractionDigits = 2 57 | formatter.minimumFractionDigits = 2 58 | formatter.locale = Locale.current 59 | 60 | return formatter 61 | }() 62 | 63 | // -MARK: Stateful representations of the input form 64 | public var xlsxShareTarget: XLSXExportFile 65 | public var csvShareTarget: CSVExportFile 66 | 67 | @Environment(\.requestReview) var requestReview 68 | @AppStorage("timesExported") public var timesExported = 0 69 | @AppStorage("categoriesExported") public var categoriesExported = 0 70 | @AppStorage("lastRequested") public var lastRequested = "0.0.0" 71 | 72 | // Date range state 73 | @Published public var dateSelectorEnabled = false 74 | @Published public var startDate = Date() 75 | @Published public var endDate = Date() 76 | public var currentDate = Date() // date is simply set to the current date without any changes 77 | 78 | public func suggestedFileName() -> String { 79 | if selectedQuantityTypes.count == total_exports { 80 | return "all-health-data" 81 | } 82 | 83 | let result = 84 | selectedQuantityTypes 85 | .map { quantityMapping[$0]! } 86 | .sorted() 87 | .joined(separator: "-") 88 | .replacingOccurrences(of: " ", with: ".").lowercased() 89 | 90 | return "\(result)" 91 | } 92 | 93 | public init() { 94 | // Setting up the two export files 95 | xlsxShareTarget = XLSXExportFile() 96 | csvShareTarget = CSVExportFile() 97 | 98 | xlsxShareTarget.collectData = asyncExportHealthData 99 | csvShareTarget.collectData = asyncExportHealthData 100 | xlsxShareTarget.fileName = suggestedFileName 101 | csvShareTarget.fileName = suggestedFileName 102 | 103 | // converting HKQuantityTypeIdentifier and HKCategoryTypeIdentifier to HKQuantityType and HKCategoryType 104 | quantityMapping.keys.forEach({ 105 | if let obj = HKObjectType.quantityType(forIdentifier: $0) { 106 | quantityTypes.append(obj) 107 | } 108 | }) 109 | 110 | categoryMapping.keys.forEach({ 111 | if let obj = HKObjectType.categoryType(forIdentifier: $0) { 112 | categoryTypes.append(obj) 113 | } 114 | }) 115 | } 116 | 117 | // -MARK: Health Kit Constants 118 | let categoryGroups = [ 119 | bodyMeasurementsGroup, 120 | // fitnessGroup, 121 | // hearingHealthGroup, 122 | heartGroup, 123 | // mobilityGroup, 124 | respiratoryGroup, 125 | vitalSignsGroup, 126 | 127 | otherGroup, 128 | 129 | // reproductiveHealthGroup, 130 | // sleepGroup, 131 | // symptomsGroup, 132 | 133 | // TODO: Add a way to export this commented out categories 134 | // reproductiveHealthGroup, 135 | // sleepGroup, 136 | // symptomsGroup, 137 | // nutritionGroup, 138 | ] 139 | var filteredCategoryGroups: [CategoryGroup] { 140 | // return everything if empty 141 | guard !searchText.isEmpty else { 142 | return categoryGroups 143 | } 144 | 145 | let lowercasedSearch = searchText.lowercased() 146 | 147 | // Filter 148 | let filteredGroups = categoryGroups.map { group in 149 | let filteredQuantities = group.quantities.filter { quantityIdentifier in 150 | if let name = quantityMapping[quantityIdentifier] { 151 | return name.lowercased().contains(lowercasedSearch) 152 | } 153 | return false 154 | } 155 | 156 | return CategoryGroup(name: group.name, quantities: filteredQuantities, categories: []) 157 | } 158 | .filter { !$0.quantities.isEmpty } // need at least one match 159 | 160 | return filteredGroups 161 | } 162 | 163 | var total_exports: Int { 164 | categoryGroups.map { $0.quantities.count + $0.categories.count }.reduce(0, +) 165 | } 166 | 167 | let fallbackUnits: [HKUnit] = [ 168 | .gram(), .ounce(), .pound(), .stone(), 169 | .meter(), .inch(), .foot(), .mile(), 170 | .liter(), .fluidOunceUS(), .fluidOunceImperial(), .pintUS(), .pintImperial(), 171 | .second(), .minute(), .hour(), .day(), 172 | .joule(), .kilocalorie(), 173 | .degreeCelsius(), .degreeFahrenheit(), .kelvin(), 174 | .siemen(), 175 | .hertz(), 176 | .volt(), 177 | .watt(), 178 | .radianAngle(), .degreeAngle(), 179 | .lux(), 180 | ] 181 | 182 | public var quantityTypes: [HKQuantityType] = [] 183 | public var categoryTypes: [HKCategoryType] = [] 184 | 185 | // healthkit selected types 186 | @AppStorage("selectedQuantityTypes") public var selectedQuantityTypes: 187 | Set = [] 188 | 189 | // -MARK: User Interactions 190 | 191 | /// Selects the `HKQuantityTypeIdentifier` 192 | func toggleTypeIdentifier(_ identifier: HKQuantityTypeIdentifier) { 193 | if selectedQuantityTypes.contains(identifier) { 194 | selectedQuantityTypes.remove(identifier) 195 | } else { 196 | selectedQuantityTypes.insert(identifier) 197 | } 198 | } 199 | 200 | /// Clears the export queue 201 | func clearExportQueue() { 202 | selectedQuantityTypes.removeAll() 203 | } 204 | 205 | // -MARK: Intents 206 | 207 | /// Allows a date range to selected 208 | func dateSelectClicked() { 209 | logger.debug("Clicked the date select") 210 | dateSelectorEnabled.toggle() 211 | } 212 | 213 | /// Exports health data in an async function which can be exported to the transferable object w/ proper await support 214 | func asyncExportHealthData() async -> URL { 215 | // analytics logging 216 | Task.detached { 217 | await MainActor.run { [weak self] in 218 | if let self = self { self.logExportOccurred() } 219 | } 220 | } 221 | 222 | return await withUnsafeContinuation { continuation in 223 | exportHealthData(continuation: continuation) 224 | } 225 | } 226 | 227 | /// Exports health data to the share sheet 228 | func exportHealthData(continuation: ExportContinuation) { 229 | // Converts the selected quantity types 230 | let generatedQuantityTypes: Set = Set( 231 | selectedQuantityTypes.map({ 232 | HKObjectType.quantityType(forIdentifier: $0)! 233 | })) 234 | 235 | if !isAuthorizedForTypes(generatedQuantityTypes) { 236 | healthStore.requestAuthorization(toShare: nil, read: generatedQuantityTypes) { 237 | (success, error) in 238 | guard success else { 239 | logger.error("Failed w/ error \(error)") 240 | return 241 | } 242 | 243 | // queries data from HealthKit 244 | self.makeAuthorizedQueryToHealthKit(continuation) 245 | } 246 | } else { 247 | // queries data from HealthKit 248 | makeAuthorizedQueryToHealthKit(continuation) 249 | } 250 | } 251 | 252 | /// Check if we need authorization for a given set of object types 253 | func isAuthorizedForTypes(_ generatedQuantityTypes: Set) -> Bool { 254 | var isAuthorized = true 255 | for quantityType in generatedQuantityTypes { 256 | switch healthStore.authorizationStatus(for: quantityType) { 257 | case .notDetermined, .sharingDenied: 258 | isAuthorized = false 259 | break 260 | default: 261 | continue 262 | } 263 | } 264 | 265 | return isAuthorized 266 | } 267 | 268 | /// Makes a query to `HealthKit` 269 | func makeAuthorizedQueryToHealthKit(_ continuation: ExportContinuation) { 270 | // Ensure we have authorization to read health data 271 | guard HKHealthStore.isHealthDataAvailable() else { 272 | logger.error("Health data is not available") 273 | return 274 | } 275 | 276 | // we have authorization for exporting health data, we need to do it 277 | let generatedQuantityTypes: Set = Set( 278 | selectedQuantityTypes.map({ 279 | HKQuantityType.quantityType(forIdentifier: $0)! 280 | })) 281 | 282 | // getting the preferred units 283 | healthStore.preferredUnits(for: generatedQuantityTypes) { (mapping, error) in 284 | if let error = error { 285 | logger.error("Failed to generate the preferred unit types \(error)") 286 | } 287 | 288 | self.fetchDataForCompletion( 289 | continuation: continuation, generatedQuantityTypes: generatedQuantityTypes, 290 | unitsMapping: mapping) 291 | } 292 | } 293 | 294 | /// Gets the data for each type 295 | func fetchDataForCompletion( 296 | continuation: ExportContinuation, generatedQuantityTypes: Set, 297 | unitsMapping: [HKObjectType: HKUnit] 298 | ) { 299 | let dispatchGroup = DispatchGroup() 300 | 301 | var resultsDictionary: [HKObjectType: [HKSample]] = [:] 302 | 303 | for quantityType in generatedQuantityTypes { 304 | // fetching in a dispatch group 305 | dispatchGroup.enter() 306 | 307 | // calling the function 308 | let query = HKSampleQuery( 309 | sampleType: quantityType, predicate: make_date_range_predicate(), limit: sample_cap, 310 | sortDescriptors: nil 311 | ) { query, sample, error in 312 | if let error = error { 313 | logger.error("Failed to fetch data with error \(error)") 314 | dispatchGroup.leave() 315 | return 316 | } 317 | 318 | if let sample = sample { 319 | resultsDictionary[quantityType] = sample 320 | } 321 | 322 | dispatchGroup.leave() 323 | } 324 | 325 | healthStore.execute(query) 326 | } 327 | 328 | dispatchGroup.notify(queue: .main) { 329 | switch self.selectedExportFormat { 330 | case .csv: 331 | self.exportCSVData( 332 | resultsDictionary, continuation: continuation, unitsMapping: unitsMapping) 333 | case .xlsx: 334 | self.exportELSXData( 335 | resultsDictionary, continuation: continuation, unitsMapping: unitsMapping) 336 | } 337 | } 338 | } 339 | 340 | /// Cleans up a csv string for sanitization 341 | func sanitizeForCSV(_ input: String) -> String { 342 | var sanitized = input 343 | 344 | // Escape double quotes by replacing `"` with `""` 345 | sanitized = sanitized.replacingOccurrences(of: "\"", with: "\"\"") 346 | 347 | // If the string contains a comma, newline, or double quote, wrap it in quotes 348 | if sanitized.contains(",") || sanitized.contains("\n") || sanitized.contains("\"") { 349 | sanitized = "\"\(sanitized)\"" 350 | } 351 | 352 | return sanitized 353 | } 354 | 355 | /// Turns the results into a CSV list 356 | func exportCSVData( 357 | _ resultsDict: [HKObjectType: [HKSample]], continuation: ExportContinuation, 358 | unitsMapping: [HKObjectType: HKUnit] 359 | ) { 360 | let uuid = UUID().uuidString 361 | let fileName = "HealthData\(uuid).csv" 362 | let fileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName) 363 | 364 | var returnString = "\(header_datetime),\(header_category),\(header_unit),\(header_value)\n" 365 | 366 | for quantityType in resultsDict.keys { 367 | let quantity_type_id = HKQuantityTypeIdentifier(rawValue: quantityType.identifier) 368 | let quantity_type_string = sanitizeForCSV( 369 | quantityMapping[quantity_type_id] ?? String(localized: "Unknown")) 370 | 371 | for entry in resultsDict[quantityType] ?? [] { 372 | let newEntry = entry as! HKQuantitySample 373 | 374 | var startDate = itemFormatter.string(from: entry.startDate) 375 | startDate = sanitizeForCSV(startDate) 376 | 377 | guard 378 | let unit = unitsMapping[quantityType] 379 | ?? fallbackUnits.first(where: { 380 | newEntry.quantityType.is(compatibleWith: $0) 381 | }) 382 | else { 383 | logger.debug( 384 | "No compatible unit found, skipping entry for quantity type: \(quantityType.identifier)" 385 | ) 386 | continue 387 | } 388 | 389 | let value_raw = newEntry.quantity.doubleValue(for: unit) 390 | var value = numberFormatter.string(from: value_raw as NSNumber) ?? String(value_raw) 391 | 392 | value = sanitizeForCSV(value) 393 | 394 | returnString += "\(startDate),\(quantity_type_string),\(unit.unitString),\(value)\n" 395 | } 396 | } 397 | 398 | do { 399 | try returnString.write(to: fileURL, atomically: true, encoding: .utf8) 400 | // Resume continuation with the file URL 401 | continuation.resume(returning: fileURL) 402 | } catch { 403 | logger.error("Failed to write CSV data to file: \(error)") 404 | // Resume continuation with a failure 405 | // continuation.resume(throwing: error) 406 | } 407 | } 408 | 409 | func exportELSXData( 410 | _ resultsDict: [HKObjectType: [HKSample]], continuation: ExportContinuation, 411 | unitsMapping: [HKObjectType: HKUnit] 412 | ) { 413 | let uuid = UUID().uuidString 414 | let fileName = "HealthData\(uuid).xlsx" 415 | 416 | // Make a fileName be random here a uuid 417 | let filePath = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName) 418 | .path 419 | 420 | guard let workbook = workbook_new(filePath) else { 421 | logger.error("Failed to create XLSX workbook at path: \(filePath)") 422 | // continuation.resume(returning: Data()) 423 | return 424 | } 425 | 426 | guard let worksheet = workbook_add_worksheet(workbook, String(localized: "Data")) else { 427 | logger.error("Failed to create XLSX worksheet.") 428 | workbook_close(workbook) 429 | // continuation.resume(returning: Data()) 430 | return 431 | } 432 | 433 | // MARK: Formats 434 | let header_format = workbook_add_format(workbook) 435 | format_set_bold(header_format) 436 | format_set_align(header_format, UInt8(LXW_ALIGN_CENTER.rawValue)) 437 | 438 | let datetime_format = workbook_add_format(workbook) 439 | if let date_format_string = itemFormatter.dateFormat { 440 | format_set_num_format(datetime_format, date_format_string) 441 | } else { 442 | logger.error("Failed to get date format string from localized date") 443 | format_set_num_format(datetime_format, "yyyy-MM-dd HH:mm:ss") 444 | } 445 | 446 | let number_format = workbook_add_format(workbook) 447 | if let number_format_string = numberFormatter.positiveFormat { 448 | format_set_num_format(number_format, number_format_string) 449 | } else { 450 | format_set_num_format(number_format, "#,##0.00") 451 | } 452 | 453 | // Growing column width 454 | worksheet_set_column_pixels(worksheet, 0, 0, 120, nil) 455 | worksheet_set_column_pixels(worksheet, 1, 1, 80, nil) 456 | worksheet_set_column_pixels(worksheet, 2, 2, 40, nil) 457 | worksheet_set_column_pixels(worksheet, 3, 3, 80, nil) 458 | 459 | for (colIndex, header) in xlsx_headers.enumerated() { 460 | worksheet_write_string(worksheet, 0, lxw_col_t(colIndex), header, header_format) 461 | } 462 | 463 | var currentRow: lxw_row_t = 1 464 | 465 | for (quantityType, samples) in resultsDict { 466 | let preferredUnit = unitsMapping[quantityType] 467 | let quantity_type_id = HKQuantityTypeIdentifier(rawValue: quantityType.identifier) 468 | let quantity_type_string = quantityMapping[quantity_type_id] ?? String(localized: "Unknown") 469 | 470 | for sample in samples { 471 | guard let quantitySample = sample as? HKQuantitySample else { continue } 472 | 473 | // Get the right unit to use per type 474 | let unitToUse: HKUnit? = 475 | preferredUnit 476 | ?? fallbackUnits.first { 477 | quantitySample.quantityType.is(compatibleWith: $0) 478 | } 479 | 480 | guard let finalUnit = unitToUse else { 481 | logger.debug( 482 | "No compatible unit found, skipping entry for type: \(quantityType.identifier)") 483 | continue 484 | } 485 | 486 | // Get the unit's numeric value 487 | let value = quantitySample.quantity.doubleValue(for: finalUnit) 488 | 489 | worksheet_write_unixtime( 490 | worksheet, currentRow, 0, Int64(quantitySample.startDate.timeIntervalSince1970), 491 | datetime_format) 492 | worksheet_write_string(worksheet, currentRow, 1, quantity_type_string, nil) 493 | worksheet_write_string(worksheet, currentRow, 2, finalUnit.unitString, nil) 494 | worksheet_write_number(worksheet, currentRow, 3, value, number_format) 495 | 496 | currentRow += 1 497 | } 498 | } 499 | 500 | // Finalizes the file by closing the workbook 501 | workbook_close(workbook) 502 | 503 | let fileURL = URL(fileURLWithPath: filePath) 504 | 505 | continuation.resume(returning: fileURL) 506 | } 507 | 508 | /// Makes a comma separated list of selectedQuantityTypes 509 | public func makeSelectedStringDescription() -> String { 510 | return selectedQuantityTypes.map({ quantityMapping[$0]! }).sorted().joined(separator: ", ") 511 | } 512 | 513 | /// Makes a predicate only if the range is large enough 514 | public func make_date_range_predicate() -> NSPredicate? { 515 | if abs(endDate.timeIntervalSince(startDate)) <= time_difference_large_enough { 516 | return nil 517 | } 518 | 519 | return HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: []) 520 | } 521 | 522 | /// Analytics reporting which may ask for a review if numbers are high enough 523 | @MainActor func logExportOccurred() { 524 | timesExported += 1 525 | categoriesExported += selectedQuantityTypes.count 526 | 527 | // decision point on whether or not to ask for a review 528 | if timesExported >= export_count || categoriesExported >= categories_exported, 529 | let bundle = Bundle.main.bundleIdentifier, 530 | lastRequested != bundle 531 | { 532 | lastRequested = bundle 533 | 534 | DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in 535 | if let self = self { 536 | self.requestReview() 537 | } 538 | } 539 | } 540 | } 541 | 542 | let quantityMapping: [HKQuantityTypeIdentifier: String] = [ 543 | // Body Measurements 544 | .bodyMass: "Weight", 545 | .bodyMassIndex: "Body Mass Index (BMI)", 546 | .leanBodyMass: "Lean Body Mass", 547 | .height: "Height", 548 | .waistCircumference: "Waist Circumference", 549 | .bodyFatPercentage: "Body Fat Percentage", 550 | .electrodermalActivity: "Electrodermal Activity", 551 | 552 | // Fitness 553 | .activeEnergyBurned: "Active Energy Burned", 554 | .appleExerciseTime: "Exercise Time", 555 | .appleMoveTime: "Move Time", 556 | .appleStandTime: "Stand Time", 557 | .basalEnergyBurned: "Basal Energy Burned", 558 | .cyclingCadence: "Cycling Cadence", 559 | .cyclingFunctionalThresholdPower: "Cycling Functional Threshold Power", 560 | .cyclingPower: "Cycling Power", 561 | .cyclingSpeed: "Cycling Speed", 562 | .distanceCycling: "Distance Cycling", 563 | .distanceDownhillSnowSports: "Distance Downhill Snow Sports", 564 | .distanceSwimming: "Distance Swimming", 565 | .distanceWalkingRunning: "Distance Walking/Running", 566 | .distanceWheelchair: "Distance Wheelchair", 567 | .flightsClimbed: "Flights Climbed", 568 | .nikeFuel: "Nike Fuel", 569 | .physicalEffort: "Physical Effort", 570 | .pushCount: "Push Count", 571 | .runningPower: "Running Power", 572 | .runningSpeed: "Running Speed", 573 | .stepCount: "Step Count", 574 | .swimmingStrokeCount: "Swimming Stroke Count", 575 | .underwaterDepth: "Underwater Depth", 576 | 577 | // Hearing Health 578 | .environmentalAudioExposure: "Environmental Audio Exposure", 579 | .environmentalSoundReduction: "Environmental Sound Reduction", 580 | .headphoneAudioExposure: "Headphone Audio Exposure", 581 | 582 | // Heart 583 | .atrialFibrillationBurden: "Atrial Fibrillation Burden", 584 | .heartRate: "Heart Rate", 585 | .heartRateRecoveryOneMinute: "Heart Rate Recovery (One Minute)", 586 | .heartRateVariabilitySDNN: "Heart Rate Variability (SDNN)", 587 | .peripheralPerfusionIndex: "Peripheral Perfusion Index", 588 | .restingHeartRate: "Resting Heart Rate", 589 | .vo2Max: "VO2 Max", 590 | .walkingHeartRateAverage: "Walking Heart Rate Average", 591 | 592 | // Mobility 593 | .appleWalkingSteadiness: "Walking Steadiness", 594 | .runningGroundContactTime: "Running Ground Contact Time", 595 | .runningStrideLength: "Running Stride Length", 596 | .runningVerticalOscillation: "Running Vertical Oscillation", 597 | .sixMinuteWalkTestDistance: "Six-Minute Walk Test Distance", 598 | .stairAscentSpeed: "Stair Ascent Speed", 599 | .stairDescentSpeed: "Stair Descent Speed", 600 | .walkingAsymmetryPercentage: "Walking Asymmetry Percentage", 601 | .walkingDoubleSupportPercentage: "Walking Double Support Percentage", 602 | .walkingSpeed: "Walking Speed", 603 | .walkingStepLength: "Walking Step Length", 604 | 605 | // Nutrition 606 | .dietaryBiotin: "Dietary Biotin", 607 | .dietaryCaffeine: "Dietary Caffeine", 608 | .dietaryCalcium: "Dietary Calcium", 609 | .dietaryCarbohydrates: "Dietary Carbohydrates", 610 | .dietaryChloride: "Dietary Chloride", 611 | .dietaryCholesterol: "Dietary Cholesterol", 612 | .dietaryChromium: "Dietary Chromium", 613 | .dietaryCopper: "Dietary Copper", 614 | .dietaryEnergyConsumed: "Dietary Energy Consumed", 615 | .dietaryFatMonounsaturated: "Dietary Fat (Monounsaturated)", 616 | .dietaryFatPolyunsaturated: "Dietary Fat (Polyunsaturated)", 617 | .dietaryFatSaturated: "Dietary Fat (Saturated)", 618 | .dietaryFatTotal: "Dietary Fat (Total)", 619 | .dietaryFiber: "Dietary Fiber", 620 | .dietaryFolate: "Dietary Folate", 621 | .dietaryIodine: "Dietary Iodine", 622 | .dietaryIron: "Dietary Iron", 623 | .dietaryMagnesium: "Dietary Magnesium", 624 | .dietaryManganese: "Dietary Manganese", 625 | .dietaryMolybdenum: "Dietary Molybdenum", 626 | .dietaryNiacin: "Dietary Niacin", 627 | .dietaryPantothenicAcid: "Dietary Pantothenic Acid", 628 | .dietaryPhosphorus: "Dietary Phosphorus", 629 | .dietaryPotassium: "Dietary Potassium", 630 | .dietaryProtein: "Dietary Protein", 631 | .dietaryRiboflavin: "Dietary Riboflavin", 632 | .dietarySelenium: "Dietary Selenium", 633 | .dietarySodium: "Dietary Sodium", 634 | .dietarySugar: "Dietary Sugar", 635 | .dietaryThiamin: "Dietary Thiamin", 636 | .dietaryVitaminA: "Dietary Vitamin A", 637 | .dietaryVitaminB12: "Dietary Vitamin B12", 638 | .dietaryVitaminB6: "Dietary Vitamin B6", 639 | .dietaryVitaminC: "Dietary Vitamin C", 640 | .dietaryVitaminD: "Dietary Vitamin D", 641 | .dietaryVitaminE: "Dietary Vitamin E", 642 | .dietaryVitaminK: "Dietary Vitamin K", 643 | .dietaryWater: "Dietary Water", 644 | .dietaryZinc: "Dietary Zinc", 645 | 646 | // Other 647 | .bloodAlcoholContent: "Blood Alcohol Content", 648 | .bloodPressureDiastolic: "Blood Pressure (Diastolic)", 649 | .bloodPressureSystolic: "Blood Pressure (Systolic)", 650 | .insulinDelivery: "Insulin Delivery", 651 | .numberOfAlcoholicBeverages: "Number of Alcoholic Beverages", 652 | .numberOfTimesFallen: "Number of Times Fallen", 653 | .timeInDaylight: "Time in Daylight", 654 | .uvExposure: "UV Exposure", 655 | .waterTemperature: "Water Temperature", 656 | 657 | // Reproductive Health 658 | .basalBodyTemperature: "Basal Body Temperature", 659 | 660 | // Respiratory 661 | .forcedExpiratoryVolume1: "Forced Expiratory Volume (1 second)", 662 | .forcedVitalCapacity: "Forced Vital Capacity", 663 | .inhalerUsage: "Inhaler Usage", 664 | .oxygenSaturation: "Oxygen Saturation", 665 | .peakExpiratoryFlowRate: "Peak Expiratory Flow Rate", 666 | .respiratoryRate: "Respiratory Rate", 667 | 668 | // Vital Signs 669 | .bloodGlucose: "Blood Glucose", 670 | .bodyTemperature: "Body Temperature", 671 | 672 | // Other recent identifiers 673 | .appleSleepingWristTemperature: "Sleeping Wrist Temperature", 674 | ] 675 | 676 | let categoryMapping: [HKCategoryTypeIdentifier: String] = [ 677 | // Stand Hour 678 | .appleStandHour: "Stand Hour", 679 | 680 | // Hearing Health 681 | .environmentalAudioExposureEvent: "Environmental Audio Exposure Event", 682 | .headphoneAudioExposureEvent: "Headphone Audio Exposure Event", 683 | 684 | // Heart 685 | .highHeartRateEvent: "High Heart Rate Event", 686 | .irregularHeartRhythmEvent: "Irregular Heart Rhythm Event", 687 | .lowCardioFitnessEvent: "Low Cardio Fitness Event", 688 | .lowHeartRateEvent: "Low Heart Rate Event", 689 | 690 | // Mindfulness 691 | .mindfulSession: "Mindful Session", 692 | 693 | // Mobility 694 | .appleWalkingSteadinessEvent: "Walking Steadiness Event", 695 | 696 | // Other 697 | .handwashingEvent: "Handwashing Event", 698 | .toothbrushingEvent: "Toothbrushing Event", 699 | 700 | // Reproductive Health 701 | .cervicalMucusQuality: "Cervical Mucus Quality", 702 | .contraceptive: "Contraceptive", 703 | .infrequentMenstrualCycles: "Infrequent Menstrual Cycles", 704 | .intermenstrualBleeding: "Intermenstrual Bleeding", 705 | .irregularMenstrualCycles: "Irregular Menstrual Cycles", 706 | .lactation: "Lactation", 707 | .menstrualFlow: "Menstrual Flow", 708 | .ovulationTestResult: "Ovulation Test Result", 709 | .persistentIntermenstrualBleeding: "Persistent Intermenstrual Bleeding", 710 | .pregnancy: "Pregnancy", 711 | .pregnancyTestResult: "Pregnancy Test Result", 712 | .progesteroneTestResult: "Progesterone Test Result", 713 | .prolongedMenstrualPeriods: "Prolonged Menstrual Periods", 714 | .sexualActivity: "Sexual Activity", 715 | 716 | // Sleep 717 | .sleepAnalysis: "Sleep Analysis", 718 | 719 | // Symptoms 720 | .abdominalCramps: "Abdominal Cramps", 721 | .acne: "Acne", 722 | .appetiteChanges: "Appetite Changes", 723 | .bladderIncontinence: "Bladder Incontinence", 724 | .bloating: "Bloating", 725 | .breastPain: "Breast Pain", 726 | .chestTightnessOrPain: "Chest Tightness or Pain", 727 | .chills: "Chills", 728 | .constipation: "Constipation", 729 | .coughing: "Coughing", 730 | .diarrhea: "Diarrhea", 731 | .dizziness: "Dizziness", 732 | .drySkin: "Dry Skin", 733 | .fainting: "Fainting", 734 | .fatigue: "Fatigue", 735 | .fever: "Fever", 736 | .generalizedBodyAche: "Generalized Body Ache", 737 | .hairLoss: "Hair Loss", 738 | .headache: "Headache", 739 | .heartburn: "Heartburn", 740 | .hotFlashes: "Hot Flashes", 741 | .lossOfSmell: "Loss of Smell", 742 | .lossOfTaste: "Loss of Taste", 743 | .lowerBackPain: "Lower Back Pain", 744 | .memoryLapse: "Memory Lapse", 745 | .moodChanges: "Mood Changes", 746 | .nausea: "Nausea", 747 | .nightSweats: "Night Sweats", 748 | .pelvicPain: "Pelvic Pain", 749 | .rapidPoundingOrFlutteringHeartbeat: "Rapid Pounding or Fluttering Heartbeat", 750 | .runnyNose: "Runny Nose", 751 | .shortnessOfBreath: "Shortness of Breath", 752 | .sinusCongestion: "Sinus Congestion", 753 | .skippedHeartbeat: "Skipped Heartbeat", 754 | .sleepChanges: "Sleep Changes", 755 | .soreThroat: "Sore Throat", 756 | .vaginalDryness: "Vaginal Dryness", 757 | .vomiting: "Vomiting", 758 | .wheezing: "Wheezing", 759 | ] 760 | } 761 | 762 | // MARK: Codable - RawRepresentable Set extensions to use @AppStorage with selectedQuantityTypes 763 | extension HKQuantityTypeIdentifier: Codable { 764 | public init(from decoder: Decoder) throws { 765 | let container = try decoder.singleValueContainer() 766 | let rawValue = try container.decode(String.self) 767 | // Attempt to rebuild the identifier from its string rawValue 768 | self = HKQuantityTypeIdentifier(rawValue: rawValue) 769 | } 770 | 771 | public func encode(to encoder: Encoder) throws { 772 | var container = encoder.singleValueContainer() 773 | try container.encode(self.rawValue) 774 | } 775 | } 776 | 777 | extension Set: @retroactive RawRepresentable where Element: Codable { 778 | public init?(rawValue: String) { 779 | guard let data = rawValue.data(using: .utf8), 780 | let result = try? JSONDecoder().decode(Set.self, from: data) 781 | else { 782 | return nil 783 | } 784 | self = result 785 | } 786 | 787 | public var rawValue: String { 788 | guard let data = try? JSONEncoder().encode(self), 789 | let result = String(data: data, encoding: .utf8) 790 | else { 791 | // Fallback for encoding failure. 792 | return "[]" 793 | } 794 | return result 795 | } 796 | } 797 | -------------------------------------------------------------------------------- /HealthLens.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3E1EAE4D2D38155B00090EC5 /* Groups.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1EAE4C2D38155600090EC5 /* Groups.swift */; }; 11 | 3E1EAE532D381D1900090EC5 /* libxlsxwriter in Frameworks */ = {isa = PBXBuildFile; productRef = 3E1EAE522D381D1900090EC5 /* libxlsxwriter */; }; 12 | 3E98758E2C46E5A300FD1CA5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E98758D2C46E5A300FD1CA5 /* Logger.swift */; }; 13 | 3EA025C02C32FE7300EEC4C3 /* HealthLensApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA025BF2C32FE7300EEC4C3 /* HealthLensApp.swift */; }; 14 | 3EA025C22C32FE7300EEC4C3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA025C12C32FE7300EEC4C3 /* ContentView.swift */; }; 15 | 3EA025C62C32FE7500EEC4C3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3EA025C52C32FE7500EEC4C3 /* Assets.xcassets */; }; 16 | 3EA025C92C32FE7500EEC4C3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3EA025C82C32FE7500EEC4C3 /* Preview Assets.xcassets */; }; 17 | 3EA025D32C32FE7500EEC4C3 /* HealthLensTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA025D22C32FE7500EEC4C3 /* HealthLensTests.swift */; }; 18 | 3EA025DD2C32FE7500EEC4C3 /* HealthLensUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA025DC2C32FE7500EEC4C3 /* HealthLensUITests.swift */; }; 19 | 3EA025DF2C32FE7500EEC4C3 /* HealthLensUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA025DE2C32FE7500EEC4C3 /* HealthLensUITestsLaunchTests.swift */; }; 20 | 3EDD0C5C2C4EB94000471A4A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3EDD0C5B2C4EB94000471A4A /* LaunchScreen.storyboard */; }; 21 | 3EE9B02F2D4D827200EA7FC6 /* MarketingScreenShotView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EE9B02E2D4D826A00EA7FC6 /* MarketingScreenShotView.swift */; }; 22 | 3EF2608C2C448000001ABA12 /* HealthKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3EF2608B2C447FFF001ABA12 /* HealthKit.framework */; }; 23 | 3EF260902C457494001ABA12 /* ContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EF2608F2C457494001ABA12 /* ContentViewModel.swift */; }; 24 | 3EF9FD132C498499000F4341 /* ExportFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EF9FD122C498499000F4341 /* ExportFile.swift */; }; 25 | /* End PBXBuildFile section */ 26 | 27 | /* Begin PBXContainerItemProxy section */ 28 | 3EA025CF2C32FE7500EEC4C3 /* PBXContainerItemProxy */ = { 29 | isa = PBXContainerItemProxy; 30 | containerPortal = 3EA025B42C32FE7300EEC4C3 /* Project object */; 31 | proxyType = 1; 32 | remoteGlobalIDString = 3EA025BB2C32FE7300EEC4C3; 33 | remoteInfo = HealthLens; 34 | }; 35 | 3EA025D92C32FE7500EEC4C3 /* PBXContainerItemProxy */ = { 36 | isa = PBXContainerItemProxy; 37 | containerPortal = 3EA025B42C32FE7300EEC4C3 /* Project object */; 38 | proxyType = 1; 39 | remoteGlobalIDString = 3EA025BB2C32FE7300EEC4C3; 40 | remoteInfo = HealthLens; 41 | }; 42 | /* End PBXContainerItemProxy section */ 43 | 44 | /* Begin PBXFileReference section */ 45 | 3E1EAE4C2D38155600090EC5 /* Groups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Groups.swift; sourceTree = ""; }; 46 | 3E98758D2C46E5A300FD1CA5 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 47 | 3EA025BC2C32FE7300EEC4C3 /* HealthLens.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HealthLens.app; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | 3EA025BF2C32FE7300EEC4C3 /* HealthLensApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthLensApp.swift; sourceTree = ""; }; 49 | 3EA025C12C32FE7300EEC4C3 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 50 | 3EA025C52C32FE7500EEC4C3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 51 | 3EA025C82C32FE7500EEC4C3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 52 | 3EA025CE2C32FE7500EEC4C3 /* HealthLensTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HealthLensTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | 3EA025D22C32FE7500EEC4C3 /* HealthLensTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthLensTests.swift; sourceTree = ""; }; 54 | 3EA025D82C32FE7500EEC4C3 /* HealthLensUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HealthLensUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 55 | 3EA025DC2C32FE7500EEC4C3 /* HealthLensUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthLensUITests.swift; sourceTree = ""; }; 56 | 3EA025DE2C32FE7500EEC4C3 /* HealthLensUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthLensUITestsLaunchTests.swift; sourceTree = ""; }; 57 | 3EDD0C5B2C4EB94000471A4A /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 58 | 3EE9B02E2D4D826A00EA7FC6 /* MarketingScreenShotView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketingScreenShotView.swift; sourceTree = ""; }; 59 | 3EF2608B2C447FFF001ABA12 /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = System/Library/Frameworks/HealthKit.framework; sourceTree = SDKROOT; }; 60 | 3EF2608D2C448056001ABA12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 61 | 3EF2608E2C4481A6001ABA12 /* HealthLens.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HealthLens.entitlements; sourceTree = ""; }; 62 | 3EF2608F2C457494001ABA12 /* ContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewModel.swift; sourceTree = ""; }; 63 | 3EF9FD122C498499000F4341 /* ExportFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportFile.swift; sourceTree = ""; }; 64 | /* End PBXFileReference section */ 65 | 66 | /* Begin PBXFrameworksBuildPhase section */ 67 | 3EA025B92C32FE7300EEC4C3 /* Frameworks */ = { 68 | isa = PBXFrameworksBuildPhase; 69 | buildActionMask = 2147483647; 70 | files = ( 71 | 3E1EAE532D381D1900090EC5 /* libxlsxwriter in Frameworks */, 72 | 3EF2608C2C448000001ABA12 /* HealthKit.framework in Frameworks */, 73 | ); 74 | runOnlyForDeploymentPostprocessing = 0; 75 | }; 76 | 3EA025CB2C32FE7500EEC4C3 /* Frameworks */ = { 77 | isa = PBXFrameworksBuildPhase; 78 | buildActionMask = 2147483647; 79 | files = ( 80 | ); 81 | runOnlyForDeploymentPostprocessing = 0; 82 | }; 83 | 3EA025D52C32FE7500EEC4C3 /* Frameworks */ = { 84 | isa = PBXFrameworksBuildPhase; 85 | buildActionMask = 2147483647; 86 | files = ( 87 | ); 88 | runOnlyForDeploymentPostprocessing = 0; 89 | }; 90 | /* End PBXFrameworksBuildPhase section */ 91 | 92 | /* Begin PBXGroup section */ 93 | 3EA025B32C32FE7300EEC4C3 = { 94 | isa = PBXGroup; 95 | children = ( 96 | 3EA025BE2C32FE7300EEC4C3 /* HealthLens */, 97 | 3EA025D12C32FE7500EEC4C3 /* HealthLensTests */, 98 | 3EA025DB2C32FE7500EEC4C3 /* HealthLensUITests */, 99 | 3EA025BD2C32FE7300EEC4C3 /* Products */, 100 | 3EF2608A2C447FFF001ABA12 /* Frameworks */, 101 | ); 102 | sourceTree = ""; 103 | }; 104 | 3EA025BD2C32FE7300EEC4C3 /* Products */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | 3EA025BC2C32FE7300EEC4C3 /* HealthLens.app */, 108 | 3EA025CE2C32FE7500EEC4C3 /* HealthLensTests.xctest */, 109 | 3EA025D82C32FE7500EEC4C3 /* HealthLensUITests.xctest */, 110 | ); 111 | name = Products; 112 | sourceTree = ""; 113 | }; 114 | 3EA025BE2C32FE7300EEC4C3 /* HealthLens */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | 3E1EAE4C2D38155600090EC5 /* Groups.swift */, 118 | 3EF2608E2C4481A6001ABA12 /* HealthLens.entitlements */, 119 | 3EF2608D2C448056001ABA12 /* Info.plist */, 120 | 3EA025BF2C32FE7300EEC4C3 /* HealthLensApp.swift */, 121 | 3EA025C12C32FE7300EEC4C3 /* ContentView.swift */, 122 | 3EA025C52C32FE7500EEC4C3 /* Assets.xcassets */, 123 | 3EA025C72C32FE7500EEC4C3 /* Preview Content */, 124 | 3EF2608F2C457494001ABA12 /* ContentViewModel.swift */, 125 | 3E98758D2C46E5A300FD1CA5 /* Logger.swift */, 126 | 3EF9FD122C498499000F4341 /* ExportFile.swift */, 127 | 3EDD0C5B2C4EB94000471A4A /* LaunchScreen.storyboard */, 128 | ); 129 | path = HealthLens; 130 | sourceTree = ""; 131 | }; 132 | 3EA025C72C32FE7500EEC4C3 /* Preview Content */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 3EA025C82C32FE7500EEC4C3 /* Preview Assets.xcassets */, 136 | ); 137 | path = "Preview Content"; 138 | sourceTree = ""; 139 | }; 140 | 3EA025D12C32FE7500EEC4C3 /* HealthLensTests */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | 3EA025D22C32FE7500EEC4C3 /* HealthLensTests.swift */, 144 | ); 145 | path = HealthLensTests; 146 | sourceTree = ""; 147 | }; 148 | 3EA025DB2C32FE7500EEC4C3 /* HealthLensUITests */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | 3EE9B02E2D4D826A00EA7FC6 /* MarketingScreenShotView.swift */, 152 | 3EA025DC2C32FE7500EEC4C3 /* HealthLensUITests.swift */, 153 | 3EA025DE2C32FE7500EEC4C3 /* HealthLensUITestsLaunchTests.swift */, 154 | ); 155 | path = HealthLensUITests; 156 | sourceTree = ""; 157 | }; 158 | 3EF2608A2C447FFF001ABA12 /* Frameworks */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | 3EF2608B2C447FFF001ABA12 /* HealthKit.framework */, 162 | ); 163 | name = Frameworks; 164 | sourceTree = ""; 165 | }; 166 | /* End PBXGroup section */ 167 | 168 | /* Begin PBXNativeTarget section */ 169 | 3EA025BB2C32FE7300EEC4C3 /* HealthLens */ = { 170 | isa = PBXNativeTarget; 171 | buildConfigurationList = 3EA025E22C32FE7500EEC4C3 /* Build configuration list for PBXNativeTarget "HealthLens" */; 172 | buildPhases = ( 173 | 3EA025B82C32FE7300EEC4C3 /* Sources */, 174 | 3EA025B92C32FE7300EEC4C3 /* Frameworks */, 175 | 3EA025BA2C32FE7300EEC4C3 /* Resources */, 176 | ); 177 | buildRules = ( 178 | ); 179 | dependencies = ( 180 | 3E1EAE512D381CFA00090EC5 /* PBXTargetDependency */, 181 | ); 182 | name = HealthLens; 183 | productName = HealthLens; 184 | productReference = 3EA025BC2C32FE7300EEC4C3 /* HealthLens.app */; 185 | productType = "com.apple.product-type.application"; 186 | }; 187 | 3EA025CD2C32FE7500EEC4C3 /* HealthLensTests */ = { 188 | isa = PBXNativeTarget; 189 | buildConfigurationList = 3EA025E52C32FE7500EEC4C3 /* Build configuration list for PBXNativeTarget "HealthLensTests" */; 190 | buildPhases = ( 191 | 3EA025CA2C32FE7500EEC4C3 /* Sources */, 192 | 3EA025CB2C32FE7500EEC4C3 /* Frameworks */, 193 | 3EA025CC2C32FE7500EEC4C3 /* Resources */, 194 | ); 195 | buildRules = ( 196 | ); 197 | dependencies = ( 198 | 3EA025D02C32FE7500EEC4C3 /* PBXTargetDependency */, 199 | ); 200 | name = HealthLensTests; 201 | productName = HealthLensTests; 202 | productReference = 3EA025CE2C32FE7500EEC4C3 /* HealthLensTests.xctest */; 203 | productType = "com.apple.product-type.bundle.unit-test"; 204 | }; 205 | 3EA025D72C32FE7500EEC4C3 /* HealthLensUITests */ = { 206 | isa = PBXNativeTarget; 207 | buildConfigurationList = 3EA025E82C32FE7500EEC4C3 /* Build configuration list for PBXNativeTarget "HealthLensUITests" */; 208 | buildPhases = ( 209 | 3EA025D42C32FE7500EEC4C3 /* Sources */, 210 | 3EA025D52C32FE7500EEC4C3 /* Frameworks */, 211 | 3EA025D62C32FE7500EEC4C3 /* Resources */, 212 | ); 213 | buildRules = ( 214 | ); 215 | dependencies = ( 216 | 3EA025DA2C32FE7500EEC4C3 /* PBXTargetDependency */, 217 | ); 218 | name = HealthLensUITests; 219 | productName = HealthLensUITests; 220 | productReference = 3EA025D82C32FE7500EEC4C3 /* HealthLensUITests.xctest */; 221 | productType = "com.apple.product-type.bundle.ui-testing"; 222 | }; 223 | /* End PBXNativeTarget section */ 224 | 225 | /* Begin PBXProject section */ 226 | 3EA025B42C32FE7300EEC4C3 /* Project object */ = { 227 | isa = PBXProject; 228 | attributes = { 229 | BuildIndependentTargetsInParallel = 1; 230 | LastSwiftUpdateCheck = 1540; 231 | LastUpgradeCheck = 1540; 232 | TargetAttributes = { 233 | 3EA025BB2C32FE7300EEC4C3 = { 234 | CreatedOnToolsVersion = 15.4; 235 | }; 236 | 3EA025CD2C32FE7500EEC4C3 = { 237 | CreatedOnToolsVersion = 15.4; 238 | TestTargetID = 3EA025BB2C32FE7300EEC4C3; 239 | }; 240 | 3EA025D72C32FE7500EEC4C3 = { 241 | CreatedOnToolsVersion = 15.4; 242 | TestTargetID = 3EA025BB2C32FE7300EEC4C3; 243 | }; 244 | }; 245 | }; 246 | buildConfigurationList = 3EA025B72C32FE7300EEC4C3 /* Build configuration list for PBXProject "HealthLens" */; 247 | compatibilityVersion = "Xcode 14.0"; 248 | developmentRegion = en; 249 | hasScannedForEncodings = 0; 250 | knownRegions = ( 251 | en, 252 | Base, 253 | ); 254 | mainGroup = 3EA025B32C32FE7300EEC4C3; 255 | packageReferences = ( 256 | 3E1EAE4B2D38148200090EC5 /* XCRemoteSwiftPackageReference "libxlsxwriter" */, 257 | ); 258 | productRefGroup = 3EA025BD2C32FE7300EEC4C3 /* Products */; 259 | projectDirPath = ""; 260 | projectRoot = ""; 261 | targets = ( 262 | 3EA025BB2C32FE7300EEC4C3 /* HealthLens */, 263 | 3EA025CD2C32FE7500EEC4C3 /* HealthLensTests */, 264 | 3EA025D72C32FE7500EEC4C3 /* HealthLensUITests */, 265 | ); 266 | }; 267 | /* End PBXProject section */ 268 | 269 | /* Begin PBXResourcesBuildPhase section */ 270 | 3EA025BA2C32FE7300EEC4C3 /* Resources */ = { 271 | isa = PBXResourcesBuildPhase; 272 | buildActionMask = 2147483647; 273 | files = ( 274 | 3EDD0C5C2C4EB94000471A4A /* LaunchScreen.storyboard in Resources */, 275 | 3EA025C92C32FE7500EEC4C3 /* Preview Assets.xcassets in Resources */, 276 | 3EA025C62C32FE7500EEC4C3 /* Assets.xcassets in Resources */, 277 | ); 278 | runOnlyForDeploymentPostprocessing = 0; 279 | }; 280 | 3EA025CC2C32FE7500EEC4C3 /* Resources */ = { 281 | isa = PBXResourcesBuildPhase; 282 | buildActionMask = 2147483647; 283 | files = ( 284 | ); 285 | runOnlyForDeploymentPostprocessing = 0; 286 | }; 287 | 3EA025D62C32FE7500EEC4C3 /* Resources */ = { 288 | isa = PBXResourcesBuildPhase; 289 | buildActionMask = 2147483647; 290 | files = ( 291 | ); 292 | runOnlyForDeploymentPostprocessing = 0; 293 | }; 294 | /* End PBXResourcesBuildPhase section */ 295 | 296 | /* Begin PBXSourcesBuildPhase section */ 297 | 3EA025B82C32FE7300EEC4C3 /* Sources */ = { 298 | isa = PBXSourcesBuildPhase; 299 | buildActionMask = 2147483647; 300 | files = ( 301 | 3E98758E2C46E5A300FD1CA5 /* Logger.swift in Sources */, 302 | 3EF260902C457494001ABA12 /* ContentViewModel.swift in Sources */, 303 | 3EA025C22C32FE7300EEC4C3 /* ContentView.swift in Sources */, 304 | 3EF9FD132C498499000F4341 /* ExportFile.swift in Sources */, 305 | 3EA025C02C32FE7300EEC4C3 /* HealthLensApp.swift in Sources */, 306 | 3E1EAE4D2D38155B00090EC5 /* Groups.swift in Sources */, 307 | ); 308 | runOnlyForDeploymentPostprocessing = 0; 309 | }; 310 | 3EA025CA2C32FE7500EEC4C3 /* Sources */ = { 311 | isa = PBXSourcesBuildPhase; 312 | buildActionMask = 2147483647; 313 | files = ( 314 | 3EA025D32C32FE7500EEC4C3 /* HealthLensTests.swift in Sources */, 315 | ); 316 | runOnlyForDeploymentPostprocessing = 0; 317 | }; 318 | 3EA025D42C32FE7500EEC4C3 /* Sources */ = { 319 | isa = PBXSourcesBuildPhase; 320 | buildActionMask = 2147483647; 321 | files = ( 322 | 3EA025DF2C32FE7500EEC4C3 /* HealthLensUITestsLaunchTests.swift in Sources */, 323 | 3EE9B02F2D4D827200EA7FC6 /* MarketingScreenShotView.swift in Sources */, 324 | 3EA025DD2C32FE7500EEC4C3 /* HealthLensUITests.swift in Sources */, 325 | ); 326 | runOnlyForDeploymentPostprocessing = 0; 327 | }; 328 | /* End PBXSourcesBuildPhase section */ 329 | 330 | /* Begin PBXTargetDependency section */ 331 | 3E1EAE512D381CFA00090EC5 /* PBXTargetDependency */ = { 332 | isa = PBXTargetDependency; 333 | productRef = 3E1EAE502D381CFA00090EC5 /* libxlsxwriter */; 334 | }; 335 | 3EA025D02C32FE7500EEC4C3 /* PBXTargetDependency */ = { 336 | isa = PBXTargetDependency; 337 | target = 3EA025BB2C32FE7300EEC4C3 /* HealthLens */; 338 | targetProxy = 3EA025CF2C32FE7500EEC4C3 /* PBXContainerItemProxy */; 339 | }; 340 | 3EA025DA2C32FE7500EEC4C3 /* PBXTargetDependency */ = { 341 | isa = PBXTargetDependency; 342 | target = 3EA025BB2C32FE7300EEC4C3 /* HealthLens */; 343 | targetProxy = 3EA025D92C32FE7500EEC4C3 /* PBXContainerItemProxy */; 344 | }; 345 | /* End PBXTargetDependency section */ 346 | 347 | /* Begin XCBuildConfiguration section */ 348 | 3EA025E02C32FE7500EEC4C3 /* Debug */ = { 349 | isa = XCBuildConfiguration; 350 | buildSettings = { 351 | ALWAYS_SEARCH_USER_PATHS = NO; 352 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 353 | CLANG_ANALYZER_NONNULL = YES; 354 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 355 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 356 | CLANG_ENABLE_MODULES = YES; 357 | CLANG_ENABLE_OBJC_ARC = YES; 358 | CLANG_ENABLE_OBJC_WEAK = YES; 359 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 360 | CLANG_WARN_BOOL_CONVERSION = YES; 361 | CLANG_WARN_COMMA = YES; 362 | CLANG_WARN_CONSTANT_CONVERSION = YES; 363 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 364 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 365 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 366 | CLANG_WARN_EMPTY_BODY = YES; 367 | CLANG_WARN_ENUM_CONVERSION = YES; 368 | CLANG_WARN_INFINITE_RECURSION = YES; 369 | CLANG_WARN_INT_CONVERSION = YES; 370 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 371 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 372 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 373 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 374 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 375 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 376 | CLANG_WARN_STRICT_PROTOTYPES = YES; 377 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 378 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 379 | CLANG_WARN_UNREACHABLE_CODE = YES; 380 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 381 | COPY_PHASE_STRIP = NO; 382 | DEBUG_INFORMATION_FORMAT = dwarf; 383 | ENABLE_STRICT_OBJC_MSGSEND = YES; 384 | ENABLE_TESTABILITY = YES; 385 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 386 | GCC_C_LANGUAGE_STANDARD = gnu17; 387 | GCC_DYNAMIC_NO_PIC = NO; 388 | GCC_NO_COMMON_BLOCKS = YES; 389 | GCC_OPTIMIZATION_LEVEL = 0; 390 | GCC_PREPROCESSOR_DEFINITIONS = ( 391 | "DEBUG=1", 392 | "$(inherited)", 393 | ); 394 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 395 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 396 | GCC_WARN_UNDECLARED_SELECTOR = YES; 397 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 398 | GCC_WARN_UNUSED_FUNCTION = YES; 399 | GCC_WARN_UNUSED_VARIABLE = YES; 400 | IPHONEOS_DEPLOYMENT_TARGET = 17.5; 401 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 402 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 403 | MTL_FAST_MATH = YES; 404 | ONLY_ACTIVE_ARCH = YES; 405 | SDKROOT = iphoneos; 406 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 407 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 408 | }; 409 | name = Debug; 410 | }; 411 | 3EA025E12C32FE7500EEC4C3 /* Release */ = { 412 | isa = XCBuildConfiguration; 413 | buildSettings = { 414 | ALWAYS_SEARCH_USER_PATHS = NO; 415 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 416 | CLANG_ANALYZER_NONNULL = YES; 417 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 418 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 419 | CLANG_ENABLE_MODULES = YES; 420 | CLANG_ENABLE_OBJC_ARC = YES; 421 | CLANG_ENABLE_OBJC_WEAK = YES; 422 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 423 | CLANG_WARN_BOOL_CONVERSION = YES; 424 | CLANG_WARN_COMMA = YES; 425 | CLANG_WARN_CONSTANT_CONVERSION = YES; 426 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 427 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 428 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 429 | CLANG_WARN_EMPTY_BODY = YES; 430 | CLANG_WARN_ENUM_CONVERSION = YES; 431 | CLANG_WARN_INFINITE_RECURSION = YES; 432 | CLANG_WARN_INT_CONVERSION = YES; 433 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 434 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 435 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 436 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 437 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 438 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 439 | CLANG_WARN_STRICT_PROTOTYPES = YES; 440 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 441 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 442 | CLANG_WARN_UNREACHABLE_CODE = YES; 443 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 444 | COPY_PHASE_STRIP = NO; 445 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 446 | ENABLE_NS_ASSERTIONS = NO; 447 | ENABLE_STRICT_OBJC_MSGSEND = YES; 448 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 449 | GCC_C_LANGUAGE_STANDARD = gnu17; 450 | GCC_NO_COMMON_BLOCKS = YES; 451 | GCC_OPTIMIZATION_LEVEL = 3; 452 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 453 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 454 | GCC_WARN_UNDECLARED_SELECTOR = YES; 455 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 456 | GCC_WARN_UNUSED_FUNCTION = YES; 457 | GCC_WARN_UNUSED_VARIABLE = YES; 458 | IPHONEOS_DEPLOYMENT_TARGET = 17.5; 459 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 460 | MTL_ENABLE_DEBUG_INFO = NO; 461 | MTL_FAST_MATH = YES; 462 | SDKROOT = iphoneos; 463 | SWIFT_COMPILATION_MODE = wholemodule; 464 | VALIDATE_PRODUCT = YES; 465 | }; 466 | name = Release; 467 | }; 468 | 3EA025E32C32FE7500EEC4C3 /* Debug */ = { 469 | isa = XCBuildConfiguration; 470 | buildSettings = { 471 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 472 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 473 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; 474 | CODE_SIGN_ENTITLEMENTS = HealthLens/HealthLens.entitlements; 475 | CODE_SIGN_IDENTITY = "Apple Development"; 476 | CODE_SIGN_STYLE = Automatic; 477 | CURRENT_PROJECT_VERSION = 5; 478 | DEVELOPMENT_ASSET_PATHS = "\"HealthLens/Preview Content\""; 479 | DEVELOPMENT_TEAM = PPA4LZZXUN; 480 | ENABLE_PREVIEWS = YES; 481 | GENERATE_INFOPLIST_FILE = YES; 482 | INFOPLIST_FILE = HealthLens/Info.plist; 483 | INFOPLIST_KEY_CFBundleDisplayName = "Health Lens"; 484 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; 485 | INFOPLIST_KEY_NSHealthShareUsageDescription = "We use the usage description to do the following: export it to a csv format"; 486 | INFOPLIST_KEY_NSHealthUpdateUsageDescription = "Updating health data is only required by HealtKit and we should never ask for this information"; 487 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 488 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 489 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 490 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen.storyboard; 491 | INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault; 492 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 493 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 494 | IPHONEOS_DEPLOYMENT_TARGET = 17.5; 495 | LD_RUNPATH_SEARCH_PATHS = ( 496 | "$(inherited)", 497 | "@executable_path/Frameworks", 498 | ); 499 | MARKETING_VERSION = 1.1.1; 500 | PRODUCT_BUNDLE_IDENTIFIER = com.smartservices.HealthLens; 501 | PRODUCT_NAME = "$(TARGET_NAME)"; 502 | PROVISIONING_PROFILE_SPECIFIER = ""; 503 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 504 | SUPPORTS_MACCATALYST = NO; 505 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 506 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 507 | SWIFT_EMIT_LOC_STRINGS = YES; 508 | SWIFT_VERSION = 5.0; 509 | TARGETED_DEVICE_FAMILY = 1; 510 | }; 511 | name = Debug; 512 | }; 513 | 3EA025E42C32FE7500EEC4C3 /* Release */ = { 514 | isa = XCBuildConfiguration; 515 | buildSettings = { 516 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 517 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 518 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; 519 | CODE_SIGN_ENTITLEMENTS = HealthLens/HealthLens.entitlements; 520 | CODE_SIGN_IDENTITY = "Apple Development"; 521 | CODE_SIGN_STYLE = Automatic; 522 | CURRENT_PROJECT_VERSION = 5; 523 | DEVELOPMENT_ASSET_PATHS = "\"HealthLens/Preview Content\""; 524 | DEVELOPMENT_TEAM = PPA4LZZXUN; 525 | ENABLE_PREVIEWS = YES; 526 | GENERATE_INFOPLIST_FILE = YES; 527 | INFOPLIST_FILE = HealthLens/Info.plist; 528 | INFOPLIST_KEY_CFBundleDisplayName = "Health Lens"; 529 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; 530 | INFOPLIST_KEY_NSHealthShareUsageDescription = "We use the usage description to do the following: export it to a csv format"; 531 | INFOPLIST_KEY_NSHealthUpdateUsageDescription = "Updating health data is only required by HealtKit and we should never ask for this information"; 532 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 533 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 534 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 535 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen.storyboard; 536 | INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault; 537 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 538 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 539 | IPHONEOS_DEPLOYMENT_TARGET = 17.5; 540 | LD_RUNPATH_SEARCH_PATHS = ( 541 | "$(inherited)", 542 | "@executable_path/Frameworks", 543 | ); 544 | MARKETING_VERSION = 1.1.1; 545 | PRODUCT_BUNDLE_IDENTIFIER = com.smartservices.HealthLens; 546 | PRODUCT_NAME = "$(TARGET_NAME)"; 547 | PROVISIONING_PROFILE_SPECIFIER = ""; 548 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 549 | SUPPORTS_MACCATALYST = NO; 550 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 551 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 552 | SWIFT_EMIT_LOC_STRINGS = YES; 553 | SWIFT_VERSION = 5.0; 554 | TARGETED_DEVICE_FAMILY = 1; 555 | }; 556 | name = Release; 557 | }; 558 | 3EA025E62C32FE7500EEC4C3 /* Debug */ = { 559 | isa = XCBuildConfiguration; 560 | buildSettings = { 561 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 562 | BUNDLE_LOADER = "$(TEST_HOST)"; 563 | CODE_SIGN_STYLE = Automatic; 564 | CURRENT_PROJECT_VERSION = 1; 565 | DEVELOPMENT_TEAM = PPA4LZZXUN; 566 | GENERATE_INFOPLIST_FILE = YES; 567 | IPHONEOS_DEPLOYMENT_TARGET = 17.5; 568 | MARKETING_VERSION = 1.0; 569 | PRODUCT_BUNDLE_IDENTIFIER = com.smartservices.HealthLensTests; 570 | PRODUCT_NAME = "$(TARGET_NAME)"; 571 | SWIFT_EMIT_LOC_STRINGS = NO; 572 | SWIFT_VERSION = 5.0; 573 | TARGETED_DEVICE_FAMILY = "1,2"; 574 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HealthLens.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/HealthLens"; 575 | }; 576 | name = Debug; 577 | }; 578 | 3EA025E72C32FE7500EEC4C3 /* Release */ = { 579 | isa = XCBuildConfiguration; 580 | buildSettings = { 581 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 582 | BUNDLE_LOADER = "$(TEST_HOST)"; 583 | CODE_SIGN_STYLE = Automatic; 584 | CURRENT_PROJECT_VERSION = 1; 585 | DEVELOPMENT_TEAM = PPA4LZZXUN; 586 | GENERATE_INFOPLIST_FILE = YES; 587 | IPHONEOS_DEPLOYMENT_TARGET = 17.5; 588 | MARKETING_VERSION = 1.0; 589 | PRODUCT_BUNDLE_IDENTIFIER = com.smartservices.HealthLensTests; 590 | PRODUCT_NAME = "$(TARGET_NAME)"; 591 | SWIFT_EMIT_LOC_STRINGS = NO; 592 | SWIFT_VERSION = 5.0; 593 | TARGETED_DEVICE_FAMILY = "1,2"; 594 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HealthLens.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/HealthLens"; 595 | }; 596 | name = Release; 597 | }; 598 | 3EA025E92C32FE7500EEC4C3 /* Debug */ = { 599 | isa = XCBuildConfiguration; 600 | buildSettings = { 601 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 602 | CODE_SIGN_STYLE = Automatic; 603 | CURRENT_PROJECT_VERSION = 1; 604 | DEVELOPMENT_TEAM = PPA4LZZXUN; 605 | GENERATE_INFOPLIST_FILE = YES; 606 | MARKETING_VERSION = 1.0; 607 | PRODUCT_BUNDLE_IDENTIFIER = com.smartservices.HealthLensUITests; 608 | PRODUCT_NAME = "$(TARGET_NAME)"; 609 | SWIFT_EMIT_LOC_STRINGS = NO; 610 | SWIFT_VERSION = 5.0; 611 | TARGETED_DEVICE_FAMILY = "1,2"; 612 | TEST_TARGET_NAME = HealthLens; 613 | }; 614 | name = Debug; 615 | }; 616 | 3EA025EA2C32FE7500EEC4C3 /* Release */ = { 617 | isa = XCBuildConfiguration; 618 | buildSettings = { 619 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 620 | CODE_SIGN_STYLE = Automatic; 621 | CURRENT_PROJECT_VERSION = 1; 622 | DEVELOPMENT_TEAM = PPA4LZZXUN; 623 | GENERATE_INFOPLIST_FILE = YES; 624 | MARKETING_VERSION = 1.0; 625 | PRODUCT_BUNDLE_IDENTIFIER = com.smartservices.HealthLensUITests; 626 | PRODUCT_NAME = "$(TARGET_NAME)"; 627 | SWIFT_EMIT_LOC_STRINGS = NO; 628 | SWIFT_VERSION = 5.0; 629 | TARGETED_DEVICE_FAMILY = "1,2"; 630 | TEST_TARGET_NAME = HealthLens; 631 | }; 632 | name = Release; 633 | }; 634 | /* End XCBuildConfiguration section */ 635 | 636 | /* Begin XCConfigurationList section */ 637 | 3EA025B72C32FE7300EEC4C3 /* Build configuration list for PBXProject "HealthLens" */ = { 638 | isa = XCConfigurationList; 639 | buildConfigurations = ( 640 | 3EA025E02C32FE7500EEC4C3 /* Debug */, 641 | 3EA025E12C32FE7500EEC4C3 /* Release */, 642 | ); 643 | defaultConfigurationIsVisible = 0; 644 | defaultConfigurationName = Release; 645 | }; 646 | 3EA025E22C32FE7500EEC4C3 /* Build configuration list for PBXNativeTarget "HealthLens" */ = { 647 | isa = XCConfigurationList; 648 | buildConfigurations = ( 649 | 3EA025E32C32FE7500EEC4C3 /* Debug */, 650 | 3EA025E42C32FE7500EEC4C3 /* Release */, 651 | ); 652 | defaultConfigurationIsVisible = 0; 653 | defaultConfigurationName = Release; 654 | }; 655 | 3EA025E52C32FE7500EEC4C3 /* Build configuration list for PBXNativeTarget "HealthLensTests" */ = { 656 | isa = XCConfigurationList; 657 | buildConfigurations = ( 658 | 3EA025E62C32FE7500EEC4C3 /* Debug */, 659 | 3EA025E72C32FE7500EEC4C3 /* Release */, 660 | ); 661 | defaultConfigurationIsVisible = 0; 662 | defaultConfigurationName = Release; 663 | }; 664 | 3EA025E82C32FE7500EEC4C3 /* Build configuration list for PBXNativeTarget "HealthLensUITests" */ = { 665 | isa = XCConfigurationList; 666 | buildConfigurations = ( 667 | 3EA025E92C32FE7500EEC4C3 /* Debug */, 668 | 3EA025EA2C32FE7500EEC4C3 /* Release */, 669 | ); 670 | defaultConfigurationIsVisible = 0; 671 | defaultConfigurationName = Release; 672 | }; 673 | /* End XCConfigurationList section */ 674 | 675 | /* Begin XCRemoteSwiftPackageReference section */ 676 | 3E1EAE4B2D38148200090EC5 /* XCRemoteSwiftPackageReference "libxlsxwriter" */ = { 677 | isa = XCRemoteSwiftPackageReference; 678 | repositoryURL = "https://github.com/jmcnamara/libxlsxwriter"; 679 | requirement = { 680 | branch = main; 681 | kind = branch; 682 | }; 683 | }; 684 | /* End XCRemoteSwiftPackageReference section */ 685 | 686 | /* Begin XCSwiftPackageProductDependency section */ 687 | 3E1EAE502D381CFA00090EC5 /* libxlsxwriter */ = { 688 | isa = XCSwiftPackageProductDependency; 689 | package = 3E1EAE4B2D38148200090EC5 /* XCRemoteSwiftPackageReference "libxlsxwriter" */; 690 | productName = libxlsxwriter; 691 | }; 692 | 3E1EAE522D381D1900090EC5 /* libxlsxwriter */ = { 693 | isa = XCSwiftPackageProductDependency; 694 | package = 3E1EAE4B2D38148200090EC5 /* XCRemoteSwiftPackageReference "libxlsxwriter" */; 695 | productName = libxlsxwriter; 696 | }; 697 | /* End XCSwiftPackageProductDependency section */ 698 | }; 699 | rootObject = 3EA025B42C32FE7300EEC4C3 /* Project object */; 700 | } 701 | --------------------------------------------------------------------------------