Begin common API

This commit is contained in:
Christoph Hagen 2024-02-01 15:56:55 +01:00
parent c36ee29afb
commit b8162e6cb9
4 changed files with 299 additions and 8 deletions

View File

@ -58,6 +58,9 @@
E27BC6962B5FD61D003A8873 /* WorkoutEvent+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6952B5FD61D003A8873 /* WorkoutEvent+Mock.swift */; };
E27BC6982B5FD76F003A8873 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6972B5FD76F003A8873 /* Data+Extensions.swift */; };
E2FDFF162B6AFD990080A7B3 /* BinaryCodable in Frameworks */ = {isa = PBXBuildFile; productRef = E2FDFF152B6AFD990080A7B3 /* BinaryCodable */; };
E2FDFF182B6BB61D0080A7B3 /* HKHealthStoreInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF172B6BB61D0080A7B3 /* HKHealthStoreInterface.swift */; };
E2FDFF1A2B6BB6A40080A7B3 /* HKHealthStore+Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF192B6BB6A40080A7B3 /* HKHealthStore+Interface.swift */; };
E2FDFF1C2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF1B2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift */; };
E2FDFF202B6BE34C0080A7B3 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */; };
E2FDFF222B6BE35B0080A7B3 /* EventMetadata.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF212B6BE35B0080A7B3 /* EventMetadata.pb.swift */; };
/* End PBXBuildFile section */
@ -110,6 +113,9 @@
E27BC6932B5FD587003A8873 /* Workout+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workout+Mock.swift"; sourceTree = "<group>"; };
E27BC6952B5FD61D003A8873 /* WorkoutEvent+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WorkoutEvent+Mock.swift"; sourceTree = "<group>"; };
E27BC6972B5FD76F003A8873 /* Data+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; };
E2FDFF172B6BB61D0080A7B3 /* HKHealthStoreInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKHealthStoreInterface.swift; sourceTree = "<group>"; };
E2FDFF192B6BB6A40080A7B3 /* HKHealthStore+Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKHealthStore+Interface.swift"; sourceTree = "<group>"; };
E2FDFF1B2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKDatabaseFile+Interface.swift"; sourceTree = "<group>"; };
E2FDFF212B6BE35B0080A7B3 /* EventMetadata.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventMetadata.pb.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -163,7 +169,7 @@
8850025E2B5C273E00E7D4DB /* Assets.xcassets */,
885002602B5C273E00E7D4DB /* Preview Content */,
885002702B5C299900E7D4DB /* HealthDatabase.swift */,
885002802B5C37A800E7D4DB /* Database Entries */,
E2FDFF1D2B6BD1F00080A7B3 /* API */,
885002812B5C37B700E7D4DB /* Model */,
885002832B5C37C600E7D4DB /* Support */,
);
@ -192,13 +198,6 @@
path = Resources;
sourceTree = "<group>";
};
885002802B5C37A800E7D4DB /* Database Entries */ = {
isa = PBXGroup;
children = (
);
path = "Database Entries";
sourceTree = "<group>";
};
885002812B5C37B700E7D4DB /* Model */ = {
isa = PBXGroup;
children = (
@ -349,6 +348,7 @@
8850028F2B5D0EAF00E7D4DB /* Date+Extensions.swift in Sources */,
885002852B5C7AD600E7D4DB /* WorkoutEvent.swift in Sources */,
885002912B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift in Sources */,
E2FDFF182B6BB61D0080A7B3 /* HKHealthStoreInterface.swift in Sources */,
E27BC6922B5FD488003A8873 /* HealthDatabase+Mock.swift in Sources */,
E27BC6902B5FCEA4003A8873 /* WorkoutActivity+SQLite.swift in Sources */,
E201EC772B626FC1005B83D3 /* MetadataKey.swift in Sources */,
@ -364,17 +364,20 @@
E27BC6962B5FD61D003A8873 /* WorkoutEvent+Mock.swift in Sources */,
E27BC6802B5E74D7003A8873 /* LocationSampleListView.swift in Sources */,
8850028D2B5D0B5000E7D4DB /* WorkoutDetailView.swift in Sources */,
E2FDFF1C2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift in Sources */,
885002A32B5D217600E7D4DB /* MetadataValue.swift in Sources */,
E201EC7B2B6275CA005B83D3 /* Metadata.swift in Sources */,
8850028B2B5C896C00E7D4DB /* WorkoutActivity.swift in Sources */,
E27BC67A2B5D99AC003A8873 /* LocationSample.swift in Sources */,
885002952B5D147100E7D4DB /* DetailRow.swift in Sources */,
E201EC732B626A30005B83D3 /* WorkoutActivity+Mock.swift in Sources */,
E2FDFF1A2B6BB6A40080A7B3 /* HKHealthStore+Interface.swift in Sources */,
E27BC68A2B5FC255003A8873 /* Sample+Unit.swift in Sources */,
8850029D2B5D197300E7D4DB /* EventDetailView.swift in Sources */,
E27BC6862B5FBF0B003A8873 /* Sample.swift in Sources */,
E27BC67E2B5E6CE3003A8873 /* Sequence+Extensions.swift in Sources */,
E27BC68C2B5FC842003A8873 /* ActivitySamplesView.swift in Sources */,
E201EC812B631708005B83D3 /* Goal.swift in Sources */,
E27BC6942B5FD587003A8873 /* Workout+Mock.swift in Sources */,
E27BC68E2B5FCBD5003A8873 /* WorkoutEvent+SQLite.swift in Sources */,
8850025B2B5C273C00E7D4DB /* HealthImportApp.swift in Sources */,

View File

@ -0,0 +1,68 @@
import Foundation
import HealthKit
struct HKNotSupportedError: Error {
}
extension HealthDatabase {
public func executeSampleQuery(sampleType: HKSampleType, predicate: NSPredicate?, limit: Int, sortDescriptors: [NSSortDescriptor]?, resultsHandler: @escaping (HKSampleQuery, [HKSample]?, Error?) -> Void) -> HKQuery {
let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: limit, sortDescriptors: sortDescriptors, resultsHandler: resultsHandler)
switch sampleType {
case is HKWorkoutType:
workouts(predicate: predicate, limit: limit, sortDescriptors: sortDescriptors, resultsHandler: resultsHandler)
return query
case is HKCorrelationType:
// TODO: Implement
break
case is HKQuantityType:
// TODO: Implement
break
case is HKAudiogramSampleType:
// TODO: Implement
break
case is HKElectrocardiogramType:
// TODO: Implement
break
case is HKPrescriptionType:
// TODO: Implement
break
case is HKClinicalType:
// TODO: Implement
break
case is HKCategoryType:
// TODO: Implement
break
case is HKDocumentType:
// TODO: Implement
break
case is HKSeriesType:
// TODO: Implement
break
//case is HKCharacteristicType:
// break
//case is HKActivitySummaryType:
// break
default:
break
}
resultsHandler(query, nil, HKNotSupportedError())
return query
//sampleType.isMinimumDurationRestricted
//sampleType.isMaximumDurationRestricted
//sampleType.maximumAllowedDuration
//sampleType.allowsRecalibrationForEstimates
}
private func workouts(predicate: NSPredicate?, limit: Int, sortDescriptors: [NSSortDescriptor]?, resultsHandler: @escaping (HKSampleQuery, [HKSample]?, Error?) -> Void) {
#warning("Get workouts and filter them")
// Example predicates
// HKQuery.predicateForWorkouts(with: .greaterThanOrEqualTo, duration: 1)
// HKQuery.predicateForWorkouts(with: HKWorkoutActivityType)
}
}

View File

@ -0,0 +1,14 @@
import Foundation
import HealthKit
extension HKHealthStore: HKHealthStoreInterface {
/**
Creates a sample query and executes it.
*/
public func executeSampleQuery(sampleType: HKSampleType, predicate: NSPredicate?, limit: Int, sortDescriptors: [NSSortDescriptor]?, resultsHandler: @escaping (HKSampleQuery, [HKSample]?, Error?) -> Void) -> HKQuery {
let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: limit, sortDescriptors: sortDescriptors, resultsHandler: resultsHandler)
execute(query)
return query
}
}

View File

@ -0,0 +1,206 @@
import Foundation
import HealthKit
import UIKit
public protocol HKHealthStoreInterface {
// MARK: - Accessing HealthKit
/**
Returns the apps authorization status for sharing the specified data type.
This method checks the authorization status for saving data.
To help prevent possible leaks of sensitive health information, your app cannot determine whether or not a user has granted permission to read data.
If you are not given permission, it simply appears as if there is no data of the requested type in the HealthKit store.
If your app is given share permission but not read permission, you see only the data that your app has written to the store.
Data from other sources remains hidden.
- Parameter type: The type of data. This can be any concrete subclass of the ``HKObjectType`` class (any of the ``HKCharacteristicType`` , ``HKQuantityType``, ``HKCategoryType``, ``HKWorkoutType`` or ``HKCorrelationType`` classes).
- Returns: A value indicating the apps authorization status for this type. For a list of possible values, see `HKAuthorizationStatus`.
*/
func authorizationStatus(for type: HKObjectType) -> HKAuthorizationStatus
/**
Indicates whether the system presents the user with a permission sheet if your app requests authorization for the provided types.
When working with clinical types, users may need to reauthorize access when new data is added.
- Important: You can call this method from synchronous code using a completion handler, as shown on this page, or you can call it as an asynchronous method that has the following declaration:
```
func statusForAuthorizationRequest(toShare typesToShare: Set<HKSampleType>, read typesToRead: Set<HKObjectType>) async throws -> HKAuthorizationRequestStatus
```
For information about concurrency and asynchronous code in Swift, see [Calling Objective-C APIs Asynchronously](doc://com.apple.documentation/documentation/swift/calling-objective-c-apis-asynchronously).
*/
func getRequestStatusForAuthorization(
toShare typesToShare: Set<HKSampleType>,
read typesToRead: Set<HKObjectType>,
completion: @escaping (HKAuthorizationRequestStatus, Error?) -> Void
)
/**
Returns a Boolean value that indicates whether HealthKit is available on this device.
By default, HealthKit data is available on iOS and watchOS. HealthKit data is also available on iPadOS 17 or later. However, devices running in an enterprise environment may restrict access to HealthKit data.
Additionally, while the HealthKit framework is available on iPadOS 16 and earlier and on MacOS 13 and later, these devices dont have a copy of the HealthKit store. This means you can include HealthKit code in apps running on these devices, simplifying the creation of multiplatform apps. However, they cant read or write HealthKit data, and calls to ``isHealthDataAvailable()`` return ``false``.
- Returns: `true` if HealthKit is available; otherwise, `false`.
*/
static func isHealthDataAvailable() -> Bool
/**
Returns a Boolean value that indicates whether the current device supports clinical records.
This method returns true if the device is set to a locale where clinical records are supported, or if the user already has clinical records downloaded to their HealthKit store. Otherwise, it returns false.
This method lets users switch their locale without losing their health records.
*/
func supportsHealthRecords() -> Bool
/**
Requests permission to save and read the specified data types.
HealthKit performs these requests asynchronously. If you call this method with a new data type (a type of data that the user hasnt previously granted or denied permission for in this app), the system automatically displays the permission form, listing all the requested permissions. After the user has finished responding, this method calls its completion block on a background queue. If the user has already chosen to grant or prohibit access to all of the types specified, HealthKit calls the completion without prompting the user.
- Important: In watchOS 6 and later, this method displays the permission form on Apple Watch, enabling independent HealthKit apps. In watchOS 5 and earlier, this method prompts the user to authorize the app on their paired iPhone. For more information, see Creating Independent watchOS Apps.
Each data type has two separate permissions, one to read it and one to share it. You can make a single request, and include all the data types your app needs.
Customize the messages displayed on the permissions sheet by setting the following keys:
* ``NSHealthShareUsageDescription`` customizes the message for reading data.
* ``NSHealthUpdateUsageDescription`` customizes the message for writing data.
- Warning: You must set the usage keys, or your app will crash when you request authorization.
For projects created using Xcode 13 or later, set these keys in the Target Properties list on the apps Info tab. For projects created with Xcode 12 or earlier, set these keys in the apps Info.plist file. For more information, see [Information Property List](doc://com.apple.documentation/documentation/bundleresources/information_property_list).
After users have set the permissions for your app, they can always change them using either the Settings or the Health app. Your app appears in the Health apps Sources tab, even if the user didnt allow permission to read or share data.
- Parameter typesToShare: A set containing the data types you want to share. This set can contain any concrete subclass of the ``HKSampleType`` class (any of the ``HKQuantityType``, ``HKCategoryType``, ``HKWorkoutType``, or ``HKCorrelationType`` classes ). If the user grants permission, your app can create and save these data types to the HealthKit store.
- Parameter typesToRead: A set containing the data types you want to read. This set can contain any concrete subclass of the ``HKObjectType`` class (any of the ``HKCharacteristicType``, ``HKQuantityType``, ``HKCategoryType``, ``HKWorkoutType``, or ``HKCorrelationType`` classes). If the user grants permission, your app can read these data types from the HealthKit store.
- Parameter completion: A block called after the user finishes responding to the request. The system calls this block with the following parameters:
- Parameter success: A Boolean value that indicates whether the request succeeded. This value doesnt indicate whether the user actually granted permission. The parameter is false if an error occurred while processing the request; otherwise, its true.
- Parameter error: An error object. If an error occurred, this object contains information about the error; otherwise, its set to nil.
*/
func requestAuthorization(
toShare typesToShare: Set<HKSampleType>?,
read typesToRead: Set<HKObjectType>?,
completion: @escaping (Bool, Error?) -> Void
)
/**
Asynchronously requests permission to read a data type that requires per-object authorization (such as vision prescriptions).
- Important: You can call this method from synchronous code using a completion handler, as shown on this page, or you can call it as an asynchronous method that has the following declaration:
```
func requestPerObjectReadAuthorization(for objectType: HKObjectType, predicate: NSPredicate?) async throws
```
For information about concurrency and asynchronous code in Swift, see [Calling Objective-C APIs Asynchronously](doc://com.apple.documentation/documentation/swift/calling-objective-c-apis-asynchronously).
Some samples require per-object authorization. For these samples, people can select which ones your app can read on a sample-by-sample basis. By default, your app can read any of the per-object authorization samples that it has saved to the HealthKit store; however, you may not always have access to those samples. People can update the authorization status for any of these samples at any time.
Your app can begin by querying for any samples that it already has permission to read.
```
// Read the newest prescription from the HealthKit store.
let queryDescriptor = HKSampleQueryDescriptor(
predicates: [.visionPrescription()],
sortDescriptors: [SortDescriptor(\.startDate, order: .reverse)],
limit: 1)
let prescription: HKVisionPrescription
do {
guard let result = try await queryDescriptor.result(for: store).first else {
print("*** No prescription found. ***")
return
}
prescription = result
} catch {
// Handle the error here.
fatalError("*** An error occurred while reading the most recent vision prescriptions: \(error.localizedDescription) ***")
}
```
Based on the results, you can then decide whether you need to request authorization for additional samples. Call `requestPerObjectReadAuthorization(for:predicate:completion:)` to prompt someone to modify the samples your app has access to read.
```
// Request authorization to read vision prescriptions.
do {
try await store.requestPerObjectReadAuthorization(for: .visionPrescriptionType(), predicate: nil)
} catch HKError.errorUserCanceled {
// Handle the user canceling the authorization request.
print("*** The user canceled the authorization request. ***")
return
} catch {
// Handle the error here.
fatalError("*** An error occurred while requesting permission to read vision prescriptions: \(error.localizedDescription) ***")
}
```
- Important: Using the ``requestAuthorization(toShare:read:)`` method to request read access to any data types that require per-object authorization fails with an ``HKError.Code.errorInvalidArgument`` error.
When your app calls this method, HealthKit displays an authorization sheet that asks for permission to read the samples that match the predicate and object type. The person using your app can then select individual samples to share with your app. The system always asks for permission, regardless of whether they previously granted it.
After the person responds, the system calls the callback handler on an arbitrary background queue.
- Parameter objectType: The data type you want to read.
- Parameter predicate: A predicate that further restricts the data type.
- Parameter completion: A completion handler that the system calls after the user responds to the request. The completion handler has the following parameters:
- Parameter success: A Boolean value that indicates whether the request succeeded. This value doesnt indicate whether the user actually granted permission. The parameter is false if an error occurred while processing the request; otherwise, its true.
- Parameter error: An error object. If an error occurred, this object contains information about the error; otherwise, the system passes `nil`.
*/
func requestPerObjectReadAuthorization(
for objectType: HKObjectType,
predicate: NSPredicate?,
completion: @escaping (Bool, Error?) -> Void
)
/**
Requests permission to save and read the data types specified by an extension.
- Important: You can call this method from synchronous code using a completion handler, as shown on this page, or you can call it as an asynchronous method that has the following declaration:
```
func handleAuthorizationForExtension() async throws
```
For information about concurrency and asynchronous code in Swift, see [Calling Objective-C APIs Asynchronously](doc://com.apple.documentation/documentation/swift/calling-objective-c-apis-asynchronously).
The host app must implement the application delegates ``applicationShouldRequestHealthAuthorization(_:)`` method.
This delegate method is called after an app extension calls ``requestAuthorization(toShare:read:completion:)``.
The host app is then responsible for calling `handleAuthorizationForExtension(completion:)`.
This method prompts the user to authorize both the app and its extensions for the types that the extension requested.
The system performs this request asynchronously.
After the user has finished responding, this method calls its completion block on a background queue.
If the user has already chosen to grant or prohibit access to all of the types specified, the completion is called without prompting the user.
- Parameters:
- completion: A block that is called after the user finishes responding to the request. This block is passed the following parameters:
- success: A Boolean value that indicates whether the user responded to the prompt (if any). This value does not indicate whether permission was actually granted. This parameter is false if the user canceled the prompt without selecting permissions; otherwise, true.
- error: An error object. If an error occurred, this object contains information about the error; otherwise, it is set to `nil`.
*/
func handleAuthorizationForExtension(completion: @escaping (Bool, Error?) -> Void)
/**
The view controller that presents HealthKit authorization sheets.
By default, the system infers the correct view controller to show HealthKits authorization sheet.
In some cases, you can improve the user experience by explicitly defining how the system presents the authentication sheets.
In particular, consider setting this property when using HealthKit in an iPadOS app.
*/
//@available(iOS 17.0, macCatalyst 17.0, visionOS 1.0, *)
//var authorizationViewControllerPresenter: UIViewController? { get set }
// MARK: - Querying HealthKit data
/**
Augments a `HKSampleQuery`.
This function performs the same action as when creating a `HKSampleQuery` and executing it on a `HKHealthStore`.
It's necessary to extract this function since not all properties of a `HKSampleType` can be accessed.
*/
func executeSampleQuery(sampleType: HKSampleType, predicate: NSPredicate?, limit: Int, sortDescriptors: [NSSortDescriptor]?, resultsHandler: @escaping (HKSampleQuery, [HKSample]?, Error?) -> Void) -> HKQuery
}