├── Tests
├── TestData
│ ├── HKDeletedObject1
│ ├── HKDeletedObject10
│ ├── HKDeletedObject2
│ ├── HKDeletedObject3
│ ├── HKDeletedObject4
│ ├── HKDeletedObject5
│ ├── HKDeletedObject6
│ ├── HKDeletedObject7
│ ├── HKDeletedObject8
│ └── HKDeletedObject9
├── MockError.swift
├── XCTestCaseExtensions.swift
├── MockPermissionsManager.swift
├── MockSynchronizer.swift
├── Info.plist
├── MockQueryObserverDelegate.swift
├── MockUserDefaults.swift
├── TestHelpers.swift
├── MockExternalObject.swift
├── MockExternalObject2.swift
├── MockExternalStore.swift
├── HDSPermissionsManagerTests.swift
├── MockStore.swift
├── HDSQueryObserverTests.swift
├── HDSManagerTests.swift
└── HDSObjectSynchronizerTests.swift
├── HealthDataSync.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ ├── IDEWorkspaceChecks.plist
│ └── swiftpm
│ └── Package.resolved
├── HealthDataSync.xcodeproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
├── xcshareddata
│ └── xcschemes
│ │ └── HealthDataSync_Tests.xcscheme
└── project.pbxproj
├── Sources
├── Constants.swift
├── HDSError.swift
├── Proxies
│ ├── HDSUserDefaultsProxy.swift
│ ├── HDSUserDefaultsProxyProtocol.swift
│ ├── HDSStoreProxy.swift
│ └── HDSStoreProxyProtocol.swift
├── HDSConverterProtocol.swift
├── Synchronizers
│ ├── HDSObjectSynchronizerProtocol.swift
│ ├── HDSExternalStoreProtocol.swift
│ ├── HDSExternalObjectProtocol.swift
│ └── HDSObjectSynchronizer.swift
├── HDSQueryObserverFactory.swift
├── HDSManagerFactory.swift
├── HDSPermissionsManager.swift
├── HDSQueryObserverDelegate.swift
├── HDSManagerProtocol.swift
├── HDSManager.swift
└── HDSQueryObserver.swift
├── CODE_OF_CONDUCT.md
├── Package.swift
├── Pipelines
├── azure-pipelines-pr.yml
└── azure-pipelines-daily.yml
├── LICENSE
├── GeoPol.xml
├── CONTRIBUTING.md
├── .gitignore
├── SECURITY.md
└── README.md
/Tests/TestData/HKDeletedObject1:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/health-data-sync/HEAD/Tests/TestData/HKDeletedObject1
--------------------------------------------------------------------------------
/Tests/TestData/HKDeletedObject10:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/health-data-sync/HEAD/Tests/TestData/HKDeletedObject10
--------------------------------------------------------------------------------
/Tests/TestData/HKDeletedObject2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/health-data-sync/HEAD/Tests/TestData/HKDeletedObject2
--------------------------------------------------------------------------------
/Tests/TestData/HKDeletedObject3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/health-data-sync/HEAD/Tests/TestData/HKDeletedObject3
--------------------------------------------------------------------------------
/Tests/TestData/HKDeletedObject4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/health-data-sync/HEAD/Tests/TestData/HKDeletedObject4
--------------------------------------------------------------------------------
/Tests/TestData/HKDeletedObject5:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/health-data-sync/HEAD/Tests/TestData/HKDeletedObject5
--------------------------------------------------------------------------------
/Tests/TestData/HKDeletedObject6:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/health-data-sync/HEAD/Tests/TestData/HKDeletedObject6
--------------------------------------------------------------------------------
/Tests/TestData/HKDeletedObject7:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/health-data-sync/HEAD/Tests/TestData/HKDeletedObject7
--------------------------------------------------------------------------------
/Tests/TestData/HKDeletedObject8:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/health-data-sync/HEAD/Tests/TestData/HKDeletedObject8
--------------------------------------------------------------------------------
/Tests/TestData/HKDeletedObject9:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/health-data-sync/HEAD/Tests/TestData/HKDeletedObject9
--------------------------------------------------------------------------------
/HealthDataSync.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/HealthDataSync.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Sources/Constants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Constants.swift
3 | // HealthDataSync
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 |
10 | public struct Constants {
11 | public static let defaultBatchSize = 25
12 | }
13 |
--------------------------------------------------------------------------------
/HealthDataSync.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Sources/HDSError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HDSError.swift
3 | // HealthDataSync
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 |
10 | public enum HDSError : Error
11 | {
12 | case unavailable
13 | case noSpecifiedTypes
14 | case notSupported
15 | case operationCancelled
16 | }
17 |
--------------------------------------------------------------------------------
/Tests/MockError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockError.swift
3 | // HealthDataSync_Tests
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 |
10 | public enum MockError : Error {
11 | case fetchObjectsFailure
12 | case addFailure
13 | case updateFailure
14 | case deleteFailure
15 | case preferredUnitsFailure
16 | case enableBackgroundDeliveryFailure
17 | case disableBackgroundDeliveryFailure
18 | }
19 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Microsoft Open Source Code of Conduct
2 |
3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
4 |
5 | Resources:
6 |
7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
10 |
--------------------------------------------------------------------------------
/Tests/XCTestCaseExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // XCTestCaseExtensions.swift
3 | // HealthDataSync_Tests
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import XCTest
9 |
10 | extension XCTestCase {
11 | public func waitForCondition(object: Any, format: String, _ args: CVarArg...) {
12 | let predicate = NSPredicate(format: format, args)
13 | let ex = expectation(for: predicate, evaluatedWith: object, handler: nil)
14 | _ = XCTWaiter.wait(for: [ex], timeout: 2)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.0
2 | // Package.swift
3 | // HealthDataSync
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import PackageDescription
9 |
10 | let package = Package(
11 | name: "HealthDataSync",
12 | platforms: [
13 | .iOS(.v11)
14 | ],
15 | products: [
16 | .library(
17 | name: "HealthDataSync",
18 | targets: ["HealthDataSync"]),
19 | ],
20 | targets: [
21 | .target(
22 | name: "HealthDataSync",
23 | path: "Sources"),
24 | ]
25 | )
26 |
--------------------------------------------------------------------------------
/Tests/MockPermissionsManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockPermissionsManager.swift
3 | // HealthDataSync_Tests
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 |
10 | public class MockPermissionsManager : HDSPermissionsManager {
11 | public var authorizeHealthKitCompletions = [(success: Bool, error: Error?)]()
12 |
13 | open override func authorizeHealthKit(_ completion: @escaping (Bool, Error?) -> Void) {
14 | let comp = authorizeHealthKitCompletions.removeFirst()
15 | completion(comp.success, comp.error)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Pipelines/azure-pipelines-pr.yml:
--------------------------------------------------------------------------------
1 | # Xcode
2 | # Build, test, and archive an Xcode workspace on macOS.
3 | # Add steps that install certificates, test, sign, and distribute an app, save build artifacts, and more:
4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/xcode
5 |
6 | trigger: none
7 |
8 | pool:
9 | vmImage: 'macos-latest'
10 |
11 | steps:
12 |
13 | - task: Xcode@5
14 | inputs:
15 | actions: 'test'
16 | configuration: 'Debug'
17 | sdk: 'iphoneos'
18 | xcWorkspacePath: '**/HealthDataSync.xcworkspace'
19 | scheme: 'HealthDataSync_Tests'
20 | xcodeVersion: '11'
21 | packageApp: false
22 | destinationPlatformOption: 'iOS'
23 | destinationSimulators: 'iPhone 8'
24 |
--------------------------------------------------------------------------------
/HealthDataSync.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "Nimble",
6 | "repositoryURL": "https://github.com/Quick/Nimble",
7 | "state": {
8 | "branch": null,
9 | "revision": "f8657642dfdec9973efc79cc68bcef43a653a2bc",
10 | "version": "8.0.2"
11 | }
12 | },
13 | {
14 | "package": "Quick",
15 | "repositoryURL": "https://github.com/Quick/Quick",
16 | "state": {
17 | "branch": null,
18 | "revision": "33682c2f6230c60614861dfc61df267e11a1602f",
19 | "version": "2.2.0"
20 | }
21 | }
22 | ]
23 | },
24 | "version": 1
25 | }
26 |
--------------------------------------------------------------------------------
/Tests/MockSynchronizer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockSynchronizer.swift
3 | // HealthDataSync_Tests
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 | import HealthKit
10 |
11 | public class MockSynchronizer : HDSObjectSynchronizer {
12 | public var synchronizeCompletions = [Error?]()
13 | public var synchronizeParams = [(objects: [HKObject]?, deletedObjects: [HKDeletedObject]?)]()
14 |
15 | open override func synchronize(objects: [HKObject]?, deletedObjects: [HKDeletedObject]?, completion: @escaping (Error?) -> Void) {
16 | synchronizeParams.append((objects, deletedObjects))
17 | let comp = synchronizeCompletions.removeFirst()
18 | completion(comp)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Pipelines/azure-pipelines-daily.yml:
--------------------------------------------------------------------------------
1 | # Xcode
2 | # Build, test, and archive an Xcode workspace on macOS.
3 | # Add steps that install certificates, test, sign, and distribute an app, save build artifacts, and more:
4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/xcode
5 |
6 | schedules:
7 | - cron: "0 12 * * *"
8 | displayName: Daily build
9 | branches:
10 | include:
11 | - master
12 |
13 | pool:
14 | vmImage: 'macos-latest'
15 |
16 | steps:
17 |
18 | - task: Xcode@5
19 | inputs:
20 | actions: 'test'
21 | configuration: 'Debug'
22 | sdk: 'iphoneos'
23 | xcWorkspacePath: '**/HealthDataSync.xcworkspace'
24 | scheme: 'HealthDataSync_Tests'
25 | xcodeVersion: '11'
26 | packageApp: false
27 | destinationPlatformOption: 'iOS'
28 | destinationSimulators: 'iPhone 8'
29 |
--------------------------------------------------------------------------------
/Tests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Sources/Proxies/HDSUserDefaultsProxy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HDSUserDefaultsProxy.swift
3 | // HealthDataSync
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 |
10 | public class HDSUserDefaultsProxy: HDSUserDefaultsProxyProtocol
11 | {
12 | private var userDefaults: UserDefaults
13 |
14 | public init(userDefaults: UserDefaults)
15 | {
16 | self.userDefaults = userDefaults
17 | }
18 |
19 | public func set(_ value: Any?, forKey defaultName: String)
20 | {
21 | self.userDefaults.set(value, forKey: defaultName)
22 | }
23 |
24 | public func removeObject(forKey defaultName: String)
25 | {
26 | self.userDefaults.removeObject(forKey: defaultName)
27 | }
28 |
29 | public func data(forKey defaultName: String) -> Data?
30 | {
31 | return self.userDefaults.data(forKey: defaultName)
32 | }
33 |
34 | public func object(forKey defaultName: String) -> Any?
35 | {
36 | return self.userDefaults.object(forKey:defaultName)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/HDSConverterProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HDSConverterProtocol.swift
3 | // HealthDataSync
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | #if os(iOS)
9 |
10 | import Foundation
11 | import HealthKit
12 |
13 | public protocol HDSConverterProtocol
14 | {
15 | /// Converts a HealthKit HKObject to a specific destination type.
16 | ///
17 | /// - Parameter object: The HealthKit HKObject to convert.
18 | /// - Returns: A new instance of the destination object created from the HealthKit HKObject.
19 | /// - Throws: If there is an error during the conversion process.
20 | func convert(object: HKObject) throws -> T
21 |
22 | /// Converts a HealthKit HKDeletedObject to a specific destination type.
23 | ///
24 | /// - Parameter object: The HealthKit HKDeletedObject to convert.
25 | /// - Returns: A new instance of the destination object created from the HealthKit HKDeletedObject.
26 | /// - Throws: If there is an error during the conversion process.
27 | func convert(deletedObject: HKDeletedObject) throws -> T
28 | }
29 |
30 | #endif
31 |
--------------------------------------------------------------------------------
/Sources/Synchronizers/HDSObjectSynchronizerProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HDSObjectSynchronizerProtocol.swift
3 | // HealthDataSync
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 | import HealthKit
10 |
11 | public protocol HDSObjectSynchronizerProtocol
12 | {
13 |
14 | /// A custom converter that handles the conversion of HealthKit types.
15 | var converter: HDSConverterProtocol? { get set }
16 |
17 | /// The external object type the used by the synchronizer to convert HealthKit objects to a format compatible with the external store.
18 | var externalObjectType: HDSExternalObjectProtocol.Type { get }
19 |
20 | /// Synchronizes a objects with an external store.
21 | ///
22 | /// - Parameters:
23 | /// - objects: HealthKit objects to be updated or created.
24 | /// - deletedObjects: HealthKit objects to be deleted.
25 | /// - completion: Envoked when the synchronization process completed.
26 | func synchronize(objects: [HKObject]?, deletedObjects: [HKDeletedObject]?, completion: @escaping (Error?) -> Void)
27 | }
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Microsoft Corporation.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE
22 |
--------------------------------------------------------------------------------
/Sources/HDSQueryObserverFactory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HDSQueryObserverFactory.swift
3 | // HealthDataSync
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 |
10 | public class HDSQueryObserverFactory : NSObject
11 | {
12 | private let store: HDSStoreProxyProtocol
13 | private let userDefaults: HDSUserDefaultsProxyProtocol
14 |
15 | public init(store: HDSStoreProxyProtocol, userDefaults: HDSUserDefaultsProxyProtocol)
16 | {
17 | self.store = store
18 | self.userDefaults = userDefaults
19 | }
20 |
21 | internal func observers(with synchronizers:[HDSObjectSynchronizerProtocol]) -> [HDSQueryObserver]
22 | {
23 | var observers = [HDSQueryObserver]()
24 |
25 | synchronizers.forEach
26 | { (synchronizer) in
27 |
28 | let observer = HDSQueryObserver(store: self.store,
29 | userDefaultsProxy: self.userDefaults,
30 | synchronizer:synchronizer)
31 |
32 | observers.append(observer)
33 | }
34 |
35 | return observers
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Tests/MockQueryObserverDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockQueryObserverDelegate.swift
3 | // HealthDataSync_Tests
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 |
10 | public class MockQueryObserverDelegate : HDSQueryObserverDelegate {
11 | public var batchSizeParams = [HDSQueryObserver]()
12 | public var batchSizeReturns = [Int?]()
13 | public var shouldExecuteParams = [HDSQueryObserver]()
14 | public var shouldExecuteCompletions = [Bool]()
15 | public var didFinishExecutionParams = [(observer: HDSQueryObserver, error: Error?)]()
16 |
17 | public func batchSize(for observer: HDSQueryObserver) -> Int? {
18 | batchSizeParams.append(observer)
19 | return batchSizeReturns.removeFirst()
20 | }
21 |
22 | public func shouldExecute(for observer: HDSQueryObserver, completion: @escaping (Bool) -> Void) {
23 | shouldExecuteParams.append(observer)
24 | let comp = shouldExecuteCompletions.removeFirst()
25 | completion(comp)
26 | }
27 |
28 | public func didFinishExecution(for observer: HDSQueryObserver, error: Error?) {
29 | didFinishExecutionParams.append((observer, error))
30 | }
31 |
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/GeoPol.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 | ]>
8 |
9 |
10 |
11 | &GitRepoName;
12 |
13 |
14 |
15 | .
16 |
17 |
18 |
19 |
20 | .gitignore
21 | GeoPol.xml
22 |
23 |
24 |
--------------------------------------------------------------------------------
/Tests/MockUserDefaults.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockUserDefaults.swift
3 | // HealthDataSync_Tests
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 |
10 | public class MockUserDefaults : HDSUserDefaultsProxyProtocol {
11 | public var setParams = [(value: Any?, defaultName: String)]()
12 | public var removeObjectParams = [String]()
13 | public var dataParams = [String]()
14 | public var dataReturns = [Data?]()
15 | public var objectParams = [String]()
16 | public var objectReturns = [Any?]()
17 |
18 | public func set(_ value: Any?, forKey defaultName: String) {
19 | setParams.append((value, defaultName))
20 | }
21 |
22 | public func removeObject(forKey defaultName: String) {
23 | removeObjectParams.append(defaultName)
24 | }
25 |
26 | public func data(forKey defaultName: String) -> Data? {
27 | dataParams.append(defaultName)
28 | if dataReturns.count > 0 {
29 | return dataReturns.removeFirst()
30 | }
31 |
32 | return setParams.first?.value as? Data
33 | }
34 |
35 | public func object(forKey defaultName: String) -> Any? {
36 | objectParams.append(defaultName)
37 | return objectReturns.removeFirst()
38 | }
39 |
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/HDSManagerFactory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HDSManagerFactory.swift
3 | // HealthDataSync
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | #if os(iOS)
9 |
10 | import Foundation
11 | import HealthKit
12 |
13 | public class HDSManagerFactory: NSObject
14 | {
15 | static private var _manager: HDSManagerProtocol?
16 | static private var instanceLock = NSObject()
17 |
18 | static public func manager() -> HDSManagerProtocol
19 | {
20 | objc_sync_enter(instanceLock)
21 |
22 | defer
23 | {
24 | objc_sync_exit(instanceLock)
25 | }
26 |
27 | if (_manager != nil)
28 | {
29 | return _manager!
30 | }
31 |
32 | let store = HDSStoreProxy(store: HKHealthStore())
33 | let userDefaults = HDSUserDefaultsProxy(userDefaults: UserDefaults.standard)
34 | let permissionsManager = HDSPermissionsManager(store: store)
35 |
36 | _manager = HDSManager(store: store,
37 | userDefaults: userDefaults,
38 | permissionsManager: permissionsManager,
39 | observerFactory: HDSQueryObserverFactory(store: store, userDefaults: userDefaults))
40 |
41 | return _manager!
42 | }
43 | }
44 |
45 | #endif
46 |
--------------------------------------------------------------------------------
/Sources/Proxies/HDSUserDefaultsProxyProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HDSUserDefaultsProxyProtocol.swift
3 | // HealthDataSync
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 |
10 | public protocol HDSUserDefaultsProxyProtocol
11 | {
12 | /*!
13 | -setObject:forKey: immediately stores a value (or removes the value if nil is passed as the value) for the provided key in the search list entry for the receiver's suite name in the current user and any host, then asynchronously stores the value persistently, where it is made available to other processes.
14 | */
15 | func set(_ value: Any?, forKey defaultName: String)
16 |
17 | /// -removeObjectForKey: is equivalent to -[... setObject:nil forKey:defaultName]
18 | func removeObject(forKey defaultName: String)
19 |
20 | /// -dataForKey: is equivalent to -objectForKey:, except that it will return nil if the value is not an NSData.
21 | func data(forKey defaultName: String) -> Data?
22 |
23 | /*!
24 | -objectForKey: will search the receiver's search list for a default with the key 'defaultName' and return it. If another process has changed defaults in the search list, NSUserDefaults will automatically update to the latest values. If the key in question has been marked as ubiquitous via a Defaults Configuration File, the latest value may not be immediately available, and the registered value will be returned instead.
25 | */
26 | func object(forKey defaultName: String) -> Any?
27 | }
28 |
--------------------------------------------------------------------------------
/Tests/TestHelpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestHelpers.swift
3 | // HealthDataSync_Tests
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 | import HealthKit
10 |
11 | public class TestHelpers {
12 |
13 | public static func healthKitObjects(count: Int) -> [HKObject] {
14 | var objects = [HKObject]()
15 |
16 | for _ in 0.. [HKDeletedObject] {
24 | var objects = [HKDeletedObject]()
25 |
26 |
27 |
28 | for i in 0.. [HKObjectType]? {
19 | return [HKObjectType.quantityType(forIdentifier: .heartRate)!]
20 | }
21 |
22 | public static func healthKitObjectType() -> HKObjectType? {
23 | return HKObjectType.quantityType(forIdentifier: .heartRate)
24 | }
25 |
26 | public static func externalObject(object: HKObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
27 | let externalObject = MockExternalObject()
28 | externalObject.uuid = object.uuid
29 | externalObject.externalObjectParams = (object, converter)
30 | return externalObject
31 | }
32 |
33 | public static func externalObject(deletedObject: HKDeletedObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
34 | let externalObject = MockExternalObject()
35 | externalObject.uuid = deletedObject.uuid
36 | externalObject.externalObjectDeletedParams = (deletedObject, converter)
37 | return externalObject
38 | }
39 |
40 | public func update(with object: HKObject) {
41 | updateCount += 1
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Tests/MockExternalObject2.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockExternalObject2.swift
3 | // HealthDataSync_Tests
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 | import HealthKit
10 |
11 | public class MockExternalObject2 : HDSExternalObjectProtocol {
12 | public var updateCount = 0
13 | public var externalObjectParams: (object: HKObject, converter: HDSConverterProtocol?)?
14 | public var externalObjectDeletedParams: (deletedObject: HKDeletedObject, converter: HDSConverterProtocol?)?
15 |
16 | public var uuid = UUID(uuidString: "11111111-1111-1111-1111-111111111111")!
17 |
18 | public static func authorizationTypes() -> [HKObjectType]? {
19 | return [HKObjectType.quantityType(forIdentifier: .stepCount)!]
20 | }
21 |
22 | public static func healthKitObjectType() -> HKObjectType? {
23 | return HKObjectType.quantityType(forIdentifier: .stepCount)
24 | }
25 |
26 | public static func externalObject(object: HKObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
27 | let externalObject = MockExternalObject2()
28 | externalObject.uuid = object.uuid
29 | externalObject.externalObjectParams = (object, converter)
30 | return externalObject
31 | }
32 |
33 | public static func externalObject(deletedObject: HKDeletedObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
34 | let externalObject = MockExternalObject2()
35 | externalObject.uuid = deletedObject.uuid
36 | externalObject.externalObjectDeletedParams = (deletedObject, converter)
37 | return externalObject
38 | }
39 |
40 | public func update(with object: HKObject) {
41 | updateCount += 1
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Mac OS generated
6 | .DS_Store
7 | .DS_Store?
8 | ._*
9 | .Spotlight-V100
10 | .Trashes
11 | ehthumbs.db
12 | Thumbs.db
13 |
14 | ## Build generated
15 | build/
16 | DerivedData/
17 |
18 | ## Various settings
19 | *.pbxuser
20 | !default.pbxuser
21 | *.mode1v3
22 | !default.mode1v3
23 | *.mode2v3
24 | !default.mode2v3
25 | *.perspectivev3
26 | !default.perspectivev3
27 | xcuserdata/
28 |
29 | ## Other
30 | *.moved-aside
31 | *.xccheckout
32 | *.xcscmblueprint
33 |
34 | ## Obj-C/Swift specific
35 | *.hmap
36 | *.ipa
37 | *.dSYM.zip
38 | *.dSYM
39 |
40 | ## Playgrounds
41 | timeline.xctimeline
42 | playground.xcworkspace
43 |
44 | # Swift Package Manager
45 | #
46 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
47 | # Packages/
48 | # Package.pins
49 | # Package.resolved
50 | .build/
51 | .swiftpm/
52 |
53 | # CocoaPods
54 | #
55 | # We recommend against adding the Pods directory to your .gitignore. However
56 | # you should judge for yourself, the pros and cons are mentioned at:
57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
58 | #
59 | # Pods/
60 |
61 | # Carthage
62 | #
63 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
64 | # Carthage/Checkouts
65 |
66 | Carthage/Build
67 |
68 | # fastlane
69 | #
70 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
71 | # screenshots whenever they are needed.
72 | # For more information about the recommended setup visit:
73 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
74 |
75 | fastlane/report.xml
76 | fastlane/Preview.html
77 | fastlane/screenshots/**/*.png
78 | fastlane/test_output
79 |
--------------------------------------------------------------------------------
/Sources/HDSPermissionsManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HDSPermissionsManager
3 | // HealthDataSync
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | #if os(iOS)
9 |
10 | import Foundation
11 | import HealthKit
12 |
13 | open class HDSPermissionsManager
14 | {
15 | // Properties
16 | public var readTypes = [HKObjectType]()
17 | public var shareTypes = [HKSampleType]()
18 | private let store: HDSStoreProxyProtocol
19 |
20 | // Initializer
21 | public init(store: HDSStoreProxyProtocol)
22 | {
23 | self.store = store
24 | }
25 |
26 | open func authorizeHealthKit(_ completion:@escaping (_ success: Bool, _ error: Error?) -> Void)
27 | {
28 | // HealthKit not available (for e.g. on iPad)
29 | if (!self.store.isHealthDataAvailable())
30 | {
31 | print("HealthKit is not available on the current device!")
32 | completion(false, HDSError.unavailable)
33 | return
34 | }
35 |
36 | // Empty Read and Write Types
37 | if (self.readTypes.isEmpty && self.shareTypes.isEmpty)
38 | {
39 | print("HealthKit read and write types were empty!")
40 | completion(false, HDSError.noSpecifiedTypes)
41 | return
42 | }
43 |
44 | // Get read and write object types to authorize.
45 | let readTypes: Set = Set(self.readTypes)
46 | let shareTypes: Set = Set(self.shareTypes)
47 |
48 | print("Requesting authorization to read and write types")
49 |
50 | self.store.requestAuthorization(toShare: shareTypes, read: readTypes)
51 | {(success, error) -> Void in
52 |
53 | print((success ? "HealthKit authorization succeeded!" : "HealthKit authorization failed!"))
54 |
55 | if let authError = error
56 | {
57 | // Error exists.
58 | print(authError)
59 | }
60 |
61 | completion(success, error)
62 | }
63 | }
64 | }
65 |
66 | #endif
67 |
--------------------------------------------------------------------------------
/Tests/MockExternalStore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockExternalStore.swift
3 | // HealthDataSync_Tests
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 |
10 | public class MockExternalStore : HDSExternalStoreProtocol {
11 | public var fetchObjectsParams = [[HDSExternalObjectProtocol]]()
12 | public var fetchObjecsCompletions = [(objects: [HDSExternalObjectProtocol]?, error: Error?)]()
13 | public var addParams = [[HDSExternalObjectProtocol]]()
14 | public var addCompletions = [Error?]()
15 | public var updateParams = [[HDSExternalObjectProtocol]]()
16 | public var updateCompletions = [Error?]()
17 | public var deleteParams = [[HDSExternalObjectProtocol]]()
18 | public var deleteCompletions = [Error?]()
19 |
20 | public func reset() {
21 | fetchObjectsParams.removeAll()
22 | fetchObjecsCompletions.removeAll()
23 | addParams.removeAll()
24 | addCompletions.removeAll()
25 | updateParams.removeAll()
26 | updateCompletions.removeAll()
27 | deleteParams.removeAll()
28 | deleteCompletions.removeAll()
29 | }
30 |
31 | public func fetchObjects(with objects: [HDSExternalObjectProtocol], completion: @escaping ([HDSExternalObjectProtocol]?, Error?) -> Void) {
32 | fetchObjectsParams.append(objects)
33 | let comp = fetchObjecsCompletions.removeFirst()
34 | completion(comp.objects, comp.error)
35 | }
36 |
37 | public func add(objects: [HDSExternalObjectProtocol], completion: @escaping (Error?) -> Void) {
38 | addParams.append(objects)
39 | let error = addCompletions.removeFirst()
40 | completion(error)
41 | }
42 |
43 | public func update(objects: [HDSExternalObjectProtocol], completion: @escaping (Error?) -> Void) {
44 | updateParams.append(objects)
45 | let error = updateCompletions.removeFirst()
46 | completion(error)
47 | }
48 |
49 | public func delete(deletedObjects: [HDSExternalObjectProtocol], completion: @escaping (Error?) -> Void) {
50 | deleteParams.append(deletedObjects)
51 | let error = deleteCompletions.removeFirst()
52 | completion(error)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/HDSQueryObserverDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HDSQueryObserverDelegate.swift
3 | // HealthDataSync
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 |
10 | public protocol HDSQueryObserverDelegate: class
11 | {
12 |
13 | /// The maximum number of samples the given HDSQueryObserver will fetch in the anchored query. If the number of changes is > than the batchLimit, queries will be recursively executed until all changes are synchronized.
14 | ///
15 | /// - Parameter observer: The HDSQueryObserver that was notified of changes.
16 | /// - Returns: The number of changes that should be included in the query result. (If nil, the default value of 25 will be used.
17 | func batchSize(for observer: HDSQueryObserver) -> Int?
18 |
19 | /// Will be called when the HDSQueryObserver is notified of changes relating to the query, before the execution of the anchored query.
20 | ///
21 | /// - Parameters:
22 | /// - observer: The HDSQueryObserver that was notified of changes.
23 | /// - completion: Must be called to start the execution of the anchored query and subsequent synchronization with the external store. Return true to start the execution and false to cancel.
24 | func shouldExecute(for observer: HDSQueryObserver, completion: @escaping (Bool) -> Void)
25 |
26 | /// Will be called after execution of the anchor query AND synchronization with the external store has completed.
27 | ///
28 | /// - Parameters:
29 | /// - observer: The HDSQueryObserver that finished execution.
30 | /// - error: An Error providing data on an execution or synchronization failure (error will be nil if the process completed successfully).
31 | func didFinishExecution(for observer: HDSQueryObserver, error: Error?)
32 | }
33 |
34 | public extension HDSQueryObserverDelegate
35 | {
36 | func batchSize(for observer: HDSQueryObserver) -> Int?
37 | {
38 | return Constants.defaultBatchSize
39 | }
40 |
41 | func shouldExecute(for observer: HDSQueryObserver, completion: @escaping (Bool) -> Void)
42 | {
43 | completion(true)
44 | }
45 |
46 | func didFinishExecution(for observer: HDSQueryObserver, error: Error?)
47 | {
48 |
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/Synchronizers/HDSExternalStoreProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HDSExternalStoreProtocol.swift
3 | // HealthDataSync
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 |
10 | public protocol HDSExternalStoreProtocol
11 | {
12 |
13 | /// Will be called to fetch objects from an external store.
14 | ///
15 | /// - Parameters:
16 | /// - objects: A collection of objects conforming to HDSExternalObjectProtocol used to fetch the externally stored objects.
17 | /// - completion: MUST be called once the operation is completed and provide objects conforming to the HDSExternalObjectProtocol (if any) or an Error object if the operation fails.
18 | /// - Important: It is assuemd that any objects returned in the completion exist in the external store and will be updated NOT created.
19 | /// - Returns: void
20 | func fetchObjects(with objects: [HDSExternalObjectProtocol], completion: @escaping ([HDSExternalObjectProtocol]? , Error?) -> Void)
21 |
22 | /// Will be called to add new objects to an external store.
23 | ///
24 | /// - Parameters:
25 | /// - objects: Objects conforming to HDSExternalObjectProtocol.
26 | /// - completion: MUST be called when the operation has completed. An Error should be provided if the operation fails.
27 | /// - Returns: void
28 | func add(objects: [HDSExternalObjectProtocol], completion: @escaping (Error?) -> Void)
29 |
30 | /// Will be called to update existing objects in an external store
31 | ///
32 | /// - Parameters:
33 | /// - objects: Objects conforming to HDSExternalObjectProtocol.
34 | /// - completion: MUST be called when the operation has completed. An Error should be provided if the operation fails.
35 | /// - Returns: void
36 | func update(objects: [HDSExternalObjectProtocol], completion: @escaping (Error?) -> Void)
37 |
38 | /// Will be called to delete existing objects from an external store
39 | ///
40 | /// - Parameters:
41 | /// - deletedObjects: A collection of objects conforming to HDSExternalObjectProtocol used to delete externally stored objects.
42 | /// - completion: MUST be called when the operation has completed. An Error should be provided if the operation fails.
43 | /// - Returns: void
44 | func delete(deletedObjects: [HDSExternalObjectProtocol], completion: @escaping (Error?) -> Void)
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/Synchronizers/HDSExternalObjectProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HDSExternalObjectProtocol.swift
3 | // HealthDataSync
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 | import HealthKit
10 |
11 | public protocol HDSExternalObjectProtocol
12 | {
13 |
14 | /// A unique identifier used to match HealthKit objects and External Objects.
15 | /// This identifier is used to query both HealthKit and the External Store to perform updates or deletes on objects that were previously synced.
16 | var uuid: UUID { get set }
17 |
18 | /// The HealthKit object type displayed to the user in the authorization UI
19 | /// In some cases (e.g. blood pressure) a user must authorize each component of blood pressure separately (systolic, diastolic, and heart rate),
20 | /// but the query will be a single correlation type
21 | static func authorizationTypes() -> [HKObjectType]?
22 |
23 | /// The HealthKit object type used to query HealthKit.
24 | static func healthKitObjectType() -> HKObjectType?
25 |
26 | /// Creates a new External Object populated with data from the HKObject.
27 | ///
28 | /// - Parameters:
29 | /// - object: An HKObject containing data to be copied to the new External Object.
30 | /// - converter: An instance of a custom converter class.
31 | /// - Returns: A new object conforming to the HDSExternalObjectProtocol populated with the data from the HKObject or nil if the HKObject cannot be processed.
32 | static func externalObject(object: HKObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol?
33 |
34 | /// Creates a new External Object populated with data from the HKDeletedObject.
35 | ///
36 | /// - Parameters:
37 | /// - deletedObject: An HKDeletedObject containing data to be copied to the new External Object.
38 | /// - converter: An instance of a custom converter class.
39 | /// - Returns: A new object conforming to the HDSExternalObjectProtocol populated with the data from the HKDeletedObject or nil if the HKDeletedObject cannot be processed.
40 | static func externalObject(deletedObject: HKDeletedObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol?
41 |
42 | /// Updates the External Object with data from the HKObject.
43 | ///
44 | /// - Parameter object: An HKObject containing data to be copied to the External Object.
45 | func update(with object: HKObject)
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/Proxies/HDSStoreProxy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HDSStoreProxy.swift
3 | // HealthDataSync
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 | import HealthKit
10 |
11 | public class HDSStoreProxy : HDSStoreProxyProtocol
12 | {
13 | private var store: HKHealthStore
14 |
15 | public init(store: HKHealthStore)
16 | {
17 | self.store = store;
18 | }
19 |
20 | public func isHealthDataAvailable() -> Bool
21 | {
22 | return HKHealthStore.isHealthDataAvailable()
23 | }
24 |
25 | public func authorizationStatus(for type: HKObjectType) -> HKAuthorizationStatus
26 | {
27 | return self.store.authorizationStatus(for: type)
28 | }
29 |
30 | public func requestAuthorization(toShare typesToShare: Set?, read typesToRead: Set?, completion: @escaping (Bool, Error?) -> Swift.Void)
31 | {
32 | self.store.requestAuthorization(toShare: typesToShare, read: typesToRead, completion: completion)
33 | }
34 |
35 | public func execute(_ query: HKQuery)
36 | {
37 | self.store.execute(query)
38 | }
39 |
40 | public func stop(_ query: HKQuery)
41 | {
42 | self.store.stop(query)
43 | }
44 |
45 | public func enableBackgroundDelivery(for type: HKObjectType, frequency: HKUpdateFrequency, withCompletion completion: @escaping (Bool, Error?) -> Swift.Void)
46 | {
47 | #if os(iOS)
48 | self.store.enableBackgroundDelivery(for: type, frequency: frequency, withCompletion: completion)
49 | #else
50 | completion(false, HealthKitError.notSupported)
51 | #endif
52 | }
53 |
54 | public func disableBackgroundDelivery(for type: HKObjectType, withCompletion completion: @escaping (Bool, Error?) -> Swift.Void)
55 | {
56 | #if os(iOS)
57 | self.store.disableBackgroundDelivery(for: type, withCompletion: completion)
58 | #else
59 | completion(false, HealthKitError.notSupported)
60 | #endif
61 | }
62 |
63 | public func disableAllBackgroundDelivery(completion: @escaping (Bool, Error?) -> Swift.Void)
64 | {
65 | #if os(iOS)
66 | self.store.disableAllBackgroundDelivery(completion: completion)
67 | #else
68 | completion(false, HealthKitError.notSupported)
69 | #endif
70 | }
71 |
72 | public func preferredUnits(for quantityTypes: Set, completion: @escaping ([HKQuantityType : HKUnit], Error?) -> Void)
73 | {
74 | self.store.preferredUnits(for: quantityTypes, completion: completion)
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Security
4 |
5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
6 |
7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below.
8 |
9 | ## Reporting Security Issues
10 |
11 | **Please do not report security vulnerabilities through public GitHub issues.**
12 |
13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report).
14 |
15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).
16 |
17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
18 |
19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
20 |
21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
22 | * Full paths of source file(s) related to the manifestation of the issue
23 | * The location of the affected source code (tag/branch/commit or direct URL)
24 | * Any special configuration required to reproduce the issue
25 | * Step-by-step instructions to reproduce the issue
26 | * Proof-of-concept or exploit code (if possible)
27 | * Impact of the issue, including how an attacker might exploit the issue
28 |
29 | This information will help us triage your report more quickly.
30 |
31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs.
32 |
33 | ## Preferred Languages
34 |
35 | We prefer all communications to be in English.
36 |
37 | ## Policy
38 |
39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).
40 |
41 |
42 |
--------------------------------------------------------------------------------
/HealthDataSync.xcodeproj/xcshareddata/xcschemes/HealthDataSync_Tests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
42 |
48 |
49 |
50 |
51 |
52 |
62 |
63 |
69 |
70 |
76 |
77 |
78 |
79 |
81 |
82 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/Tests/HDSPermissionsManagerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HDSPermissionsManagerTests.swift
3 | // HealthDataSync_Tests
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 | import HealthKit
10 | import Quick
11 | import Nimble
12 |
13 | class HDSPermissionsManagerSpec: QuickSpec {
14 | override func spec() {
15 | describe("HDSPermissionsManager") {
16 | context("authorizeHealthKit is called") {
17 | context("when health kit is not available on the device") {
18 | let test = testObjects()
19 | test.store.isHealthDataAvailableValue = false
20 | waitUntil { completed in
21 | test.permissionsManager.authorizeHealthKit { (success, error) in
22 | it("fails with the expected error") {
23 | expect(success).to(beFalse())
24 | expect(error).to(matchError(HDSError.unavailable))
25 | }
26 | completed()
27 | }
28 | }
29 | }
30 | context("when readTypes and shareTypes is not set") {
31 | let test = testObjects()
32 | waitUntil { completed in
33 | test.permissionsManager.authorizeHealthKit { (success, error) in
34 | it("fails with the expected error") {
35 | expect(success).to(beFalse())
36 | expect(error).to(matchError(HDSError.noSpecifiedTypes))
37 | }
38 | completed()
39 | }
40 | }
41 | }
42 | context("when readTypes and shareTypes are set") {
43 | let test = testObjects()
44 | let type = HKObjectType.quantityType(forIdentifier: .heartRate)!
45 | let type2 = HKObjectType.quantityType(forIdentifier: .stepCount)!
46 | let type3 = HKObjectType.quantityType(forIdentifier: .bloodGlucose)!
47 | test.permissionsManager.readTypes = [type, type2, type3]
48 | test.permissionsManager.shareTypes = [type, type2, type3]
49 | test.store.requestAuthorizationCompletions.append((true, nil))
50 | waitUntil { completed in
51 | test.permissionsManager.authorizeHealthKit { (success, error) in
52 | it("completes successfully") {
53 | expect(success).to(beTrue())
54 | }
55 | it("does note return an error") {
56 | expect(error).to(beNil())
57 | }
58 | it("calls the store with the expected types") {
59 | expect(test.store.requestAuthorizationParams.count) == 1
60 | expect(test.store.requestAuthorizationParams[0].toShare?.count) == 3
61 | expect(test.store.requestAuthorizationParams[0].read?.count) == 3
62 | }
63 | completed()
64 | }
65 | }
66 | }
67 | }
68 | }
69 | }
70 |
71 | private func testObjects() -> (store: MockStore, permissionsManager: HDSPermissionsManager) {
72 | let mockStore = MockStore()
73 | let permissionsManager = HDSPermissionsManager(store: mockStore)
74 | return (mockStore, permissionsManager)
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Tests/MockStore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockStore.swift
3 | // HealthDataSync_Tests
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 | import HealthKit
10 |
11 | public class MockStore : HDSStoreProxyProtocol {
12 | public var isHealthDataAvailableValue = true
13 | public var isHealthDataAvailableCount = 0
14 | public var authorizationStatusValue = HKAuthorizationStatus.notDetermined
15 | public var authorizationStatusParams = [HKObjectType]()
16 | public var requestAuthorizationParams = [(toShare: Set?, read: Set?)]()
17 | public var requestAuthorizationCompletions = [(success: Bool, error: Error?)]()
18 | public var executeParams = [HKQuery]()
19 | public var stopParams = [HKQuery]()
20 | public var enableBackgroundDeliveryParams = [(HKObjectType, HKUpdateFrequency)]()
21 | public var enableBackgroundDeliveryCompletions = [(success: Bool, error: Error?)]()
22 | public var disableBackgroundDeliveryParams = [HKObjectType]()
23 | public var disableBackgroundDeliveryCompletions = [(success: Bool, error: Error?)]()
24 | public var disableAllBackgroundDeliveryCount = 0
25 | public var disableAllBackgroundDeliveryCompletions = [(success: Bool, error: Error?)]()
26 | public var preferredUnitsParams = [Set]()
27 | public var preferredUnitsCompletions = [(dict: [HKQuantityType : HKUnit], error: Error?)]()
28 |
29 | public func reset() {
30 | isHealthDataAvailableValue = true
31 | isHealthDataAvailableCount = 0
32 | authorizationStatusValue = HKAuthorizationStatus.notDetermined
33 | authorizationStatusParams.removeAll()
34 | requestAuthorizationParams.removeAll()
35 | requestAuthorizationCompletions.removeAll()
36 | executeParams.removeAll()
37 | stopParams.removeAll()
38 | enableBackgroundDeliveryParams.removeAll()
39 | enableBackgroundDeliveryCompletions.removeAll()
40 | disableBackgroundDeliveryParams.removeAll()
41 | disableBackgroundDeliveryCompletions.removeAll()
42 | disableAllBackgroundDeliveryCount = 0
43 | disableAllBackgroundDeliveryCompletions.removeAll()
44 | preferredUnitsParams.removeAll()
45 | preferredUnitsCompletions.removeAll()
46 | }
47 |
48 | public func isHealthDataAvailable() -> Bool {
49 | isHealthDataAvailableCount += 1
50 | return isHealthDataAvailableValue
51 | }
52 |
53 | public func authorizationStatus(for type: HKObjectType) -> HKAuthorizationStatus {
54 | authorizationStatusParams.append(type)
55 | return authorizationStatusValue
56 | }
57 |
58 | public func requestAuthorization(toShare typesToShare: Set?, read typesToRead: Set?, completion: @escaping (Bool, Error?) -> Void) {
59 | requestAuthorizationParams.append((typesToShare, typesToRead))
60 | let comp = requestAuthorizationCompletions.removeFirst()
61 | completion(comp.success, comp.error)
62 | }
63 |
64 | public func execute(_ query: HKQuery) {
65 | executeParams.append(query)
66 | }
67 |
68 | public func stop(_ query: HKQuery) {
69 | stopParams.append(query)
70 | }
71 |
72 | public func enableBackgroundDelivery(for type: HKObjectType, frequency: HKUpdateFrequency, withCompletion completion: @escaping (Bool, Error?) -> Void) {
73 | enableBackgroundDeliveryParams.append((type, frequency))
74 | let comp = enableBackgroundDeliveryCompletions.removeFirst()
75 | completion(comp.success, comp.error)
76 | }
77 |
78 | public func disableBackgroundDelivery(for type: HKObjectType, withCompletion completion: @escaping (Bool, Error?) -> Void) {
79 | disableBackgroundDeliveryParams.append(type)
80 | let comp = disableBackgroundDeliveryCompletions.removeFirst()
81 | completion(comp.success, comp.error)
82 | }
83 |
84 | public func disableAllBackgroundDelivery(completion: @escaping (Bool, Error?) -> Void) {
85 | let comp = disableAllBackgroundDeliveryCompletions.removeFirst()
86 | completion(comp.success, comp.error)
87 | }
88 |
89 | public func preferredUnits(for quantityTypes: Set, completion: @escaping ([HKQuantityType : HKUnit], Error?) -> Void) {
90 | preferredUnitsParams.append(quantityTypes)
91 |
92 | if preferredUnitsCompletions.count > 0 {
93 | let comp = preferredUnitsCompletions.removeFirst()
94 | completion(comp.dict, comp.error)
95 | return
96 | }
97 |
98 | var dict = [HKQuantityType : HKUnit]()
99 | for type in quantityTypes {
100 | dict[type] = HKUnit.init(from: "count")
101 | }
102 |
103 | completion(dict, nil)
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/Sources/HDSManagerProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HDSManagerProtocol.swift
3 | // HealthDataSync
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | #if os(iOS)
9 |
10 | import Foundation
11 | import HealthKit
12 |
13 | public protocol HDSManagerProtocol
14 | {
15 | /// An array of all HDSQueryObservers that have been added to the manager. This includes observers created by calling addObjectTypes() and addSynchronizers()
16 | var allObservers: [HDSQueryObserver] { get }
17 |
18 | /// An object conforming to the HDSQueryObserverDelegate protocol that will handle delegate methods for ALL observers in the allObservers array. Default value is nil.
19 | var observerDelegate: HDSQueryObserverDelegate? { get set }
20 |
21 | /// An object conforming to HDSConverterProtocol that can be used in the conversion of HKObjects to HDSExternalObjectProtocol types.
22 | var converter: HDSConverterProtocol? { get set }
23 |
24 | /// Begins the user authorization process for allowing access to HealthKit data. Calling this method may envoke a system UI if the user has not previousy given permission for ALL data types managed by the observers the allObservers array.
25 | /// - Note: Calling this method asks HealthKit to authorize access for all HealthKit data types managed by the observers in the allObservers array.
26 | ///
27 | /// - Parameter completion: Called after the permissions process has completed.
28 | /// - Parameter success: A boolean representing whether or not the process completed successfully.
29 | /// - Parameter error: An error providing information on the failure, or nil if the process completes successfully.
30 | /// - Returns: nil
31 | func requestPermissionsForAllObservers(completion:@escaping (_ success: Bool, _ error: Error?) -> Void)
32 |
33 | /// Begins the user authorization process for allowing access to HealthKit data managed by a given Observer. Calling this method may envoke a system UI if the user has not previousy given permission for ALL data types managed by the observers.
34 | /// - Note: HDSQueryObservers do not need to be added to the allObservers collection to use this method.
35 | ///
36 | /// - Parameter observers: An array of HDSQueryObservers.
37 | /// - Parameter completion: Called after the permissions process has completed.
38 | /// - Parameter success: A boolean representing whether or not the process completed successfully.
39 | /// - Parameter error: An error providing information on the failure, or nil if the process completes successfully.
40 | /// - Returns: nil
41 | func requestPermissions(with observers: [HDSQueryObserver], _ completion:@escaping (_ success: Bool, _ error: Error?) -> Void)
42 |
43 | /// Creates a new HDSQueryObserver for each type in the objectTypes array and adds it to the allObservers array.
44 | ///
45 | /// - Parameters:
46 | /// - objectTypes: an array of HDSExternalObjectProtocol Types
47 | /// - externalStore: An object conforming to HDSExternalStoreProtocol - The external store instance will be used to synchronize all types in the objectTypes array.
48 | /// - Returns: nil
49 | func addObjectTypes(_ objectTypes: [HDSExternalObjectProtocol.Type], externalStore: HDSExternalStoreProtocol)
50 |
51 | /// Creates a new HDSQueryObserver for each synchronizer in the synchronizers array and adds it to the allObservers array.
52 | ///
53 | /// - Parameters:
54 | /// - synchronizers: an array of HDSObjectSynchronizerProtocol objects
55 | /// - Returns: nil
56 | func addSynchronizers(_ synchronizers: [HDSObjectSynchronizerProtocol])
57 |
58 | /// Executes the observer queries for observers in the allObservers array.
59 | ///
60 | /// - Returns: nil
61 | @available(watchOS, unavailable, message: "HDSQueryObservers cannot receive background deliveries on watchOS. Use call exectue() on individual observers instead.")
62 | func startObserving()
63 |
64 | /// Stops the observer queries for observers in the allObservers array.
65 | /// - Note: Stopping the observer queries will remove any persisted data stored by an observer (lastSuccessfulExecutionDate, query anchors, and predicates)
66 | ///
67 | /// - Returns: nil
68 | @available(watchOS, unavailable, message: "HDSQueryObservers cannot receive background deliveries on watchOS. Use call exectue() on individual observers instead.")
69 | func stopObserving()
70 |
71 | /// Provides a collection of HKSources for each HDSQueryObserver provided in the observers parameter.
72 | /// - Note: HDSQueryObservers do not need to be added to the allObservers collection to use this method.
73 | ///
74 | /// - Parameters:
75 | /// - observers: An array of HDSQueryObserver objects.
76 | /// - completion: Called after the process completes. The completion will provide a dictionary mapping each observer with their sources and an array of errors (if an error occurs).
77 | func sources(for observers: [HDSQueryObserver], completion: @escaping ([HDSQueryObserver : Set], [Error]?) -> Void)
78 | }
79 |
80 | #endif
81 |
--------------------------------------------------------------------------------
/Sources/Proxies/HDSStoreProxyProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HDSStoreProxyProtocol.swift
3 | // HealthDataSync
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 | import HealthKit
10 |
11 | @available(iOS 8.0, watchOS 2.0, *)
12 | public protocol HDSStoreProxyProtocol
13 | {
14 | /*!
15 | @method isHealthDataAvailable
16 | @abstract Returns YES if HealthKit is supported on the device.
17 | @discussion HealthKit is not supported on all iOS devices. Using HKHealthStore APIs on devices which are not
18 | supported will result in errors with the HKErrorHealthDataUnavailable code. Call isHealthDataAvailable
19 | before attempting to use other parts of the framework.
20 | */
21 | func isHealthDataAvailable() -> Bool
22 |
23 | /*!
24 | @method authorizationStatusForType:
25 | @abstract Returns the application's authorization status for the given object type.
26 | */
27 | func authorizationStatus(for type: HKObjectType) -> HKAuthorizationStatus
28 |
29 | /*!
30 | @method requestAuthorizationToShareTypes:readTypes:completion:
31 | @abstract Prompts the user to authorize the application for reading and saving objects of the given types.
32 | @discussion Before attempting to execute queries or save objects, the application should first request authorization
33 | from the user to read and share every type of object for which the application may require access.
34 |
35 | The request is performed asynchronously and its completion will be executed on an arbitrary background
36 | queue after the user has responded. If the user has already chosen whether to grant the application
37 | access to all of the types provided, then the completion will be called without prompting the user.
38 | The success parameter of the completion indicates whether prompting the user, if necessary, completed
39 | successfully and was not cancelled by the user. It does NOT indicate whether the application was
40 | granted authorization.
41 | */
42 | func requestAuthorization(toShare typesToShare: Set?, read typesToRead: Set?, completion: @escaping (Bool, Error?) -> Swift.Void)
43 |
44 | /*!
45 | @method executeQuery:
46 | @abstract Begins executing the given query.
47 | @discussion After executing a query, the completion, update, and/or results handlers of that query will be invoked
48 | asynchronously on an arbitrary background queue as results become available. Errors that prevent a
49 | query from executing will be delivered to one of the query's handlers. Which handler the error will be
50 | delivered to is defined by the HKQuery subclass.
51 |
52 | Each HKQuery instance may only be executed once and calling this method with a currently executing query
53 | or one that was previously executed will result in an exception.
54 |
55 | If a query would retrieve objects with an HKObjectType property, then the application must request
56 | authorization to access objects of that type before executing the query.
57 | */
58 | func execute(_ query: HKQuery)
59 |
60 | /*!
61 | @method stopQuery:
62 | @abstract Stops a query that is executing from continuing to run.
63 | @discussion Calling this method will prevent the handlers of the query from being invoked in the future. If the
64 | query is already stopped, this method does nothing.
65 | */
66 | func stop(_ query: HKQuery)
67 |
68 | /*!
69 | @method enableBackgroundDeliveryForType:frequency:withCompletion:
70 | @abstract This method enables activation of your app when data of the type is recorded at the cadence specified.
71 | @discussion When an app has subscribed to a certain data type it will get activated at the cadence that is specified
72 | with the frequency parameter. The app is still responsible for creating an HKObserverQuery to know which
73 | data types have been updated and the corresponding fetch queries. Note that certain data types (such as
74 | HKQuantityTypeIdentifierStepCount) have a minimum frequency of HKUpdateFrequencyHourly. This is enforced
75 | transparently to the caller.
76 | */
77 | func enableBackgroundDelivery(for type: HKObjectType, frequency: HKUpdateFrequency, withCompletion completion: @escaping (Bool, Error?) -> Swift.Void)
78 |
79 | func disableBackgroundDelivery(for type: HKObjectType, withCompletion completion: @escaping (Bool, Error?) -> Swift.Void)
80 |
81 | func disableAllBackgroundDelivery(completion: @escaping (Bool, Error?) -> Swift.Void)
82 |
83 | /*!
84 | @method preferredUnitsForQuantityTypes:completion:
85 | @abstract Calls the completion with the preferred HKUnits for a given set of HKQuantityTypes.
86 | @discussion A preferred unit is either the unit that the user has chosen in Health for displaying samples of the
87 | given quantity type or the default unit for that type in the current locale of the device. To access the
88 | user's preferences it is necessary to request read or share authorization for the set of HKQuantityTypes
89 | provided. If neither read nor share authorization has been granted to the app, then the default unit for
90 | the locale is provided.
91 |
92 | An error will be returned when preferred units are inaccessible because protected health data is
93 | unavailable or authorization status is not determined for one or more of the provided types.
94 |
95 | The returned dictionary will map HKQuantityType to HKUnit.
96 | */
97 | @available(iOS 8.2, watchOS 2.0, *)
98 | func preferredUnits(for quantityTypes: Set, completion: @escaping ([HKQuantityType : HKUnit], Error?) -> Swift.Void)
99 | }
100 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # HealthDataSync Swift Library
2 |
3 | [](https://microsofthealth.visualstudio.com/Health/_build/latest?definitionId=431&branchName=master)
4 |
5 | HealthDataSync is a Swift library that simplifies and automates the export of HealthKit data to an external store. The most basic usage requires the implementation of just two protocols:
6 |
7 | * HDSExternalStoreProtocol - An object that handles the connection to an external store with basic CRUD functionality.
8 | * HDSExternalObjectProtocol - An implementation of the data transport object used to send data to the external store.
9 |
10 | Once the required protocols have been implemented, the HDSManager can be initialized and used to request permissions from the user to access the specified HealthKit data types defined in the HDSExternalObjectProtocol; start and stop the underlying HKObserverQuery that will observe changes to the specified HealthKit data types and call back to your application when changes occur; and execute the underlying HKAnchoredObjectQuery to fetch changes in the requested HealthKit data and synchronize them with your HDSExternalStoreProtocol implementation.
11 |
12 | ## Installation
13 |
14 | HealthDataSync uses **Swift Package Manager** to manage dependencies. It is recommended that you use Xcode 11 or newer to add HealthDataSync to your project.
15 |
16 | 1. Using Xcode 11 go to File > Swift Packages > Add Package Dependency
17 | 2. Paste the project URL: https://github.com/microsoft/health-data-sync
18 | 3. Click on next and select the project target
19 |
20 | ## Basic Implementation
21 |
22 | ### Implement HDSExternalStoreProtocol
23 |
24 | When changes to observed HealthKit data types occur, the HealthDataSync library will call back to your "external store" to handle the changes. The synchronization process begins by checking if the data has already been synchronized by calling the fetchObjects() function (the call to fetch objects happens regardless of whether the change(s) in HealthKit data were creates, updates, or deletes).
25 |
26 | If the change is NOT a DELETE, if the "external store" returns an array of HDSExternalObjectProtocol objects, the update() function will be called to update the instance(s) of HDSExternalObjectProtocol using the new HealthKit data. If no HDSExternalObjectProtocol objects are returned, the add() function will be called to add a new instance(s) of HealthKit Data.
27 |
28 | For deleted HealthKit data, if the "external store" returns an array of HDSExternalObjectProtocol objects, the delete() function will be called to delete the data stored externally. However, if no HDSExternalObjectProtocol objects are returned, the delete() function will NOT be called.
29 |
30 | ```swift
31 | public protocol HDSExternalStoreProtocol
32 | {
33 | /// Will be called to fetch objects from an external store.
34 | func fetchObjects(with objects: [HDSExternalObjectProtocol], completion: @escaping ([HDSExternalObjectProtocol]? , Error?) -> Void)
35 |
36 | /// Will be called to add new objects to an external store.
37 | func add(objects: [HDSExternalObjectProtocol], completion: @escaping (Error?) -> Void)
38 |
39 | /// Will be called to update existing objects in an external store
40 | func update(objects: [HDSExternalObjectProtocol], completion: @escaping (Error?) -> Void)
41 |
42 | /// Will be called to delete existing objects from an external store
43 | func delete(deletedObjects: [HDSExternalObjectProtocol], completion: @escaping (Error?) -> Void)
44 | }
45 | ```
46 |
47 | ### Implement HDSExternalObjectProtocol
48 |
49 | The HDSExternalObjectProtocol defines an object type that is used to map HealthKit HKObjects to an instance of a DTO used to synchronize with an external store.
50 |
51 | The HDSManager uses the authorizationTypes() to obtain read consent from the user when authorization functions are called and healthKitObjectType() is used to create an HKObserverQuery and an HKAnchoredObjectQuery to monitor changes in HealthKit data.
52 |
53 | During the synchronization process, static functions are called to initialize new HDSExternalObjectProtocol objects with an HKObject or an HKDeletedObject. If the operation is an update, the update function will be called on the instance.
54 |
55 | ```swift
56 | public protocol HDSExternalObjectProtocol
57 | {
58 | /// A unique identifier used to match HealthKit objects and External Objects.
59 | var uuid: UUID { get set }
60 |
61 | /// The HealthKit object type displayed to the user in the authorization UI.
62 | static func authorizationTypes() -> [HKObjectType]?
63 |
64 | /// The HealthKit object type used to query HealthKit.
65 | static func healthKitObjectType() -> HKObjectType?
66 |
67 | /// Creates a new External Object populated with data from the HKObject.=
68 | static func externalObject(object: HKObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol?
69 |
70 | /// Creates a new External Object populated with data from the HKDeletedObject.
71 | static func externalObject(deletedObject: HKDeletedObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol?
72 |
73 | /// Updates the External Object with data from the HKObject.
74 | func update(with object: HKObject)
75 | }
76 | ```
77 |
78 | ### Initialize an instance of HDSManager and create observers using the external object and external store
79 |
80 | For simplicity, the HDSManagerFactory class can be used to create a singleton HDSManager. Once on observer is created by calling addObjectTypes() passing the type of HDSExternalObjectProtocol you implemented and your implementation of HDSExternalStoreProtocol the manager is ready to be used.
81 |
82 | **Important: The HDSManager must be initialized in the AppDelegate application:didFinishLaunchingWithOptions: function to handle changes in HealthKit when the application is not running.**
83 |
84 | ```swift
85 | // Get the HDSManager.
86 | let manager = HDSManagerFactory.manager()
87 |
88 | // Initialize an instance of your external store.
89 | let externalStore = ExampleExternalStore()
90 |
91 | // Create observers by calling addObjectTypes.
92 | manager.addObjectTypes(ExampleExternalObject.self, externalStore: ExampleExternalStore)
93 | ```
94 |
95 | The HDSManager is now ready to be used. The application can ask the user to grant permission for access to HealthKit:
96 |
97 | ```swift
98 | manager.requestPermissionsForAllObservers(completion: { (success, error) in })
99 | ```
100 |
101 | The application can start observing changes in HealthKit:
102 |
103 | ```swift
104 | manager.startObserving()
105 | ```
106 |
107 | ## Contributing
108 |
109 | This project welcomes contributions and suggestions. Most contributions require you to agree to a
110 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
111 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
112 |
113 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide
114 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
115 | provided by the bot. You will only need to do this once across all repos using our CLA.
116 |
117 | There are many other ways to contribute to the HealthDataSync Project.
118 |
119 | * [Submit bugs](https://github.com/microsoft/health-data-sync/issues) and help us verify fixes as they are checked in.
120 | * Review the [source code changes](https://github.com/microsoft/health-data-sync/pulls).
121 | * [Contribute bug fixes](CONTRIBUTING.md).
122 |
123 | See [Contributing to HealthDataSync](CONTRIBUTING.md) for more information.
124 |
125 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
126 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
127 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
128 |
--------------------------------------------------------------------------------
/Sources/HDSManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HDSManager.swift
3 | // HealthDataSync
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | #if os(iOS)
9 |
10 | import Foundation
11 | import HealthKit
12 |
13 | @available(iOS 8.0, watchOS 2.0, *)
14 | open class HDSManager : NSObject, HDSManagerProtocol
15 | {
16 | open var observerDelegate: HDSQueryObserverDelegate?
17 | {
18 | didSet
19 | {
20 | objc_sync_enter(self.synchronizerLockObject)
21 | self.allObservers.forEach
22 | { observer in
23 |
24 | observer.delegate = self.observerDelegate;
25 | }
26 | objc_sync_exit(self.synchronizerLockObject)
27 | }
28 | }
29 | open var converter: HDSConverterProtocol?
30 | {
31 | didSet
32 | {
33 | objc_sync_enter(self.synchronizerLockObject)
34 | self.allObservers.forEach
35 | { observer in
36 |
37 | observer.converter = self.converter;
38 | }
39 | objc_sync_exit(self.synchronizerLockObject)
40 | }
41 | }
42 | public private(set) var allObservers: [HDSQueryObserver]
43 | private let store: HDSStoreProxyProtocol
44 | private let userDefaults: HDSUserDefaultsProxyProtocol
45 | private let permissionsManager: HDSPermissionsManager
46 | private let observerFactory: HDSQueryObserverFactory
47 | private let synchronizerLockObject = NSObject()
48 |
49 | public init(store: HDSStoreProxyProtocol,
50 | userDefaults: HDSUserDefaultsProxyProtocol,
51 | permissionsManager: HDSPermissionsManager,
52 | observerFactory: HDSQueryObserverFactory)
53 | {
54 | self.store = store
55 | self.userDefaults = userDefaults
56 | self.permissionsManager = permissionsManager
57 | self.observerFactory = observerFactory
58 | self.allObservers = [HDSQueryObserver]()
59 |
60 | super.init()
61 | }
62 |
63 | open func requestPermissionsForAllObservers(completion:@escaping (_ success: Bool, _ error: Error?) -> Void)
64 | {
65 | objc_sync_enter(self.synchronizerLockObject)
66 | self.requestPermissions(with: self.allObservers, completion)
67 | objc_sync_exit(self.synchronizerLockObject)
68 | }
69 |
70 | open func requestPermissions(with observers: [HDSQueryObserver], _ completion:@escaping (_ success: Bool, _ error: Error?) -> Void)
71 | {
72 | self.permissionsManager.readTypes = self.authorizationTypes(from: observers)
73 | self.permissionsManager.authorizeHealthKit(completion)
74 | }
75 |
76 | open func addObjectTypes(_ objectTypes: [HDSExternalObjectProtocol.Type], externalStore: HDSExternalStoreProtocol)
77 | {
78 | objc_sync_enter(self.synchronizerLockObject)
79 | let observers = objectTypes.reduce( [HDSQueryObserver](),
80 | { result, objectType in
81 |
82 | var mutableResult = result
83 |
84 | if (!self.hasObserver(for: objectType))
85 | {
86 | let synchronizer = HDSObjectSynchronizer(externalObjectType:objectType, store: self.store, externalStore: externalStore)
87 | let observers = self.observerFactory.observers(with: [synchronizer])
88 | observers.forEach { observer in observer.delegate = self.observerDelegate; }
89 | mutableResult.append(contentsOf: observers)
90 | }
91 |
92 | return mutableResult;
93 | })
94 | self.allObservers.append(contentsOf: observers)
95 | objc_sync_exit(self.synchronizerLockObject)
96 | }
97 |
98 | open func addSynchronizers(_ synchronizers: [HDSObjectSynchronizerProtocol])
99 | {
100 | objc_sync_enter(self.synchronizerLockObject)
101 | let observers = self.observerFactory.observers(with: synchronizers.filter { synchronizer in !self.hasObserver(for: synchronizer.externalObjectType) })
102 | observers.forEach { observer in observer.delegate = self.observerDelegate; }
103 | self.allObservers.append(contentsOf: observers)
104 | objc_sync_exit(self.synchronizerLockObject)
105 | }
106 |
107 | open func startObserving()
108 | {
109 | objc_sync_enter(self.synchronizerLockObject)
110 | self.allObservers.forEach(
111 | { (observer) in
112 |
113 | observer.start()
114 | })
115 | objc_sync_exit(self.synchronizerLockObject)
116 | }
117 |
118 | open func stopObserving()
119 | {
120 | objc_sync_enter(self.synchronizerLockObject)
121 | self.allObservers.forEach(
122 | { (observer) in
123 |
124 | observer.stop()
125 | })
126 | objc_sync_exit(self.synchronizerLockObject)
127 | }
128 |
129 | open func sources(for observers: [HDSQueryObserver], completion:@escaping ([HDSQueryObserver : Set], [Error]?) -> Void)
130 | {
131 | let dispatchGroup = DispatchGroup()
132 | let lockObject = NSObject()
133 | var sourcesDictionary = [HDSQueryObserver : Set]()
134 | var errors = [Error]()
135 |
136 | objc_sync_enter(self.synchronizerLockObject)
137 | observers.forEach
138 | { (observer) in
139 |
140 | if let type = observer.externalObjectType.healthKitObjectType() as? HKSampleType
141 | {
142 | dispatchGroup.enter()
143 |
144 | let query = HKSourceQuery(sampleType: type,
145 | samplePredicate: nil,
146 | completionHandler:
147 | { (query, sources, error) in
148 |
149 | if (error != nil)
150 | {
151 | objc_sync_enter(lockObject)
152 | errors.append(error!)
153 | objc_sync_exit(lockObject)
154 | }
155 | else if (sources != nil)
156 | {
157 | objc_sync_enter(lockObject)
158 | sourcesDictionary[observer] = sources
159 | objc_sync_exit(lockObject)
160 | }
161 |
162 | dispatchGroup.leave()
163 | })
164 |
165 | self.store.execute(query)
166 | }
167 | }
168 | objc_sync_exit(self.synchronizerLockObject)
169 |
170 | dispatchGroup.notify(queue: DispatchQueue.global())
171 | {
172 | completion(sourcesDictionary, errors)
173 | }
174 | }
175 |
176 | private func authorizationTypes(from observers: [HDSQueryObserver]) -> [HKObjectType]
177 | {
178 | var types = [HKObjectType]()
179 |
180 | objc_sync_enter(self.synchronizerLockObject)
181 | observers.forEach
182 | { (observer) in
183 |
184 | observer.externalObjectType.authorizationTypes()?.forEach(
185 | { (type) in
186 |
187 | types.append(type)
188 | })
189 | }
190 | objc_sync_exit(self.synchronizerLockObject)
191 |
192 | return types
193 | }
194 |
195 | // Helpers
196 |
197 | private func hasObserver(for objectType: HDSExternalObjectProtocol.Type) -> Bool
198 | {
199 | return self.allObservers.contains(where: { return objectType.authorizationTypes() == $0.externalObjectType.authorizationTypes() && objectType.healthKitObjectType() == $0.externalObjectType.healthKitObjectType() })
200 | }
201 | }
202 |
203 | #endif
204 |
--------------------------------------------------------------------------------
/Sources/Synchronizers/HDSObjectSynchronizer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HDSObjectSynchronizer.swift
3 | // HealthDataSync
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 | import HealthKit
10 |
11 | open class HDSObjectSynchronizer: HDSObjectSynchronizerProtocol
12 | {
13 | open private(set) var externalObjectType: HDSExternalObjectProtocol.Type
14 | open var unitsDictionary: [HKQuantityType : HKUnit]?
15 | public var converter: HDSConverterProtocol?
16 | private let store: HDSStoreProxyProtocol
17 | private let externalObjectStore: HDSExternalStoreProtocol
18 |
19 | public init(externalObjectType: HDSExternalObjectProtocol.Type,
20 | store: HDSStoreProxyProtocol,
21 | externalStore: HDSExternalStoreProtocol)
22 | {
23 | self.externalObjectType = externalObjectType
24 | self.store = store
25 | self.externalObjectStore = externalStore
26 | }
27 |
28 | open func synchronize(objects: [HKObject]?, deletedObjects: [HKDeletedObject]?, completion: @escaping (Error?) -> Void)
29 | {
30 | // Give subclasses an opportunity to execute code before syncing
31 | self.willSyncronize(objects: objects, deletedObjects: deletedObjects)
32 | {
33 | // First process any deleted objects
34 | self.delete(deletedObjects: deletedObjects)
35 | { (error) in
36 |
37 | guard error == nil else
38 | {
39 | completion(error)
40 | return
41 | }
42 |
43 | // Next process any updates and/or creates
44 | self.createOrUpdate(objects: objects, completion:
45 | { (error) in
46 |
47 | // Give subclasses an opportunity to execute code after syncing
48 | self.willFinishSyncronizing(after:
49 | {
50 | completion(error)
51 | })
52 | })
53 | }
54 | }
55 | }
56 |
57 | private func delete(deletedObjects: [HKDeletedObject]?, completion: @escaping (Error?) -> Void)
58 | {
59 | guard let deleted = deletedObjects,
60 | deleted.count > 0 else {
61 | completion(nil)
62 | return
63 | }
64 |
65 | // Create an array of external objects from the deleted objects
66 | let externalObjects = deleted.compactMap({ self.externalObjectType.externalObject(deletedObject: $0, converter: self.converter) })
67 |
68 | // Fetch the objects from the external store
69 | self.fetchExternalObjects(with: externalObjects)
70 | { (externalObjects, error) in
71 |
72 | if (externalObjects != nil && !externalObjects!.isEmpty)
73 | {
74 | // Delete the objects from the external store
75 | self.externalObjectStore.delete(deletedObjects: externalObjects!, completion: completion)
76 | }
77 | else
78 | {
79 | completion(error)
80 | }
81 | }
82 | }
83 |
84 | private func createOrUpdate(objects: [HKObject]?, completion: @escaping (Error?) -> Void)
85 | {
86 | guard let createOrUpdateObjects = objects,
87 | createOrUpdateObjects.count > 0 else {
88 | completion(nil)
89 | return
90 | }
91 |
92 | // Create an array of external objects from the deleted objects
93 | let externalObjects = createOrUpdateObjects.compactMap({ self.externalObjectType.externalObject(object: $0, converter: self.converter) })
94 |
95 | // Fetch the objects from the external store
96 | self.fetchExternalObjects(with: externalObjects)
97 | { (externalObjects, error) in
98 |
99 | guard error == nil else
100 | {
101 | completion(error)
102 | return
103 | }
104 |
105 | if let types = self.externalObjectType.authorizationTypes() as? [HKQuantityType]
106 | {
107 | self.store.preferredUnits(for: Set(types), completion:
108 | { (unitsDictionary, error) in
109 |
110 | // Try to fetch the preferred units for objects.
111 | self.unitsDictionary = unitsDictionary
112 | self.applyUpdates(objects: createOrUpdateObjects, existingObjects: externalObjects, completion: completion)
113 | })
114 | }
115 | else
116 | {
117 | self.applyUpdates(objects: createOrUpdateObjects, existingObjects: externalObjects, completion: completion)
118 | }
119 | }
120 | }
121 |
122 | private func applyUpdates(objects: [HKObject], existingObjects: [HDSExternalObjectProtocol]?, completion: @escaping (Error?) -> Void)
123 | {
124 | if let externalObjects = self.externalObjects(from: objects, existingObjects: existingObjects)
125 | {
126 | if (externalObjects.add.count > 0)
127 | {
128 | self.externalObjectStore.add(objects: externalObjects.add, completion:
129 | { error in
130 |
131 | guard error == nil else
132 | {
133 | completion(error)
134 | return
135 | }
136 |
137 | if (externalObjects.update.count > 0)
138 | {
139 | self.externalObjectStore.update(objects: externalObjects.update, completion: completion)
140 | }
141 | else
142 | {
143 | completion(error)
144 | }
145 | })
146 | return
147 | }
148 | else if (externalObjects.update.count > 0)
149 | {
150 | self.externalObjectStore.update(objects: externalObjects.update, completion: completion)
151 | return
152 | }
153 | }
154 |
155 | completion(nil)
156 | }
157 |
158 | private func fetchExternalObjects(with objects: [HDSExternalObjectProtocol]?, completion: @escaping ([HDSExternalObjectProtocol]?, Error?) -> Void)
159 | {
160 | if (objects != nil && !objects!.isEmpty)
161 | {
162 | self.externalObjectStore.fetchObjects(with: objects!, completion: completion)
163 | return
164 | }
165 |
166 | completion(nil, nil)
167 | }
168 |
169 | private func externalObjects(from objects: [HKObject], existingObjects: [HDSExternalObjectProtocol]?) -> (add: [HDSExternalObjectProtocol], update: [HDSExternalObjectProtocol])?
170 | {
171 | var add = [HDSExternalObjectProtocol]()
172 | var update = [HDSExternalObjectProtocol]()
173 |
174 | objects.forEach(
175 | { (object) in
176 |
177 | var isUpdate = false
178 |
179 | existingObjects?.forEach(
180 | { (existingObject) in
181 |
182 | if (object.uuid == existingObject.uuid)
183 | {
184 | isUpdate = true
185 | existingObject.update(with: object)
186 | update.append(existingObject)
187 |
188 | return
189 | }
190 | })
191 |
192 | // Create a new external object
193 | if (!isUpdate)
194 | {
195 | if let newExternalObject = self.externalObjectType.externalObject(object: object, converter: self.converter)
196 | {
197 | add.append(newExternalObject)
198 | }
199 | }
200 | })
201 |
202 | return (add.count < 1 && update.count < 1) ? nil : (add, update)
203 | }
204 |
205 | // MARK: Subclass overrides
206 |
207 | // Subclasses can override to execute code to BEFORE requests to the external store are made.
208 | open func willSyncronize(objects: [HKObject]?, deletedObjects: [HKDeletedObject]?, completion:@escaping () -> Void)
209 | {
210 | completion()
211 | }
212 |
213 | // Subclasses can override to execute code to AFTER requests to the external store are made.
214 | open func willFinishSyncronizing(after completion:@escaping () -> Void)
215 | {
216 | completion()
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/Tests/HDSQueryObserverTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HDSQueryObserverTests.swift
3 | // HealthDataSync_Tests
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 | import HealthKit
10 | import Quick
11 | import Nimble
12 |
13 | class HDSQueryObserverSpec: QuickSpec {
14 | override func spec() {
15 | describe("HDSQueryObserver") {
16 | context("start is called") {
17 | it ("checks the availability of health kit on the device") {
18 | let test = self.testObjects()
19 | test.queryObserver.start()
20 | expect(test.store.isHealthDataAvailableCount) == 1
21 | }
22 | it ("checks if the user has authorized the observer type") {
23 | let test = self.testObjects()
24 | test.queryObserver.start()
25 | expect(test.store.authorizationStatusParams.count) == 1
26 | expect(test.store.authorizationStatusParams[0]).to(equal(HKObjectType.quantityType(forIdentifier: .heartRate)))
27 | }
28 | context("health kit is unavailable") {
29 | let test = self.testObjects()
30 | test.store.isHealthDataAvailableValue = false
31 | test.queryObserver.start()
32 | it("does not enable background delivery") {
33 | expect(test.store.enableBackgroundDeliveryParams.count) == 0
34 | }
35 | it("does not execute the observer query") {
36 | expect(test.store.executeParams.count) == 0
37 | }
38 | it("does not set isObserving to true") {
39 | expect(test.queryObserver.isObserving).to(beFalse())
40 | }
41 | }
42 | context("the observer type is not determined") {
43 | let test = self.testObjects()
44 | test.queryObserver.start()
45 | it("does not enable background delivery") {
46 | expect(test.store.enableBackgroundDeliveryParams.count) == 0
47 | }
48 | it("does not execute the observer query") {
49 | expect(test.store.executeParams.count) == 0
50 | }
51 | it("does not set isObserving to true") {
52 | expect(test.queryObserver.isObserving).to(beFalse())
53 | }
54 | }
55 | context("the observer type is not authorized") {
56 | context("enabling background delivery fails") {
57 | let test = self.testObjects()
58 | test.store.authorizationStatusValue = .sharingDenied
59 | test.store.enableBackgroundDeliveryCompletions.append((false, MockError.enableBackgroundDeliveryFailure))
60 | test.userDefaults.dataReturns.append(nil)
61 | test.queryObserver.start()
62 | waitForCondition(object: test.queryObserver, format: "isObserving == false")
63 | it("sets isObserving to false") {
64 | expect(test.queryObserver.isObserving).to(beFalse())
65 | }
66 | it("attempts to enables background delivery") {
67 | expect(test.store.enableBackgroundDeliveryParams.count) == 1
68 | }
69 | it("does not execute the observer query") {
70 | expect(test.store.executeParams.count) == 0
71 | }
72 | }
73 | context("enabling background delivery succeeds") {
74 | let test = self.testObjects()
75 | test.store.authorizationStatusValue = .sharingDenied
76 | test.store.enableBackgroundDeliveryCompletions.append((true, nil))
77 | test.userDefaults.dataReturns.append(nil)
78 | test.queryObserver.start()
79 | it("enables background delivery") {
80 | expect(test.store.enableBackgroundDeliveryParams.count) == 1
81 | }
82 | it("executes the observer query") {
83 | expect(test.store.executeParams.count) == 1
84 | expect(test.store.executeParams[0].objectType).to(equal(HKObjectType.quantityType(forIdentifier: .heartRate)))
85 | }
86 | it("sets isObserving to true") {
87 | expect(test.queryObserver.isObserving).to(beTrue())
88 | }
89 | }
90 | }
91 | context("the observer type is authorized") {
92 | context("enabling background delivery fails") {
93 | let test = self.testObjects()
94 | test.store.authorizationStatusValue = .sharingAuthorized
95 | test.store.enableBackgroundDeliveryCompletions.append((false, MockError.enableBackgroundDeliveryFailure))
96 | test.userDefaults.dataReturns.append(nil)
97 | test.queryObserver.start()
98 | waitForCondition(object: test.queryObserver, format: "isObserving == false")
99 | it("attempts to enables background delivery") {
100 | expect(test.store.enableBackgroundDeliveryParams.count) == 1
101 | }
102 | it("does not execute the observer query") {
103 | expect(test.store.executeParams.count) == 0
104 | }
105 | it("sets isObserving to true") {
106 | expect(test.queryObserver.isObserving).to(beFalse())
107 | }
108 | }
109 | context("enabling background delivery succeeds") {
110 | let test = self.testObjects()
111 | test.store.authorizationStatusValue = .sharingAuthorized
112 | test.store.enableBackgroundDeliveryCompletions.append((true, nil))
113 | test.userDefaults.dataReturns.append(nil)
114 | test.queryObserver.start()
115 | it("enables background delivery") {
116 | expect(test.store.enableBackgroundDeliveryParams.count) == 1
117 | }
118 | it("executes the observer query") {
119 | expect(test.store.executeParams.count) == 1
120 | expect(test.store.executeParams[0].objectType).to(equal(HKObjectType.quantityType(forIdentifier: .heartRate)))
121 | }
122 | it("sets isObserving to true") {
123 | expect(test.queryObserver.isObserving).to(beTrue())
124 | }
125 | }
126 | }
127 | context("when queryPredicate is set") {
128 | let test = self.testObjects()
129 | test.store.authorizationStatusValue = .sharingAuthorized
130 | let expectedPredicate = HKQuery.predicateForSamples(withStart: Date(), end: nil, options: HKQueryOptions.strictStartDate)
131 | test.store.enableBackgroundDeliveryCompletions.append((true, nil))
132 | test.queryObserver.queryPredicate = expectedPredicate
133 | test.queryObserver.start()
134 | it("executes the observer query using the custom predicate") {
135 | expect(test.store.executeParams.count) == 1
136 | expect(test.store.executeParams[0].objectType).to(equal(HKObjectType.quantityType(forIdentifier: .heartRate)))
137 | expect(test.store.executeParams[0].predicate).to(equal(expectedPredicate))
138 | }
139 | }
140 | }
141 | context("stop is called") {
142 | it ("checks the availability of health kit on the device") {
143 | let test = self.testObjects()
144 | test.queryObserver.start()
145 | expect(test.store.isHealthDataAvailableCount) == 1
146 | }
147 | context("health kit is unavailable") {
148 | let test = self.testObjects()
149 | test.store.authorizationStatusValue = .sharingAuthorized
150 | test.store.enableBackgroundDeliveryCompletions.append((true, nil))
151 | test.userDefaults.dataReturns.append(nil)
152 | test.queryObserver.start()
153 | test.store.isHealthDataAvailableValue = false
154 | test.queryObserver.stop()
155 | it("does not attempt to disable background delivery") {
156 | expect(test.store.disableBackgroundDeliveryParams.count) == 0
157 | }
158 | it("does not attempt to stop the observer query") {
159 | expect(test.store.stopParams.count) == 0
160 | }
161 | it("does not set isObserving to false") {
162 | expect(test.queryObserver.isObserving).to(beTrue())
163 | }
164 | it("does not delete any saved state objects") {
165 | expect(test.userDefaults.removeObjectParams.count) == 0
166 | }
167 | }
168 | context("disabling background delivery fails") {
169 | let test = self.testObjects()
170 | test.store.authorizationStatusValue = .sharingAuthorized
171 | test.store.enableBackgroundDeliveryCompletions.append((true, nil))
172 | test.userDefaults.dataReturns.append(nil)
173 | test.queryObserver.start()
174 | waitForCondition(object: test.queryObserver, format:"isObserving == true")
175 | test.store.disableBackgroundDeliveryCompletions.append((false, MockError.disableBackgroundDeliveryFailure))
176 | test.queryObserver.stop()
177 | it("calls disable background delivery") {
178 | expect(test.store.disableBackgroundDeliveryParams.count) == 1
179 | }
180 | it("calls stop with the observer query") {
181 | expect(test.store.stopParams.count) == 1
182 | }
183 | it("sets isObserving to false") {
184 | expect(test.queryObserver.isObserving).to(beFalse())
185 | }
186 | it("deletes the saved state objects") {
187 | expect(test.userDefaults.removeObjectParams.count) == 3
188 | expect(test.userDefaults.removeObjectParams).to(contain(["HKQuantityTypeIdentifierHeartRate-Predicate", "HKQuantityTypeIdentifierHeartRate-Anchor", "HKQuantityTypeIdentifierHeartRate-Last-Execution-Date"]))
189 | }
190 | }
191 | context("disabling background delivery succeeds") {
192 | let test = self.testObjects()
193 | test.store.authorizationStatusValue = .sharingAuthorized
194 | test.store.enableBackgroundDeliveryCompletions.append((true, nil))
195 | test.userDefaults.dataReturns.append(nil)
196 | test.queryObserver.start()
197 | waitForCondition(object: test.queryObserver, format:"isObserving == true")
198 | test.store.disableBackgroundDeliveryCompletions.append((true, nil))
199 | test.queryObserver.stop()
200 | it("calls disable background delivery") {
201 | expect(test.store.disableBackgroundDeliveryParams.count) == 1
202 | }
203 | it("calls stop with the observer query") {
204 | expect(test.store.stopParams.count) == 1
205 | }
206 | it("sets isObserving to false") {
207 | expect(test.queryObserver.isObserving).to(beFalse())
208 | }
209 | it("deletes the saved state objects") {
210 | expect(test.userDefaults.removeObjectParams.count) == 3
211 | expect(test.userDefaults.removeObjectParams).to(contain(["HKQuantityTypeIdentifierHeartRate-Predicate", "HKQuantityTypeIdentifierHeartRate-Anchor", "HKQuantityTypeIdentifierHeartRate-Last-Execution-Date"]))
212 | }
213 | }
214 | }
215 | context("execute is called") {
216 | let test = self.testObjects()
217 | test.queryObserver.execute(completion: { (success, error) in })
218 | it("executes the anchored object query") {
219 | expect(test.store.executeParams.count) == 1
220 | expect(test.store.executeParams[0]).to(beAKindOf(HKAnchoredObjectQuery.self))
221 | expect(test.store.executeParams[0].objectType).to(equal(HKObjectType.quantityType(forIdentifier: .heartRate)))
222 | }
223 | context("when queryPredicate is set") {
224 | let test = self.testObjects()
225 | let expectedPredicate = HKQuery.predicateForSamples(withStart: Date(), end: nil, options: HKQueryOptions.strictStartDate)
226 | test.queryObserver.queryPredicate = expectedPredicate
227 | test.queryObserver.execute(completion: { (success, error) in })
228 | it("executes the anchored object query") {
229 | expect(test.store.executeParams.count) == 1
230 | expect(test.store.executeParams[0]).to(beAKindOf(HKAnchoredObjectQuery.self))
231 | expect(test.store.executeParams[0].objectType).to(equal(HKObjectType.quantityType(forIdentifier: .heartRate)))
232 | }
233 | it("includes the custom predicate") {
234 | expect(test.store.executeParams[0].predicate).to(equal(expectedPredicate))
235 | }
236 | }
237 | context("a delegate is set") {
238 | context("shouldExecute returns false") {
239 | let test = self.testObjects()
240 | let delegate = MockQueryObserverDelegate()
241 | delegate.shouldExecuteCompletions.append(false)
242 | test.queryObserver.delegate = delegate
243 | waitUntil { completed in
244 | test.queryObserver.execute(completion: { (success, error) in
245 | it("completes unsuccessfully") {
246 | expect(success).to(beFalse())
247 | }
248 | it("returns an operation cancelled error") {
249 | expect(error).to(matchError(HDSError.operationCancelled))
250 | }
251 | completed()
252 | })
253 | }
254 | }
255 | context("shouldExecute returns true") {
256 | let test = self.testObjects()
257 | let delegate = MockQueryObserverDelegate()
258 | delegate.shouldExecuteCompletions.append(true)
259 | delegate.batchSizeReturns.append(100)
260 | test.queryObserver.delegate = delegate
261 | test.queryObserver.execute(completion: { (success, error) in })
262 | it("executes the anchored object query") {
263 | expect(test.store.executeParams.count) == 1
264 | expect(test.store.executeParams[0]).to(beAKindOf(HKAnchoredObjectQuery.self))
265 | expect(test.store.executeParams[0].objectType).to(equal(HKObjectType.quantityType(forIdentifier: .heartRate)))
266 | }
267 | it("calls batchSize on the delegate") {
268 | expect(delegate.batchSizeParams.count) == 1
269 | expect(delegate.batchSizeParams[0]) == test.queryObserver
270 | }
271 | }
272 | }
273 | }
274 | context("when when queryPredicate is set") {
275 | let test = self.testObjects()
276 | let expectedPredicate = HKQuery.predicateForSamples(withStart: Date(), end: nil, options: HKQueryOptions.strictStartDate)
277 | test.queryObserver.queryPredicate = expectedPredicate
278 | it("saves the predicate") {
279 | expect(test.userDefaults.setParams.count) == 1
280 | expect(test.userDefaults.setParams[0].defaultName) == "HKQuantityTypeIdentifierHeartRate-Predicate"
281 | }
282 | }
283 | }
284 | }
285 |
286 | private func testObjects() -> (store: MockStore, externalStore: MockExternalStore, userDefaults: MockUserDefaults, synchronizer: MockSynchronizer, queryObserver: HDSQueryObserver) {
287 | let mockStore = MockStore()
288 | let mockExternalStore = MockExternalStore()
289 | let mockUserDefaults = MockUserDefaults()
290 | let mockSynchronizer = MockSynchronizer(externalObjectType: MockExternalObject.self, store: mockStore, externalStore: mockExternalStore)
291 | let queryObserver = HDSQueryObserver(store: mockStore, userDefaultsProxy: mockUserDefaults, synchronizer: mockSynchronizer)
292 | return (mockStore, mockExternalStore, mockUserDefaults, mockSynchronizer, queryObserver)
293 | }
294 | }
295 |
--------------------------------------------------------------------------------
/Sources/HDSQueryObserver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HDSQueryObserver.swift
3 | // HealthDataSync
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | #if os(iOS)
9 |
10 | import Foundation
11 | import HealthKit
12 |
13 | @available(iOS 8.0, watchOS 2.0, *)
14 | public class HDSQueryObserver: NSObject
15 | {
16 | public weak var delegate: HDSQueryObserverDelegate?
17 | public var converter: HDSConverterProtocol? { get { return self.synchronizer.converter } set { self.synchronizer.converter = newValue } }
18 | public var queryPredicate: NSPredicate? { get { return self.predicate() } set { self.savePredicate(newValue) } }
19 | public private(set) var isObserving = false
20 | public var externalObjectType: HDSExternalObjectProtocol.Type { return self.synchronizer.externalObjectType }
21 | public var lastSuccessfulExecutionDate: Date? { get { return self.lastExecutionDate() } }
22 | public var canStartObserving : Bool
23 | {
24 | if let types = self.externalObjectType.authorizationTypes()
25 | {
26 | for type in types
27 | {
28 | if (self.store.authorizationStatus(for: type) == .notDetermined)
29 | {
30 | return false
31 | }
32 | }
33 |
34 | return true
35 | }
36 |
37 | return false
38 | }
39 |
40 | private var handlerCompletions = [(query: HKObserverQuery, completion: HealthKit.HKObserverQueryCompletionHandler)]()
41 | private var handlerLockObject = NSObject()
42 | private var observerQuery: HKObserverQuery?
43 | private var observerLockObject = NSObject()
44 | private let store: HDSStoreProxyProtocol
45 | private let userDefaultsProxy: HDSUserDefaultsProxyProtocol
46 | private var synchronizer: HDSObjectSynchronizerProtocol
47 | private let lastExecutionKeySuffix = "-Last-Execution-Date"
48 | private let anchorKeySuffix = "-Anchor"
49 | private let predicateKeySuffix = "-Predicate"
50 |
51 | public init(store: HDSStoreProxyProtocol,
52 | userDefaultsProxy: HDSUserDefaultsProxyProtocol,
53 | synchronizer: HDSObjectSynchronizerProtocol)
54 | {
55 | self.store = store
56 | self.userDefaultsProxy = userDefaultsProxy
57 | self.synchronizer = synchronizer
58 | }
59 |
60 | /// Starts the observer query and enables background deliveries of HealthKit objects for the observer's authorizationTypes.
61 | @available(watchOS, unavailable, message: "HDSQueryObservers cannot receive background deliveries on watchOS. Use exectue() instead.")
62 | public func start()
63 | {
64 | guard self.store.isHealthDataAvailable() && self.canStartObserving else
65 | {
66 | return
67 | }
68 |
69 | objc_sync_enter(self.observerLockObject)
70 |
71 | defer
72 | {
73 | objc_sync_exit(self.observerLockObject)
74 | }
75 |
76 | if (self.isObserving)
77 | {
78 | return
79 | }
80 |
81 | if let authTypes = self.externalObjectType.authorizationTypes(), let queryType = self.externalObjectType.healthKitObjectType()
82 | {
83 | self.enableBackgroundDelivery(for: authTypes,
84 | frequency: .immediate)
85 | { (success, errors) in
86 |
87 | objc_sync_enter(self.observerLockObject)
88 | self.isObserving = success
89 | objc_sync_exit(self.observerLockObject)
90 |
91 | if (success)
92 | {
93 | print("Starting observer query for " + queryType.debugDescription)
94 | self.store.execute(self.query())
95 | }
96 | }
97 | }
98 | }
99 |
100 | /// Stops the observer query and disables background deliveries of HealthKit objects for the observer's authorizationTypes.
101 | /// All persisted data for the observer will be deleted.
102 | @available(watchOS, unavailable, message: "HDSQueryObservers cannot receive background deliveries on watchOS. Use exectue() instead.")
103 | public func stop()
104 | {
105 | guard self.store.isHealthDataAvailable() else
106 | {
107 | return
108 | }
109 |
110 | objc_sync_enter(self.observerLockObject)
111 |
112 | defer
113 | {
114 | objc_sync_exit(self.observerLockObject)
115 | }
116 |
117 | if (!self.isObserving)
118 | {
119 | return
120 | }
121 |
122 | if let authTypes = self.externalObjectType.authorizationTypes(), let queryType = self.externalObjectType.healthKitObjectType()
123 | {
124 | self.disableBackgroundDelivery(for: authTypes)
125 | { (success, errors) in
126 |
127 | print("Stopping observer query for " + queryType.debugDescription)
128 | self.store.stop(self.query())
129 | self.deleteAnchor()
130 | self.deleteLastExecutionDate()
131 | self.deletePredicate()
132 |
133 | objc_sync_enter(self.observerLockObject)
134 | self.isObserving = false
135 | objc_sync_exit(self.observerLockObject)
136 | }
137 | }
138 | }
139 |
140 |
141 | /// Forces the execution of the observer's query and synchronized data to an external store.
142 | ///
143 | /// - Parameter completion: A closure that is executed with a Bool indicating the success or failure of the operation and an optional error.
144 | /// - Parameter success: A bool indicating whether the operation was successful.
145 | /// - Parameter error: Optional - an error with details about the failure if an operation is unsuccessful.
146 | public func execute(completion: @escaping (_ success: Bool, _ error: Error?) -> Void = { _, _ in })
147 | {
148 | print("Execute called for type \(externalObjectType)")
149 | if let type = self.externalObjectType.healthKitObjectType() as? HKSampleType
150 | {
151 | if let delegate = self.delegate
152 | {
153 | delegate.shouldExecute(for: self)
154 | { shouldExecute in
155 |
156 | if (shouldExecute)
157 | {
158 | self.executeAnchorQuery(type: type, completion: completion)
159 | }
160 | else
161 | {
162 | completion(false, HDSError.operationCancelled)
163 | }
164 | }
165 | }
166 | else
167 | {
168 | self.executeAnchorQuery(type: type, completion: completion)
169 | }
170 | }
171 | else
172 | {
173 | completion(false, HDSError.noSpecifiedTypes)
174 | }
175 | }
176 |
177 | private func query() -> HKObserverQuery
178 | {
179 | if (self.observerQuery != nil)
180 | {
181 | return self.observerQuery!
182 | }
183 |
184 | if let type = self.externalObjectType.healthKitObjectType() as? HKSampleType
185 | {
186 | self.observerQuery = HKObserverQuery(sampleType: type,
187 | predicate: self.queryPredicate,
188 | updateHandler:
189 | { (query, completion, error) in
190 |
191 | guard error == nil else
192 | {
193 | print("The " + self.externalObjectType.healthKitObjectType().debugDescription + " query has returned an error. " + error!.localizedDescription)
194 | return
195 | }
196 |
197 | if (query == self.observerQuery)
198 | {
199 | self.handleObservedQuery(query: query, completion: completion)
200 | }
201 | })
202 | }
203 |
204 | return self.observerQuery!
205 | }
206 |
207 | private func handleObservedQuery(query: HKObserverQuery, completion: @escaping HealthKit.HKObserverQueryCompletionHandler)
208 | {
209 | print("HandleObserverQuery called for type \(externalObjectType)")
210 |
211 | objc_sync_enter(self.handlerLockObject)
212 |
213 | defer
214 | {
215 | objc_sync_exit(self.handlerLockObject)
216 | }
217 |
218 | self.handlerCompletions.append((query, completion))
219 |
220 | if (self.handlerCompletions.count > 1)
221 | {
222 | // A previous observed query is still being processed.
223 | // To avoid executing the same observed query, handling must be performed serially.
224 | print("HDSQueryObserver for type '\(self.externalObjectType)' is busy - execution will be delayed.")
225 | return
226 | }
227 |
228 | if let type = query.objectType as? HKSampleType
229 | {
230 | if let delegate = self.delegate
231 | {
232 | delegate.shouldExecute(for: self)
233 | { shouldExecute in
234 |
235 | if (shouldExecute)
236 | {
237 | self.executeAnchorQuery(type: type)
238 | { (success, error) in
239 | self.completeObservedQuery()
240 | }
241 | }
242 | else
243 | {
244 | self.completeObservedQuery()
245 | }
246 | }
247 | }
248 | else
249 | {
250 | self.executeAnchorQuery(type: type)
251 | { (success, error) in
252 | self.completeObservedQuery()
253 | }
254 | }
255 | }
256 | else
257 | {
258 | completion()
259 | self.completeObservedQuery()
260 | }
261 | }
262 |
263 | private func completeObservedQuery() -> Void
264 | {
265 | objc_sync_enter(self.handlerLockObject)
266 | if (self.handlerCompletions.count > 0)
267 | {
268 | let completion = self.handlerCompletions.remove(at: 0).completion
269 | completion()
270 | }
271 |
272 | if (self.handlerCompletions.count > 0)
273 | {
274 | let parameters = self.handlerCompletions.remove(at: 0)
275 | self.handleObservedQuery(query: parameters.query, completion: parameters.completion)
276 | }
277 | objc_sync_exit(self.handlerLockObject)
278 | }
279 |
280 | private func executeAnchorQuery(type: HKSampleType, completion: @escaping (Bool, Error?) -> Void)
281 | {
282 | let limit = self.delegate?.batchSize(for: self) ?? Constants.defaultBatchSize
283 |
284 | let anchoredQuery = HKAnchoredObjectQuery(type: type,
285 | predicate: self.queryPredicate,
286 | anchor: self.anchor(),
287 | limit: limit)
288 | { (query, objects, deletedObjects, anchor, error) in
289 |
290 | guard error == nil else
291 | {
292 | print("The " + self.externalObjectType.healthKitObjectType().debugDescription + " anchored query has returned an error. " + error!.localizedDescription)
293 | completion(false, error)
294 | self.delegate?.didFinishExecution(for: self, error: error)
295 | return
296 | }
297 |
298 | print("Executing anchor query for \(objects!.count) object(s)")
299 |
300 | for object in objects! {
301 | print("Anchor query object id \(object.uuid)")
302 | }
303 |
304 | self.synchronizer.synchronize(objects: objects, deletedObjects: deletedObjects)
305 | { (error) in
306 |
307 | guard error == nil else
308 | {
309 | print("an error occured while synchronizing " + self.externalObjectType.healthKitObjectType().debugDescription + " objects")
310 | completion(false, error)
311 | self.delegate?.didFinishExecution(for: self, error: error)
312 | return
313 | }
314 |
315 | self.saveAnchor(anchor: anchor)
316 | self.saveLastExecutionDate()
317 |
318 | let count = (objects?.count ?? 0) + (deletedObjects?.count ?? 0)
319 |
320 | if count < limit
321 | {
322 | completion(true, nil)
323 | self.delegate?.didFinishExecution(for: self, error: error)
324 | }
325 | else
326 | {
327 | print("Executing next query for type \(self.externalObjectType)")
328 | self.executeAnchorQuery(type: type, completion: completion)
329 | }
330 | }
331 | }
332 |
333 | self.store.execute(anchoredQuery)
334 | }
335 |
336 | private func enableBackgroundDelivery(for types: [HKObjectType], frequency: HKUpdateFrequency, completion: @escaping (Bool, [Error]) -> Void)
337 | {
338 | var didSucceed = true
339 | var errors = [Error]()
340 | let lockObject = NSObject()
341 |
342 | let dispatchGroup = DispatchGroup()
343 |
344 | types.forEach
345 | { (type) in
346 |
347 | dispatchGroup.enter()
348 |
349 | self.store.enableBackgroundDelivery(for: type,
350 | frequency: .immediate,
351 | withCompletion:
352 | { (success, error) in
353 |
354 | if (success)
355 | {
356 | print("Enabled background delivery for " + type.debugDescription)
357 | }
358 | else if (error != nil)
359 | {
360 | didSucceed = false
361 | objc_sync_enter(lockObject)
362 | errors.append(error!)
363 | objc_sync_exit(lockObject)
364 | print("an error occured enabling background delivery for " + type.debugDescription)
365 | }
366 |
367 | dispatchGroup.leave()
368 | })
369 | }
370 |
371 | dispatchGroup.notify(queue: DispatchQueue.global())
372 | {
373 | completion(didSucceed, errors)
374 | }
375 | }
376 |
377 | private func disableBackgroundDelivery(for types: [HKObjectType], completion: @escaping (Bool, [Error]) -> Void)
378 | {
379 | var didSucceed = true
380 | var errors = [Error]()
381 | let lockObject = NSObject()
382 |
383 | let dispatchGroup = DispatchGroup()
384 |
385 | types.forEach
386 | { (type) in
387 |
388 | dispatchGroup.enter()
389 |
390 | self.store.disableBackgroundDelivery(for: type,
391 | withCompletion:
392 | { (success, error) in
393 |
394 | if (success)
395 | {
396 | print("Disabled background delivery for " + type.debugDescription)
397 | }
398 | else if (error != nil)
399 | {
400 | didSucceed = false
401 | objc_sync_enter(lockObject)
402 | errors.append(error!)
403 | objc_sync_exit(lockObject)
404 | print("an error occured disabling background delivery for " + type.debugDescription)
405 | }
406 |
407 | dispatchGroup.leave()
408 | })
409 | }
410 |
411 | dispatchGroup.notify(queue: DispatchQueue.global())
412 | {
413 | completion(didSucceed, errors)
414 | }
415 | }
416 |
417 | /// MARK - User Defaults
418 |
419 | private func saveLastExecutionDate()
420 | {
421 | if let key = self.externalObjectType.healthKitObjectType()?.identifier
422 | {
423 | self.userDefaultsProxy.set(Date(), forKey: key + self.lastExecutionKeySuffix)
424 | }
425 | }
426 |
427 | private func deleteLastExecutionDate()
428 | {
429 | if let key = self.externalObjectType.healthKitObjectType()?.identifier
430 | {
431 | self.userDefaultsProxy.removeObject(forKey: key + self.lastExecutionKeySuffix)
432 | }
433 | }
434 |
435 | private func lastExecutionDate() -> Date?
436 | {
437 | if let key = self.externalObjectType.healthKitObjectType()?.identifier
438 | {
439 | return self.userDefaultsProxy.object(forKey: key + self.lastExecutionKeySuffix) as? Date
440 | }
441 |
442 | return nil
443 | }
444 |
445 | private func saveAnchor(anchor: HKQueryAnchor?)
446 | {
447 | if let key = self.externalObjectType.healthKitObjectType()?.identifier,
448 | let queryAnchor = anchor
449 | {
450 | do
451 | {
452 | let data = try NSKeyedArchiver.archivedData(withRootObject: queryAnchor, requiringSecureCoding: true)
453 | self.userDefaultsProxy.set(data, forKey: key + self.anchorKeySuffix)
454 | return
455 | }
456 | catch
457 | {
458 | print("An error occured while encoding the Anchor \(error)")
459 | }
460 | }
461 |
462 | self.deleteAnchor()
463 | }
464 |
465 | private func deleteAnchor()
466 | {
467 | if let key = self.externalObjectType.healthKitObjectType()?.identifier
468 | {
469 | self.userDefaultsProxy.removeObject(forKey: key + self.anchorKeySuffix)
470 | }
471 | }
472 |
473 | private func anchor() -> HKQueryAnchor?
474 | {
475 | if let key = self.externalObjectType.healthKitObjectType()?.identifier,
476 | let data = self.userDefaultsProxy.data(forKey: key + self.anchorKeySuffix)
477 | {
478 | do
479 | {
480 | return try NSKeyedUnarchiver.unarchivedObject(ofClass: HKQueryAnchor.self, from: data)
481 | }
482 | catch
483 | {
484 | print("An error occured while decoding the Anchor \(error)")
485 | }
486 | }
487 |
488 | return nil
489 | }
490 |
491 | private func savePredicate(_ predicate: NSPredicate?)
492 | {
493 | if let key = self.externalObjectType.healthKitObjectType()?.identifier,
494 | let queryPredicate = predicate
495 | {
496 | do
497 | {
498 | let data = try NSKeyedArchiver.archivedData(withRootObject: queryPredicate, requiringSecureCoding: true)
499 | self.userDefaultsProxy.set(data, forKey: key + self.predicateKeySuffix)
500 | return
501 | }
502 | catch
503 | {
504 | print("An error occured while encoding the Predicate \(error)")
505 | }
506 | }
507 |
508 | self.deletePredicate()
509 | }
510 |
511 | private func deletePredicate()
512 | {
513 | if let key = self.externalObjectType.healthKitObjectType()?.identifier
514 | {
515 | self.userDefaultsProxy.removeObject(forKey: key + self.predicateKeySuffix)
516 | }
517 | }
518 |
519 | private func predicate() -> NSPredicate?
520 | {
521 | if let key = self.externalObjectType.healthKitObjectType()?.identifier,
522 | let data = self.userDefaultsProxy.data(forKey: key + self.predicateKeySuffix)
523 | {
524 | do
525 | {
526 | return try NSKeyedUnarchiver.unarchivedObject(ofClass: NSPredicate.self, from: data)
527 | }
528 | catch
529 | {
530 | print("An error occured while decoding the Predicate \(error)")
531 | }
532 | }
533 |
534 | return nil
535 | }
536 | }
537 |
538 | #endif
539 |
--------------------------------------------------------------------------------
/Tests/HDSManagerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HDSManagerTests.swift
3 | // HealthDataSync_Tests
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 | import HealthKit
10 | import Quick
11 | import Nimble
12 |
13 | class HDSManagerSpec: QuickSpec {
14 | override func spec() {
15 | describe("HDSManager") {
16 | context("requestPermissionsForAllObservers is called") {
17 | context("permissionsManager error unavailable") {
18 | let test = testObjects()
19 | test.permissionsManager.authorizeHealthKitCompletions.append((false, HDSError.unavailable))
20 | waitUntil { completed in
21 | test.manager.requestPermissionsForAllObservers(completion: { (success, error) in
22 | it("completes unsuccessfully") {
23 | expect(success).to(beFalse())
24 | }
25 | it("returns the error") {
26 | expect(error).to(matchError(HDSError.unavailable))
27 | }
28 | completed()
29 | })
30 | }
31 | }
32 | context("permissionsManager error noSpecifiedTypes") {
33 | let test = testObjects()
34 | test.permissionsManager.authorizeHealthKitCompletions.append((false, HDSError.noSpecifiedTypes))
35 | waitUntil { completed in
36 | test.manager.requestPermissionsForAllObservers(completion: { (success, error) in
37 | it("completes unsuccessfully") {
38 | expect(success).to(beFalse())
39 | }
40 | it("returns the error") {
41 | expect(error).to(matchError(HDSError.noSpecifiedTypes))
42 | }
43 | completed()
44 | })
45 | }
46 | }
47 | context("is successful") {
48 | let test = testObjects()
49 | test.permissionsManager.authorizeHealthKitCompletions.append((true, nil))
50 | waitUntil { completed in
51 | test.manager.requestPermissionsForAllObservers(completion: { (success, error) in
52 | it("complete with success") {
53 | expect(success).to(beTrue())
54 | }
55 | it("returns no error") {
56 | expect(error).to(beNil())
57 | }
58 | completed()
59 | })
60 | }
61 | }
62 | }
63 | context("requestPermissions with observers is called") {
64 | context("permissionsManager error unavailable") {
65 | let test = testObjects()
66 | test.permissionsManager.authorizeHealthKitCompletions.append((false, HDSError.unavailable))
67 | waitUntil { completed in
68 | test.manager.requestPermissions(with: [], { (success, error) in
69 | it("completes unsuccessfully") {
70 | expect(success).to(beFalse())
71 | }
72 | it("returns the error") {
73 | expect(error).to(matchError(HDSError.unavailable))
74 | }
75 | completed()
76 | })
77 | }
78 | }
79 | context("permissionsManager error noSpecifiedTypes") {
80 | let test = testObjects()
81 | test.permissionsManager.authorizeHealthKitCompletions.append((false, HDSError.noSpecifiedTypes))
82 | waitUntil { completed in
83 | test.manager.requestPermissions(with: [], { (success, error) in
84 | it("completes unsuccessfully") {
85 | expect(success).to(beFalse())
86 | }
87 | it("returns the error") {
88 | expect(error).to(matchError(HDSError.noSpecifiedTypes))
89 | }
90 | completed()
91 | })
92 | }
93 | }
94 | context("is successful") {
95 | let test = testObjects()
96 | test.permissionsManager.authorizeHealthKitCompletions.append((true, nil))
97 | waitUntil { completed in
98 | test.manager.requestPermissions(with: [], { (success, error) in
99 | it("complete with success") {
100 | expect(success).to(beTrue())
101 | }
102 | it("returns no error") {
103 | expect(error).to(beNil())
104 | }
105 | completed()
106 | })
107 | }
108 | }
109 | }
110 | context("addObjectTypes and external store is called") {
111 | context("with an empty array") {
112 | let test = testObjects()
113 | test.manager.addObjectTypes([], externalStore: MockExternalStore())
114 | it("has and empty allObservers collection") {
115 | expect(test.manager.allObservers.count) == 0
116 | }
117 | }
118 | context("with a single type") {
119 | let test = testObjects()
120 | test.manager.addObjectTypes([MockExternalObject.self], externalStore: MockExternalStore())
121 | it("creates an observer and adds it to the allObservers collection") {
122 | expect(test.manager.allObservers.count) == 1
123 | }
124 | it("creates the expected type of observer") {
125 | expect(test.manager.allObservers[0].externalObjectType).to(be(MockExternalObject.self))
126 | }
127 | }
128 | context("with 2 types") {
129 | let test = testObjects()
130 | test.manager.addObjectTypes([MockExternalObject.self, MockExternalObject2.self], externalStore: MockExternalStore())
131 | it("creates an observer for each type and adds them to the allObservers collection") {
132 | expect(test.manager.allObservers.count) == 2
133 | }
134 | it("creates the expected types of observers") {
135 | expect(test.manager.allObservers.contains(where: { $0.externalObjectType == MockExternalObject.self })).to(beTrue())
136 | expect(test.manager.allObservers.contains(where: { $0.externalObjectType == MockExternalObject2.self })).to(beTrue())
137 | }
138 | }
139 | context("with a type that already exists in the allObservers collection") {
140 | let test = testObjects()
141 | let mockExternalStore = MockExternalStore()
142 | test.manager.addObjectTypes([MockExternalObject.self], externalStore: mockExternalStore)
143 | test.manager.addObjectTypes([MockExternalObject.self], externalStore: mockExternalStore)
144 | it("does not create another observer") {
145 | expect(test.manager.allObservers.count) == 1
146 | }
147 | }
148 | context("twice with different types") {
149 | let test = testObjects()
150 | let mockExternalStore = MockExternalStore()
151 | test.manager.addObjectTypes([MockExternalObject.self], externalStore: mockExternalStore)
152 | test.manager.addObjectTypes([MockExternalObject2.self], externalStore: mockExternalStore)
153 | it("creates an observer for each type and adds them to the allObservers collection") {
154 | expect(test.manager.allObservers.count) == 2
155 | }
156 | it("creates the expected types of observers") {
157 | expect(test.manager.allObservers.contains(where: { $0.externalObjectType == MockExternalObject.self })).to(beTrue())
158 | expect(test.manager.allObservers.contains(where: { $0.externalObjectType == MockExternalObject2.self })).to(beTrue())
159 | }
160 | }
161 | }
162 | context("addSynchronizers is called") {
163 | context("with an empty array") {
164 | let test = testObjects()
165 | test.manager.addSynchronizers([])
166 | it("has and empty allObservers collection") {
167 | expect(test.manager.allObservers.count) == 0
168 | }
169 | }
170 | context("with a single synchronizer") {
171 | let test = testObjects()
172 | let synchronizer = HDSObjectSynchronizer(externalObjectType: MockExternalObject.self, store: test.store, externalStore: MockExternalStore())
173 | test.manager.addSynchronizers([synchronizer])
174 | it("creates an observer and adds it to the allObservers collection") {
175 | expect(test.manager.allObservers.count) == 1
176 | }
177 | it("creates the expected type of observer") {
178 | expect(test.manager.allObservers[0].externalObjectType).to(be(MockExternalObject.self))
179 | }
180 | }
181 | context("with 2 synchronizers") {
182 | let test = testObjects()
183 | let mockExternalStore = MockExternalStore()
184 | let synchronizer = HDSObjectSynchronizer(externalObjectType: MockExternalObject.self, store: test.store, externalStore: mockExternalStore)
185 | let synchronizer2 = HDSObjectSynchronizer(externalObjectType: MockExternalObject2.self, store: test.store, externalStore: mockExternalStore)
186 | test.manager.addSynchronizers([synchronizer, synchronizer2])
187 | it("creates an observer for each synchronizer and adds them to the allObservers collection") {
188 | expect(test.manager.allObservers.count) == 2
189 | }
190 | it("creates the expected types of observers") {
191 | expect(test.manager.allObservers.contains(where: { $0.externalObjectType == MockExternalObject.self })).to(beTrue())
192 | expect(test.manager.allObservers.contains(where: { $0.externalObjectType == MockExternalObject2.self })).to(beTrue())
193 | }
194 | }
195 | context("with a synchronizer that is already represented in the allObservers collection") {
196 | let test = testObjects()
197 | let mockExternalStore = MockExternalStore()
198 | let synchronizer = HDSObjectSynchronizer(externalObjectType: MockExternalObject.self, store: test.store, externalStore: mockExternalStore)
199 | let synchronizer2 = HDSObjectSynchronizer(externalObjectType: MockExternalObject.self, store: test.store, externalStore: mockExternalStore)
200 | test.manager.addSynchronizers([synchronizer])
201 | test.manager.addSynchronizers([synchronizer2])
202 | it("does not create another observer") {
203 | expect(test.manager.allObservers.count) == 1
204 | }
205 | }
206 | context("twice with different synchronizers") {
207 | let test = testObjects()
208 | let mockExternalStore = MockExternalStore()
209 | let synchronizer = HDSObjectSynchronizer(externalObjectType: MockExternalObject.self, store: test.store, externalStore: mockExternalStore)
210 | let synchronizer2 = HDSObjectSynchronizer(externalObjectType: MockExternalObject2.self, store: test.store, externalStore: mockExternalStore)
211 | test.manager.addSynchronizers([synchronizer])
212 | test.manager.addSynchronizers([synchronizer2])
213 | it("creates an observer for each synchronizer and adds them to the allObservers collection") {
214 | expect(test.manager.allObservers.count) == 2
215 | }
216 | it("creates the expected types of observers") {
217 | expect(test.manager.allObservers.contains(where: { $0.externalObjectType == MockExternalObject.self })).to(beTrue())
218 | expect(test.manager.allObservers.contains(where: { $0.externalObjectType == MockExternalObject2.self })).to(beTrue())
219 | }
220 | }
221 | }
222 | context("startObserving is called") {
223 | context("when no observers have been added") {
224 | let test = testObjects()
225 | test.manager.startObserving()
226 | it("does nothing") {
227 | _ = succeed()
228 | }
229 | }
230 | context("when one observer has been added") {
231 | let test = testObjects()
232 | test.store.authorizationStatusValue = .sharingAuthorized
233 | test.store.enableBackgroundDeliveryCompletions.append((true, nil))
234 | test.userDefaults.dataReturns.append(nil)
235 | test.manager.addObjectTypes([MockExternalObject.self], externalStore: MockExternalStore())
236 | test.manager.startObserving()
237 | it("calls start on the observer") {
238 | expect(test.store.enableBackgroundDeliveryParams.count) == 1
239 | }
240 | }
241 | context("when two observers have been added") {
242 | let test = testObjects()
243 | test.store.authorizationStatusValue = .sharingAuthorized
244 | test.store.enableBackgroundDeliveryCompletions.append((true, nil))
245 | test.store.enableBackgroundDeliveryCompletions.append((true, nil))
246 | test.userDefaults.dataReturns.append(contentsOf: [nil, nil])
247 | test.manager.addObjectTypes([MockExternalObject.self, MockExternalObject2.self], externalStore: MockExternalStore())
248 | test.manager.startObserving()
249 | it("calls start on the observer") {
250 | expect(test.store.enableBackgroundDeliveryParams.count) == 2
251 | }
252 | }
253 | }
254 | context("stopObserving is called") {
255 | context("when no observers have been added") {
256 | let test = testObjects()
257 | test.manager.stopObserving()
258 | it("does nothing") {
259 | _ = succeed()
260 | }
261 | }
262 | context("when one observer has been added") {
263 | let test = testObjects()
264 | test.store.authorizationStatusValue = .sharingAuthorized
265 | test.store.enableBackgroundDeliveryCompletions.append((true, nil))
266 | test.store.disableBackgroundDeliveryCompletions.append((true, nil))
267 | test.userDefaults.dataReturns.append(contentsOf: [nil, nil])
268 | test.manager.addObjectTypes([MockExternalObject.self], externalStore: MockExternalStore())
269 | test.manager.startObserving()
270 | waitForCondition(object: test.manager.allObservers[0], format:"isObserving == true")
271 | test.manager.stopObserving()
272 | it("calls stop on the observer") {
273 | expect(test.store.disableBackgroundDeliveryParams.count).toEventually(equal(1))
274 | }
275 | }
276 | context("when two observers have been added") {
277 | let test = testObjects()
278 | test.store.authorizationStatusValue = .sharingAuthorized
279 | test.store.enableBackgroundDeliveryCompletions.append((true, nil))
280 | test.store.enableBackgroundDeliveryCompletions.append((true, nil))
281 | test.store.disableBackgroundDeliveryCompletions.append((true, nil))
282 | test.store.disableBackgroundDeliveryCompletions.append((true, nil))
283 | test.userDefaults.dataReturns.append(contentsOf: [nil, nil, nil, nil])
284 | test.manager.addObjectTypes([MockExternalObject.self, MockExternalObject2.self], externalStore: MockExternalStore())
285 | test.manager.startObserving()
286 | waitForCondition(object: test.manager.allObservers[0], format:"isObserving == true")
287 | test.manager.stopObserving()
288 | it("calls stop on the observer") {
289 | expect(test.store.disableBackgroundDeliveryParams.count) == 2
290 | }
291 | }
292 | }
293 | context("sources for observers is called") {
294 | context("with an empty array") {
295 | let test = testObjects()
296 | waitUntil { completed in
297 | test.manager.sources(for: [], completion: { (dict, errors) in
298 | it("returns an empty sources dictionary") {
299 | expect(dict.count) == 0
300 | }
301 | it("returns no errors") {
302 | expect(errors!.count) == 0
303 | }
304 | completed()
305 | })
306 | }
307 | }
308 | context("with a single observer") {
309 | let test = testObjects()
310 | test.manager.addObjectTypes([MockExternalObject.self], externalStore: MockExternalStore())
311 | test.manager.sources(for: test.manager.allObservers, completion: { (dict, errors) in })
312 | it("calls execute on the store") {
313 | expect(test.store.executeParams.count) == 1
314 | }
315 | it("contains the expected type") {
316 | expect(test.store.executeParams[0].objectType).to(equal(HKObjectType.quantityType(forIdentifier: .heartRate)))
317 | }
318 | }
319 | context("with a two observers") {
320 | let test = testObjects()
321 | test.manager.addObjectTypes([MockExternalObject.self, MockExternalObject2.self], externalStore: MockExternalStore())
322 | test.manager.sources(for: test.manager.allObservers, completion: { (dict, errors) in })
323 | it("calls execute on the store twice") {
324 | expect(test.store.executeParams.count) == 2
325 | }
326 | it("contains the expected types") {
327 | expect(test.store.executeParams[0].objectType).to(equal(HKObjectType.quantityType(forIdentifier: .heartRate)))
328 | expect(test.store.executeParams[1].objectType).to(equal(HKObjectType.quantityType(forIdentifier: .stepCount)))
329 | }
330 | }
331 | }
332 | }
333 | }
334 |
335 | private func testObjects() -> (store: MockStore, userDefaults: MockUserDefaults, permissionsManager: MockPermissionsManager, manager: HDSManager) {
336 | let mockStore = MockStore()
337 | let mockUserDefaults = MockUserDefaults()
338 | let mockPermissionsManager = MockPermissionsManager(store: mockStore)
339 | let observerFactory = HDSQueryObserverFactory(store: mockStore, userDefaults: mockUserDefaults)
340 | let manager = HDSManager(store: mockStore, userDefaults: mockUserDefaults, permissionsManager: mockPermissionsManager, observerFactory: observerFactory)
341 | return (mockStore, mockUserDefaults, mockPermissionsManager, manager)
342 | }
343 | }
344 |
--------------------------------------------------------------------------------
/Tests/HDSObjectSynchronizerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HDSObjectSynchronizerTests.swift
3 | // HealthDataSync_Tests
4 | //
5 | // Copyright (c) Microsoft Corporation.
6 | // Licensed under the MIT License.
7 |
8 | import Foundation
9 | import HealthKit
10 | import Quick
11 | import Nimble
12 |
13 | class HDSObjectSynchronizerSpec: QuickSpec {
14 | override func spec() {
15 | describe("HDSObjectSynchronizer") {
16 | context("synchronize is called") {
17 | context("with nil objects") {
18 | context("nil deleted objects") {
19 | let test = testObjects()
20 | waitUntil { completed in
21 | test.synchronizer.synchronize(objects: nil, deletedObjects: nil, completion: { (error) in
22 | it("does not return an error") {
23 | expect(error).to(beNil())
24 | }
25 | it("did not call fetchObjects") {
26 | expect(test.externalStore.fetchObjectsParams.count) == 0
27 | }
28 | it("did not call delete") {
29 | expect(test.externalStore.deleteParams.count) == 0
30 | }
31 | it("did not call preferredUnits") {
32 | expect(test.store.preferredUnitsParams.count) == 0
33 | }
34 | it("did not call update") {
35 | expect(test.externalStore.updateParams.count) == 0
36 | }
37 | it("did not call add") {
38 | expect(test.externalStore.addParams.count) == 0
39 | }
40 | completed()
41 | })
42 | }
43 | }
44 | }
45 | context("with no objects") {
46 | context("no deleted objects") {
47 | let test = testObjects()
48 | waitUntil { completed in
49 | test.synchronizer.synchronize(objects: TestHelpers.healthKitObjects(count: 0), deletedObjects: TestHelpers.healthKitDeletedObjects(count: 0), completion: { (error) in
50 | it("does not return an error") {
51 | expect(error).to(beNil())
52 | }
53 | it("did not call fetchObjects") {
54 | expect(test.externalStore.fetchObjectsParams.count) == 0
55 | }
56 | it("did not call delete") {
57 | expect(test.externalStore.deleteParams.count) == 0
58 | }
59 | it("did not call preferredUnits") {
60 | expect(test.store.preferredUnitsParams.count) == 0
61 | }
62 | it("did not call update") {
63 | expect(test.externalStore.updateParams.count) == 0
64 | }
65 | it("did not call add") {
66 | expect(test.externalStore.addParams.count) == 0
67 | }
68 | completed()
69 | })
70 | }
71 | }
72 | }
73 | context("with one object") {
74 | context("no deleted objects") {
75 | context("fetch is succesful") {
76 | context("no fetch objects") {
77 | context("preferredUnits is successful") {
78 | context("add is successful") {
79 | let test = testObjects()
80 | test.externalStore.fetchObjecsCompletions.append((nil, nil))
81 | test.externalStore.addCompletions.append(nil)
82 | waitUntil { completed in
83 | test.synchronizer.synchronize(objects: TestHelpers.healthKitObjects(count: 1), deletedObjects: TestHelpers.healthKitDeletedObjects(count: 0), completion: { (error) in
84 | it("does not return an error") {
85 | expect(error).to(beNil())
86 | }
87 | it("called fetchObjects") {
88 | expect(test.externalStore.fetchObjectsParams.count) == 1
89 | expect(test.externalStore.fetchObjectsParams[0].count) == 1
90 | }
91 | it("did not call delete") {
92 | expect(test.externalStore.deleteParams.count) == 0
93 | }
94 | it("called preferredUnits") {
95 | expect(test.store.preferredUnitsParams.count) == 1
96 | expect(test.store.preferredUnitsParams[0].count) == 1
97 | }
98 | it("did not call update") {
99 | expect(test.externalStore.updateParams.count) == 0
100 | }
101 | it("called add") {
102 | expect(test.externalStore.addParams.count) == 1
103 | }
104 | completed()
105 | })
106 | }
107 | }
108 | context("add fails") {
109 | let test = testObjects()
110 | test.externalStore.fetchObjecsCompletions.append((nil, nil))
111 | test.externalStore.addCompletions.append(MockError.addFailure)
112 | waitUntil { completed in
113 | test.synchronizer.synchronize(objects: TestHelpers.healthKitObjects(count: 1), deletedObjects: TestHelpers.healthKitDeletedObjects(count: 0), completion: { (error) in
114 | it("returns the error") {
115 | expect(error).toNot(beNil())
116 | expect(error).to(matchError(MockError.addFailure))
117 | }
118 | it("called fetchObjects") {
119 | expect(test.externalStore.fetchObjectsParams.count) == 1
120 | expect(test.externalStore.fetchObjectsParams[0].count) == 1
121 | }
122 | it("did not call delete") {
123 | expect(test.externalStore.deleteParams.count) == 0
124 | }
125 | it("called preferredUnits") {
126 | expect(test.store.preferredUnitsParams.count) == 1
127 | expect(test.store.preferredUnitsParams[0].count) == 1
128 | }
129 | it("did not call update") {
130 | expect(test.externalStore.updateParams.count) == 0
131 | }
132 | it("called add") {
133 | expect(test.externalStore.addParams.count) == 1
134 | expect(test.externalStore.addParams[0].count) == 1
135 | }
136 | completed()
137 | })
138 | }
139 | }
140 | }
141 | context("preferredUnits fails") {
142 | let test = testObjects()
143 | test.externalStore.fetchObjecsCompletions.append((nil, nil))
144 | test.store.preferredUnitsCompletions.append(([:], MockError.preferredUnitsFailure))
145 | test.externalStore.addCompletions.append(nil)
146 | waitUntil { completed in
147 | test.synchronizer.synchronize(objects: TestHelpers.healthKitObjects(count: 1), deletedObjects: TestHelpers.healthKitDeletedObjects(count: 0), completion: { (error) in
148 | it("does not return an error") {
149 | expect(error).to(beNil())
150 | }
151 | it("called fetchObjects") {
152 | expect(test.externalStore.fetchObjectsParams.count) == 1
153 | expect(test.externalStore.fetchObjectsParams[0].count) == 1
154 | }
155 | it("did not call delete") {
156 | expect(test.externalStore.deleteParams.count) == 0
157 | }
158 | it("called preferredUnits") {
159 | expect(test.store.preferredUnitsParams.count) == 1
160 | expect(test.store.preferredUnitsParams[0].count) == 1
161 | }
162 | it("did not call update") {
163 | expect(test.externalStore.updateParams.count) == 0
164 | }
165 | it("called add") {
166 | expect(test.externalStore.addParams.count) == 1
167 | expect(test.externalStore.addParams[0].count) == 1
168 | }
169 | completed()
170 | })
171 | }
172 | }
173 | }
174 | context("one fetch object") {
175 | context("update is successful") {
176 | let test = testObjects()
177 | let objects = TestHelpers.healthKitObjects(count: 1)
178 | let externalObject = MockExternalObject.externalObject(object: objects[0], converter: nil)!
179 | test.externalStore.fetchObjecsCompletions.append(([externalObject], nil))
180 | test.externalStore.updateCompletions.append(nil)
181 | waitUntil { completed in
182 | test.synchronizer.synchronize(objects: objects, deletedObjects: TestHelpers.healthKitDeletedObjects(count: 0), completion: { (error) in
183 | it("does not return an error") {
184 | expect(error).to(beNil())
185 | }
186 | it("called fetchObjects") {
187 | expect(test.externalStore.fetchObjectsParams.count) == 1
188 | expect(test.externalStore.fetchObjectsParams[0].count) == 1
189 | }
190 | it("did not call delete") {
191 | expect(test.externalStore.deleteParams.count) == 0
192 | }
193 | it("called preferredUnits") {
194 | expect(test.store.preferredUnitsParams.count) == 1
195 | expect(test.store.preferredUnitsParams[0].count) == 1
196 | }
197 | it("called update") {
198 | expect(test.externalStore.updateParams.count) == 1
199 | }
200 | it("did not call add") {
201 | expect(test.externalStore.addParams.count) == 0
202 | }
203 | completed()
204 | })
205 | }
206 | }
207 | context("update fails") {
208 | let test = testObjects()
209 | let objects = TestHelpers.healthKitObjects(count: 1)
210 | let externalObject = MockExternalObject.externalObject(object: objects[0], converter: nil)!
211 | test.externalStore.fetchObjecsCompletions.append(([externalObject], nil))
212 | test.externalStore.updateCompletions.append(MockError.updateFailure)
213 | waitUntil { completed in
214 | test.synchronizer.synchronize(objects: objects, deletedObjects: TestHelpers.healthKitDeletedObjects(count: 0), completion: { (error) in
215 | it("returns the error") {
216 | expect(error).toNot(beNil())
217 | expect(error).to(matchError(MockError.updateFailure))
218 | }
219 | it("called fetchObjects") {
220 | expect(test.externalStore.fetchObjectsParams.count) == 1
221 | expect(test.externalStore.fetchObjectsParams[0].count) == 1
222 | }
223 | it("did not call delete") {
224 | expect(test.externalStore.deleteParams.count) == 0
225 | }
226 | it("called preferredUnits") {
227 | expect(test.store.preferredUnitsParams.count) == 1
228 | expect(test.store.preferredUnitsParams[0].count) == 1
229 | }
230 | it("called update") {
231 | expect(test.externalStore.updateParams.count) == 1
232 | expect(test.externalStore.updateParams[0].count) == 1
233 | }
234 | it("did not call add") {
235 | expect(test.externalStore.addParams.count) == 0
236 | }
237 | completed()
238 | })
239 | }
240 | }
241 | }
242 | }
243 | context("fetch fails") {
244 | let test = testObjects()
245 | test.externalStore.fetchObjecsCompletions.append((nil, MockError.fetchObjectsFailure))
246 | test.externalStore.addCompletions.append(nil)
247 | waitUntil { completed in
248 | test.synchronizer.synchronize(objects: TestHelpers.healthKitObjects(count: 1), deletedObjects: TestHelpers.healthKitDeletedObjects(count: 0), completion: { (error) in
249 | it("returns the error") {
250 | expect(error).toNot(beNil())
251 | expect(error).to(matchError(MockError.fetchObjectsFailure))
252 | }
253 | it("called fetchObjects") {
254 | expect(test.externalStore.fetchObjectsParams.count) == 1
255 | expect(test.externalStore.fetchObjectsParams[0].count) == 1
256 | }
257 | it("did not call delete") {
258 | expect(test.externalStore.deleteParams.count) == 0
259 | }
260 | it("did not call preferredUnits") {
261 | expect(test.store.preferredUnitsParams.count) == 0
262 | }
263 | it("did not call update") {
264 | expect(test.externalStore.updateParams.count) == 0
265 | }
266 | it("did not call add") {
267 | expect(test.externalStore.addParams.count) == 0
268 | }
269 | completed()
270 | })
271 | }
272 | }
273 | }
274 | context("one deleted object") {
275 | context("delete is succesful") {
276 | let test = testObjects()
277 | let deletedObjects = TestHelpers.healthKitDeletedObjects(count: 1)
278 | let externalObject = MockExternalObject.externalObject(deletedObject: deletedObjects[0], converter: nil)!
279 | test.externalStore.fetchObjecsCompletions.append(([externalObject], nil))
280 | test.externalStore.fetchObjecsCompletions.append((nil, nil))
281 | test.externalStore.deleteCompletions.append(nil)
282 | test.externalStore.addCompletions.append(nil)
283 | waitUntil { completed in
284 | test.synchronizer.synchronize(objects: TestHelpers.healthKitObjects(count: 1), deletedObjects: deletedObjects, completion: { (error) in
285 | it("does not return an error") {
286 | expect(error).to(beNil())
287 | }
288 | it("called fetchObjects twice") {
289 | expect(test.externalStore.fetchObjectsParams.count) == 2
290 | expect(test.externalStore.fetchObjectsParams[0].count) == 1
291 | expect(test.externalStore.fetchObjectsParams[1].count) == 1
292 | }
293 | it("called delete") {
294 | expect(test.externalStore.deleteParams.count) == 1
295 | }
296 | it("called preferredUnits") {
297 | expect(test.store.preferredUnitsParams.count) == 1
298 | expect(test.store.preferredUnitsParams[0].count) == 1
299 | }
300 | it("did not call update") {
301 | expect(test.externalStore.updateParams.count) == 0
302 | }
303 | it("called add") {
304 | expect(test.externalStore.addParams.count) == 1
305 | }
306 | completed()
307 | })
308 | }
309 | }
310 | context("delete fails") {
311 | let test = testObjects()
312 | let deletedObjects = TestHelpers.healthKitDeletedObjects(count: 1)
313 | let externalObject = MockExternalObject.externalObject(deletedObject: deletedObjects[0], converter: nil)!
314 | test.externalStore.fetchObjecsCompletions.append(([externalObject], nil))
315 | test.externalStore.fetchObjecsCompletions.append((nil, nil))
316 | test.externalStore.deleteCompletions.append(MockError.deleteFailure)
317 | test.externalStore.addCompletions.append(nil)
318 | waitUntil { completed in
319 | test.synchronizer.synchronize(objects: TestHelpers.healthKitObjects(count: 1), deletedObjects: deletedObjects, completion: { (error) in
320 | it("returns the error") {
321 | expect(error).toNot(beNil())
322 | expect(error).to(matchError(MockError.deleteFailure))
323 | }
324 | it("called fetchObjects") {
325 | expect(test.externalStore.fetchObjectsParams.count) == 1
326 | expect(test.externalStore.fetchObjectsParams[0].count) == 1
327 | }
328 | it("called delete") {
329 | expect(test.externalStore.deleteParams.count) == 1
330 | expect(test.externalStore.deleteParams[0].count) == 1
331 | }
332 | it("called preferredUnits") {
333 | expect(test.store.preferredUnitsParams.count) == 0
334 | }
335 | it("did not call update") {
336 | expect(test.externalStore.updateParams.count) == 0
337 | }
338 | it("called add") {
339 | expect(test.externalStore.addParams.count) == 0
340 | }
341 | completed()
342 | })
343 | }
344 | }
345 | }
346 | }
347 | context("with 10 Healthkit objects") {
348 | context("no fetch objects") {
349 | let test = testObjects()
350 | test.externalStore.fetchObjecsCompletions.append((nil, nil))
351 | test.externalStore.addCompletions.append(nil)
352 | waitUntil { completed in
353 | test.synchronizer.synchronize(objects: TestHelpers.healthKitObjects(count: 10), deletedObjects: TestHelpers.healthKitDeletedObjects(count: 0), completion: { (error) in
354 | it("does not return an error") {
355 | expect(error).to(beNil())
356 | }
357 | it("called fetchObjects") {
358 | expect(test.externalStore.fetchObjectsParams.count) == 1
359 | expect(test.externalStore.fetchObjectsParams[0].count) == 10
360 | }
361 | it("did not call delete") {
362 | expect(test.externalStore.deleteParams.count) == 0
363 | }
364 | it("called preferredUnits") {
365 | expect(test.store.preferredUnitsParams.count) == 1
366 | expect(test.store.preferredUnitsParams[0].count) == 1
367 | }
368 | it("did not call update") {
369 | expect(test.externalStore.updateParams.count) == 0
370 | }
371 | it("called add") {
372 | expect(test.externalStore.addParams.count) == 1
373 | expect(test.externalStore.addParams[0].count) == 10
374 | }
375 | completed()
376 | })
377 | }
378 | }
379 | context("10 fetch objects") {
380 | let test = testObjects()
381 | let objects = TestHelpers.healthKitObjects(count: 10)
382 | let existingObjects = objects.compactMap({ MockExternalObject.externalObject(object: $0, converter: nil) })
383 | test.externalStore.fetchObjecsCompletions.append((existingObjects, nil))
384 | test.externalStore.updateCompletions.append(nil)
385 | waitUntil { completed in
386 | test.synchronizer.synchronize(objects: objects, deletedObjects: TestHelpers.healthKitDeletedObjects(count: 0), completion: { (error) in
387 | it("does not return an error") {
388 | expect(error).to(beNil())
389 | }
390 | it("called fetchObjects") {
391 | expect(test.externalStore.fetchObjectsParams.count) == 1
392 | expect(test.externalStore.fetchObjectsParams[0].count) == 10
393 | }
394 | it("did not call delete") {
395 | expect(test.externalStore.deleteParams.count) == 0
396 | }
397 | it("called preferredUnits") {
398 | expect(test.store.preferredUnitsParams.count) == 1
399 | expect(test.store.preferredUnitsParams[0].count) == 1
400 | }
401 | it("called update") {
402 | expect(test.externalStore.updateParams.count) == 1
403 | expect(test.externalStore.updateParams[0].count) == 10
404 | }
405 | it("did not call add") {
406 | expect(test.externalStore.addParams.count) == 0
407 | }
408 | completed()
409 | })
410 | }
411 | }
412 | context("5 fetch objects") {
413 | let test = testObjects()
414 | var objects = TestHelpers.healthKitObjects(count: 5)
415 | let existingObjects = objects.compactMap({ MockExternalObject.externalObject(object: $0, converter: nil) })
416 | objects.append(contentsOf: TestHelpers.healthKitObjects(count: 5))
417 | test.externalStore.fetchObjecsCompletions.append((existingObjects, nil))
418 | test.externalStore.updateCompletions.append(nil)
419 | test.externalStore.addCompletions.append(nil)
420 | waitUntil { completed in
421 | test.synchronizer.synchronize(objects: objects, deletedObjects: TestHelpers.healthKitDeletedObjects(count: 0), completion: { (error) in
422 | it("does not return an error") {
423 | expect(error).to(beNil())
424 | }
425 | it("called fetchObjects") {
426 | expect(test.externalStore.fetchObjectsParams.count) == 1
427 | expect(test.externalStore.fetchObjectsParams[0].count) == 10
428 | }
429 | it("did not call delete") {
430 | expect(test.externalStore.deleteParams.count) == 0
431 | }
432 | it("called preferredUnits") {
433 | expect(test.store.preferredUnitsParams.count) == 1
434 | expect(test.store.preferredUnitsParams[0].count) == 1
435 | }
436 | it("called update") {
437 | expect(test.externalStore.updateParams.count) == 1
438 | expect(test.externalStore.updateParams[0].count) == 5
439 | }
440 | it("did not called add") {
441 | expect(test.externalStore.addParams.count) == 1
442 | expect(test.externalStore.addParams[0].count) == 5
443 | }
444 | completed()
445 | })
446 | }
447 | }
448 | }
449 | }
450 | }
451 | }
452 |
453 | private func testObjects() -> (store: MockStore, externalStore: MockExternalStore, synchronizer: HDSObjectSynchronizer) {
454 | let mockStore = MockStore()
455 | let mockExternalStore = MockExternalStore()
456 | let synchronizer = HDSObjectSynchronizer(externalObjectType: MockExternalObject.self, store: mockStore, externalStore: mockExternalStore)
457 | return (mockStore, mockExternalStore, synchronizer)
458 | }
459 | }
460 |
--------------------------------------------------------------------------------
/HealthDataSync.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | A5577625234B94130027F7A4 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5577610234B94130027F7A4 /* Constants.swift */; };
11 | A5577626234B94130027F7A4 /* HDSManagerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5577611234B94130027F7A4 /* HDSManagerFactory.swift */; };
12 | A5577627234B94130027F7A4 /* HDSExternalObjectProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5577613234B94130027F7A4 /* HDSExternalObjectProtocol.swift */; };
13 | A5577628234B94130027F7A4 /* HDSObjectSynchronizerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5577614234B94130027F7A4 /* HDSObjectSynchronizerProtocol.swift */; };
14 | A5577629234B94130027F7A4 /* HDSExternalStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5577615234B94130027F7A4 /* HDSExternalStoreProtocol.swift */; };
15 | A557762A234B94130027F7A4 /* HDSObjectSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5577616234B94130027F7A4 /* HDSObjectSynchronizer.swift */; };
16 | A557762B234B94130027F7A4 /* HDSManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5577617234B94130027F7A4 /* HDSManager.swift */; };
17 | A557762C234B94130027F7A4 /* HDSConverterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5577618234B94130027F7A4 /* HDSConverterProtocol.swift */; };
18 | A557762E234B94130027F7A4 /* HDSQueryObserverDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A557761A234B94130027F7A4 /* HDSQueryObserverDelegate.swift */; };
19 | A557762F234B94130027F7A4 /* HDSPermissionsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A557761B234B94130027F7A4 /* HDSPermissionsManager.swift */; };
20 | A5577630234B94130027F7A4 /* HDSManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A557761C234B94130027F7A4 /* HDSManagerProtocol.swift */; };
21 | A5577631234B94130027F7A4 /* HDSUserDefaultsProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A557761E234B94130027F7A4 /* HDSUserDefaultsProxy.swift */; };
22 | A5577632234B94130027F7A4 /* HDSStoreProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A557761F234B94130027F7A4 /* HDSStoreProxyProtocol.swift */; };
23 | A5577633234B94130027F7A4 /* HDSUserDefaultsProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5577620234B94130027F7A4 /* HDSUserDefaultsProxyProtocol.swift */; };
24 | A5577634234B94130027F7A4 /* HDSStoreProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5577621234B94130027F7A4 /* HDSStoreProxy.swift */; };
25 | A5577635234B94130027F7A4 /* HDSError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5577622234B94130027F7A4 /* HDSError.swift */; };
26 | A5577636234B94130027F7A4 /* HDSQueryObserverFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5577623234B94130027F7A4 /* HDSQueryObserverFactory.swift */; };
27 | A5577637234B94130027F7A4 /* HDSQueryObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5577624234B94130027F7A4 /* HDSQueryObserver.swift */; };
28 | A557763A234B94840027F7A4 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = A5577639234B94840027F7A4 /* Quick */; };
29 | A557763D234B94A60027F7A4 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = A557763C234B94A60027F7A4 /* Nimble */; };
30 | A557763F234BA1EC0027F7A4 /* XCTestCaseExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A557763E234BA1EC0027F7A4 /* XCTestCaseExtensions.swift */; };
31 | A55FD8D62305D54E003E6220 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55FD8D52305D54E003E6220 /* TestHelpers.swift */; };
32 | A55FD8E42305F4DE003E6220 /* HKDeletedObject1 in Resources */ = {isa = PBXBuildFile; fileRef = A55FD8D92305F4DE003E6220 /* HKDeletedObject1 */; };
33 | A55FD8E52305F4DE003E6220 /* HKDeletedObject6 in Resources */ = {isa = PBXBuildFile; fileRef = A55FD8DA2305F4DE003E6220 /* HKDeletedObject6 */; };
34 | A55FD8E62305F4DE003E6220 /* HKDeletedObject8 in Resources */ = {isa = PBXBuildFile; fileRef = A55FD8DB2305F4DE003E6220 /* HKDeletedObject8 */; };
35 | A55FD8E72305F4DE003E6220 /* HKDeletedObject9 in Resources */ = {isa = PBXBuildFile; fileRef = A55FD8DC2305F4DE003E6220 /* HKDeletedObject9 */; };
36 | A55FD8E82305F4DE003E6220 /* HKDeletedObject7 in Resources */ = {isa = PBXBuildFile; fileRef = A55FD8DD2305F4DE003E6220 /* HKDeletedObject7 */; };
37 | A55FD8EA2305F4DE003E6220 /* HKDeletedObject10 in Resources */ = {isa = PBXBuildFile; fileRef = A55FD8DF2305F4DE003E6220 /* HKDeletedObject10 */; };
38 | A55FD8EB2305F4DE003E6220 /* HKDeletedObject5 in Resources */ = {isa = PBXBuildFile; fileRef = A55FD8E02305F4DE003E6220 /* HKDeletedObject5 */; };
39 | A55FD8EC2305F4DE003E6220 /* HKDeletedObject2 in Resources */ = {isa = PBXBuildFile; fileRef = A55FD8E12305F4DE003E6220 /* HKDeletedObject2 */; };
40 | A55FD8ED2305F4DE003E6220 /* HKDeletedObject3 in Resources */ = {isa = PBXBuildFile; fileRef = A55FD8E22305F4DE003E6220 /* HKDeletedObject3 */; };
41 | A55FD8EE2305F4DE003E6220 /* HKDeletedObject4 in Resources */ = {isa = PBXBuildFile; fileRef = A55FD8E32305F4DE003E6220 /* HKDeletedObject4 */; };
42 | A55FD8F023060FE0003E6220 /* MockError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55FD8EF23060FE0003E6220 /* MockError.swift */; };
43 | A55FD8F22306FEB8003E6220 /* HDSQueryObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55FD8F12306FEB8003E6220 /* HDSQueryObserverTests.swift */; };
44 | A55FD8F42306FEE2003E6220 /* HDSPermissionsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55FD8F32306FEE2003E6220 /* HDSPermissionsManagerTests.swift */; };
45 | A55FD8F62306FF46003E6220 /* HDSManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55FD8F52306FF46003E6220 /* HDSManagerTests.swift */; };
46 | A55FD8F8230737B2003E6220 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55FD8F7230737B2003E6220 /* MockUserDefaults.swift */; };
47 | A55FD8FA23075460003E6220 /* MockPermissionsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55FD8F923075460003E6220 /* MockPermissionsManager.swift */; };
48 | A55FD8FC23075C15003E6220 /* MockExternalObject2.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55FD8FB23075C15003E6220 /* MockExternalObject2.swift */; };
49 | A55FD8FE230B00AC003E6220 /* MockSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55FD8FD230B00AC003E6220 /* MockSynchronizer.swift */; };
50 | A55FD900230B47FF003E6220 /* MockQueryObserverDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55FD8FF230B47FF003E6220 /* MockQueryObserverDelegate.swift */; };
51 | A5F9C2BF2303104F00B5A359 /* HDSObjectSynchronizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F9C2BE2303104F00B5A359 /* HDSObjectSynchronizerTests.swift */; };
52 | A5F9C2C22303110300B5A359 /* MockExternalObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F9C2C12303110300B5A359 /* MockExternalObject.swift */; };
53 | A5F9C2C42303115200B5A359 /* MockStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F9C2C32303115200B5A359 /* MockStore.swift */; };
54 | A5F9C2C62303116600B5A359 /* MockExternalStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F9C2C52303116600B5A359 /* MockExternalStore.swift */; };
55 | /* End PBXBuildFile section */
56 |
57 | /* Begin PBXFileReference section */
58 | 607FACE51AFB9204008FA782 /* HealthDataSync_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HealthDataSync_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
59 | 607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
60 | A5577610234B94130027F7A4 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; };
61 | A5577611234B94130027F7A4 /* HDSManagerFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HDSManagerFactory.swift; sourceTree = ""; };
62 | A5577613234B94130027F7A4 /* HDSExternalObjectProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HDSExternalObjectProtocol.swift; sourceTree = ""; };
63 | A5577614234B94130027F7A4 /* HDSObjectSynchronizerProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HDSObjectSynchronizerProtocol.swift; sourceTree = ""; };
64 | A5577615234B94130027F7A4 /* HDSExternalStoreProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HDSExternalStoreProtocol.swift; sourceTree = ""; };
65 | A5577616234B94130027F7A4 /* HDSObjectSynchronizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HDSObjectSynchronizer.swift; sourceTree = ""; };
66 | A5577617234B94130027F7A4 /* HDSManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HDSManager.swift; sourceTree = ""; };
67 | A5577618234B94130027F7A4 /* HDSConverterProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HDSConverterProtocol.swift; sourceTree = ""; };
68 | A557761A234B94130027F7A4 /* HDSQueryObserverDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HDSQueryObserverDelegate.swift; sourceTree = ""; };
69 | A557761B234B94130027F7A4 /* HDSPermissionsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HDSPermissionsManager.swift; sourceTree = ""; };
70 | A557761C234B94130027F7A4 /* HDSManagerProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HDSManagerProtocol.swift; sourceTree = ""; };
71 | A557761E234B94130027F7A4 /* HDSUserDefaultsProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HDSUserDefaultsProxy.swift; sourceTree = ""; };
72 | A557761F234B94130027F7A4 /* HDSStoreProxyProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HDSStoreProxyProtocol.swift; sourceTree = ""; };
73 | A5577620234B94130027F7A4 /* HDSUserDefaultsProxyProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HDSUserDefaultsProxyProtocol.swift; sourceTree = ""; };
74 | A5577621234B94130027F7A4 /* HDSStoreProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HDSStoreProxy.swift; sourceTree = ""; };
75 | A5577622234B94130027F7A4 /* HDSError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HDSError.swift; sourceTree = ""; };
76 | A5577623234B94130027F7A4 /* HDSQueryObserverFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HDSQueryObserverFactory.swift; sourceTree = ""; };
77 | A5577624234B94130027F7A4 /* HDSQueryObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HDSQueryObserver.swift; sourceTree = ""; };
78 | A557763E234BA1EC0027F7A4 /* XCTestCaseExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestCaseExtensions.swift; sourceTree = ""; };
79 | A55FD8D52305D54E003E6220 /* TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = ""; };
80 | A55FD8D92305F4DE003E6220 /* HKDeletedObject1 */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = HKDeletedObject1; sourceTree = ""; };
81 | A55FD8DA2305F4DE003E6220 /* HKDeletedObject6 */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = HKDeletedObject6; sourceTree = ""; };
82 | A55FD8DB2305F4DE003E6220 /* HKDeletedObject8 */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = HKDeletedObject8; sourceTree = ""; };
83 | A55FD8DC2305F4DE003E6220 /* HKDeletedObject9 */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = HKDeletedObject9; sourceTree = ""; };
84 | A55FD8DD2305F4DE003E6220 /* HKDeletedObject7 */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = HKDeletedObject7; sourceTree = ""; };
85 | A55FD8DF2305F4DE003E6220 /* HKDeletedObject10 */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = HKDeletedObject10; sourceTree = ""; };
86 | A55FD8E02305F4DE003E6220 /* HKDeletedObject5 */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = HKDeletedObject5; sourceTree = ""; };
87 | A55FD8E12305F4DE003E6220 /* HKDeletedObject2 */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = HKDeletedObject2; sourceTree = ""; };
88 | A55FD8E22305F4DE003E6220 /* HKDeletedObject3 */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = HKDeletedObject3; sourceTree = ""; };
89 | A55FD8E32305F4DE003E6220 /* HKDeletedObject4 */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = HKDeletedObject4; sourceTree = ""; };
90 | A55FD8EF23060FE0003E6220 /* MockError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockError.swift; sourceTree = ""; };
91 | A55FD8F12306FEB8003E6220 /* HDSQueryObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HDSQueryObserverTests.swift; sourceTree = ""; };
92 | A55FD8F32306FEE2003E6220 /* HDSPermissionsManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HDSPermissionsManagerTests.swift; sourceTree = ""; };
93 | A55FD8F52306FF46003E6220 /* HDSManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HDSManagerTests.swift; sourceTree = ""; };
94 | A55FD8F7230737B2003E6220 /* MockUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserDefaults.swift; sourceTree = ""; };
95 | A55FD8F923075460003E6220 /* MockPermissionsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPermissionsManager.swift; sourceTree = ""; };
96 | A55FD8FB23075C15003E6220 /* MockExternalObject2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockExternalObject2.swift; sourceTree = ""; };
97 | A55FD8FD230B00AC003E6220 /* MockSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSynchronizer.swift; sourceTree = ""; };
98 | A55FD8FF230B47FF003E6220 /* MockQueryObserverDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockQueryObserverDelegate.swift; sourceTree = ""; };
99 | A5F9C2BE2303104F00B5A359 /* HDSObjectSynchronizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HDSObjectSynchronizerTests.swift; sourceTree = ""; };
100 | A5F9C2C12303110300B5A359 /* MockExternalObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockExternalObject.swift; sourceTree = ""; };
101 | A5F9C2C32303115200B5A359 /* MockStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStore.swift; sourceTree = ""; };
102 | A5F9C2C52303116600B5A359 /* MockExternalStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockExternalStore.swift; sourceTree = ""; };
103 | /* End PBXFileReference section */
104 |
105 | /* Begin PBXFrameworksBuildPhase section */
106 | 607FACE21AFB9204008FA782 /* Frameworks */ = {
107 | isa = PBXFrameworksBuildPhase;
108 | buildActionMask = 2147483647;
109 | files = (
110 | A557763A234B94840027F7A4 /* Quick in Frameworks */,
111 | A557763D234B94A60027F7A4 /* Nimble in Frameworks */,
112 | );
113 | runOnlyForDeploymentPostprocessing = 0;
114 | };
115 | /* End PBXFrameworksBuildPhase section */
116 |
117 | /* Begin PBXGroup section */
118 | 607FACC71AFB9204008FA782 = {
119 | isa = PBXGroup;
120 | children = (
121 | 607FACE81AFB9204008FA782 /* Tests */,
122 | A557760F234B94130027F7A4 /* Sources */,
123 | 607FACD11AFB9204008FA782 /* Products */,
124 | );
125 | sourceTree = "";
126 | };
127 | 607FACD11AFB9204008FA782 /* Products */ = {
128 | isa = PBXGroup;
129 | children = (
130 | 607FACE51AFB9204008FA782 /* HealthDataSync_Tests.xctest */,
131 | );
132 | name = Products;
133 | sourceTree = "";
134 | };
135 | 607FACE81AFB9204008FA782 /* Tests */ = {
136 | isa = PBXGroup;
137 | children = (
138 | A55FD8D82305F4DE003E6220 /* TestData */,
139 | A55FD8D42305D536003E6220 /* Helpers */,
140 | A5F9C2C0230310CD00B5A359 /* Mocks */,
141 | 607FACE91AFB9204008FA782 /* Supporting Files */,
142 | A55FD8F12306FEB8003E6220 /* HDSQueryObserverTests.swift */,
143 | A55FD8F32306FEE2003E6220 /* HDSPermissionsManagerTests.swift */,
144 | A55FD8F52306FF46003E6220 /* HDSManagerTests.swift */,
145 | A5F9C2BE2303104F00B5A359 /* HDSObjectSynchronizerTests.swift */,
146 | );
147 | path = Tests;
148 | sourceTree = "";
149 | };
150 | 607FACE91AFB9204008FA782 /* Supporting Files */ = {
151 | isa = PBXGroup;
152 | children = (
153 | 607FACEA1AFB9204008FA782 /* Info.plist */,
154 | );
155 | name = "Supporting Files";
156 | sourceTree = "";
157 | };
158 | A557760F234B94130027F7A4 /* Sources */ = {
159 | isa = PBXGroup;
160 | children = (
161 | A5577610234B94130027F7A4 /* Constants.swift */,
162 | A5577611234B94130027F7A4 /* HDSManagerFactory.swift */,
163 | A5577612234B94130027F7A4 /* Synchronizers */,
164 | A5577617234B94130027F7A4 /* HDSManager.swift */,
165 | A5577618234B94130027F7A4 /* HDSConverterProtocol.swift */,
166 | A557761A234B94130027F7A4 /* HDSQueryObserverDelegate.swift */,
167 | A557761B234B94130027F7A4 /* HDSPermissionsManager.swift */,
168 | A557761C234B94130027F7A4 /* HDSManagerProtocol.swift */,
169 | A557761D234B94130027F7A4 /* Proxies */,
170 | A5577622234B94130027F7A4 /* HDSError.swift */,
171 | A5577623234B94130027F7A4 /* HDSQueryObserverFactory.swift */,
172 | A5577624234B94130027F7A4 /* HDSQueryObserver.swift */,
173 | );
174 | path = Sources;
175 | sourceTree = "";
176 | };
177 | A5577612234B94130027F7A4 /* Synchronizers */ = {
178 | isa = PBXGroup;
179 | children = (
180 | A5577613234B94130027F7A4 /* HDSExternalObjectProtocol.swift */,
181 | A5577614234B94130027F7A4 /* HDSObjectSynchronizerProtocol.swift */,
182 | A5577615234B94130027F7A4 /* HDSExternalStoreProtocol.swift */,
183 | A5577616234B94130027F7A4 /* HDSObjectSynchronizer.swift */,
184 | );
185 | path = Synchronizers;
186 | sourceTree = "";
187 | };
188 | A557761D234B94130027F7A4 /* Proxies */ = {
189 | isa = PBXGroup;
190 | children = (
191 | A557761E234B94130027F7A4 /* HDSUserDefaultsProxy.swift */,
192 | A557761F234B94130027F7A4 /* HDSStoreProxyProtocol.swift */,
193 | A5577620234B94130027F7A4 /* HDSUserDefaultsProxyProtocol.swift */,
194 | A5577621234B94130027F7A4 /* HDSStoreProxy.swift */,
195 | );
196 | path = Proxies;
197 | sourceTree = "";
198 | };
199 | A55FD8D42305D536003E6220 /* Helpers */ = {
200 | isa = PBXGroup;
201 | children = (
202 | A55FD8D52305D54E003E6220 /* TestHelpers.swift */,
203 | A557763E234BA1EC0027F7A4 /* XCTestCaseExtensions.swift */,
204 | );
205 | name = Helpers;
206 | sourceTree = "";
207 | };
208 | A55FD8D82305F4DE003E6220 /* TestData */ = {
209 | isa = PBXGroup;
210 | children = (
211 | A55FD8D92305F4DE003E6220 /* HKDeletedObject1 */,
212 | A55FD8DA2305F4DE003E6220 /* HKDeletedObject6 */,
213 | A55FD8DB2305F4DE003E6220 /* HKDeletedObject8 */,
214 | A55FD8DC2305F4DE003E6220 /* HKDeletedObject9 */,
215 | A55FD8DD2305F4DE003E6220 /* HKDeletedObject7 */,
216 | A55FD8DF2305F4DE003E6220 /* HKDeletedObject10 */,
217 | A55FD8E02305F4DE003E6220 /* HKDeletedObject5 */,
218 | A55FD8E12305F4DE003E6220 /* HKDeletedObject2 */,
219 | A55FD8E22305F4DE003E6220 /* HKDeletedObject3 */,
220 | A55FD8E32305F4DE003E6220 /* HKDeletedObject4 */,
221 | );
222 | path = TestData;
223 | sourceTree = "";
224 | };
225 | A5F9C2C0230310CD00B5A359 /* Mocks */ = {
226 | isa = PBXGroup;
227 | children = (
228 | A5F9C2C12303110300B5A359 /* MockExternalObject.swift */,
229 | A55FD8FB23075C15003E6220 /* MockExternalObject2.swift */,
230 | A5F9C2C32303115200B5A359 /* MockStore.swift */,
231 | A5F9C2C52303116600B5A359 /* MockExternalStore.swift */,
232 | A55FD8EF23060FE0003E6220 /* MockError.swift */,
233 | A55FD8F7230737B2003E6220 /* MockUserDefaults.swift */,
234 | A55FD8F923075460003E6220 /* MockPermissionsManager.swift */,
235 | A55FD8FD230B00AC003E6220 /* MockSynchronizer.swift */,
236 | A55FD8FF230B47FF003E6220 /* MockQueryObserverDelegate.swift */,
237 | );
238 | name = Mocks;
239 | sourceTree = "";
240 | };
241 | /* End PBXGroup section */
242 |
243 | /* Begin PBXNativeTarget section */
244 | 607FACE41AFB9204008FA782 /* HealthDataSync_Tests */ = {
245 | isa = PBXNativeTarget;
246 | buildConfigurationList = 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "HealthDataSync_Tests" */;
247 | buildPhases = (
248 | 607FACE11AFB9204008FA782 /* Sources */,
249 | 607FACE21AFB9204008FA782 /* Frameworks */,
250 | 607FACE31AFB9204008FA782 /* Resources */,
251 | );
252 | buildRules = (
253 | );
254 | dependencies = (
255 | );
256 | name = HealthDataSync_Tests;
257 | packageProductDependencies = (
258 | A5577639234B94840027F7A4 /* Quick */,
259 | A557763C234B94A60027F7A4 /* Nimble */,
260 | );
261 | productName = Tests;
262 | productReference = 607FACE51AFB9204008FA782 /* HealthDataSync_Tests.xctest */;
263 | productType = "com.apple.product-type.bundle.unit-test";
264 | };
265 | /* End PBXNativeTarget section */
266 |
267 | /* Begin PBXProject section */
268 | 607FACC81AFB9204008FA782 /* Project object */ = {
269 | isa = PBXProject;
270 | attributes = {
271 | LastSwiftUpdateCheck = 0830;
272 | LastUpgradeCheck = 1020;
273 | ORGANIZATIONNAME = CocoaPods;
274 | TargetAttributes = {
275 | 607FACE41AFB9204008FA782 = {
276 | CreatedOnToolsVersion = 6.3.1;
277 | DevelopmentTeam = UBF8T346G9;
278 | LastSwiftMigration = 1020;
279 | TestTargetID = 607FACCF1AFB9204008FA782;
280 | };
281 | };
282 | };
283 | buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "HealthDataSync" */;
284 | compatibilityVersion = "Xcode 3.2";
285 | developmentRegion = en;
286 | hasScannedForEncodings = 0;
287 | knownRegions = (
288 | en,
289 | Base,
290 | );
291 | mainGroup = 607FACC71AFB9204008FA782;
292 | packageReferences = (
293 | A5577638234B94840027F7A4 /* XCRemoteSwiftPackageReference "Quick" */,
294 | A557763B234B94A60027F7A4 /* XCRemoteSwiftPackageReference "Nimble" */,
295 | );
296 | productRefGroup = 607FACD11AFB9204008FA782 /* Products */;
297 | projectDirPath = "";
298 | projectRoot = "";
299 | targets = (
300 | 607FACE41AFB9204008FA782 /* HealthDataSync_Tests */,
301 | );
302 | };
303 | /* End PBXProject section */
304 |
305 | /* Begin PBXResourcesBuildPhase section */
306 | 607FACE31AFB9204008FA782 /* Resources */ = {
307 | isa = PBXResourcesBuildPhase;
308 | buildActionMask = 2147483647;
309 | files = (
310 | A55FD8EC2305F4DE003E6220 /* HKDeletedObject2 in Resources */,
311 | A55FD8EA2305F4DE003E6220 /* HKDeletedObject10 in Resources */,
312 | A55FD8E62305F4DE003E6220 /* HKDeletedObject8 in Resources */,
313 | A55FD8E42305F4DE003E6220 /* HKDeletedObject1 in Resources */,
314 | A55FD8EE2305F4DE003E6220 /* HKDeletedObject4 in Resources */,
315 | A55FD8E72305F4DE003E6220 /* HKDeletedObject9 in Resources */,
316 | A55FD8E52305F4DE003E6220 /* HKDeletedObject6 in Resources */,
317 | A55FD8ED2305F4DE003E6220 /* HKDeletedObject3 in Resources */,
318 | A55FD8E82305F4DE003E6220 /* HKDeletedObject7 in Resources */,
319 | A55FD8EB2305F4DE003E6220 /* HKDeletedObject5 in Resources */,
320 | );
321 | runOnlyForDeploymentPostprocessing = 0;
322 | };
323 | /* End PBXResourcesBuildPhase section */
324 |
325 | /* Begin PBXSourcesBuildPhase section */
326 | 607FACE11AFB9204008FA782 /* Sources */ = {
327 | isa = PBXSourcesBuildPhase;
328 | buildActionMask = 2147483647;
329 | files = (
330 | A5577628234B94130027F7A4 /* HDSObjectSynchronizerProtocol.swift in Sources */,
331 | A55FD8F42306FEE2003E6220 /* HDSPermissionsManagerTests.swift in Sources */,
332 | A5577635234B94130027F7A4 /* HDSError.swift in Sources */,
333 | A5F9C2C62303116600B5A359 /* MockExternalStore.swift in Sources */,
334 | A55FD8F22306FEB8003E6220 /* HDSQueryObserverTests.swift in Sources */,
335 | A55FD8F62306FF46003E6220 /* HDSManagerTests.swift in Sources */,
336 | A5577626234B94130027F7A4 /* HDSManagerFactory.swift in Sources */,
337 | A557762E234B94130027F7A4 /* HDSQueryObserverDelegate.swift in Sources */,
338 | A5577636234B94130027F7A4 /* HDSQueryObserverFactory.swift in Sources */,
339 | A557762A234B94130027F7A4 /* HDSObjectSynchronizer.swift in Sources */,
340 | A55FD8D62305D54E003E6220 /* TestHelpers.swift in Sources */,
341 | A5577625234B94130027F7A4 /* Constants.swift in Sources */,
342 | A5577627234B94130027F7A4 /* HDSExternalObjectProtocol.swift in Sources */,
343 | A557762C234B94130027F7A4 /* HDSConverterProtocol.swift in Sources */,
344 | A5577637234B94130027F7A4 /* HDSQueryObserver.swift in Sources */,
345 | A5F9C2BF2303104F00B5A359 /* HDSObjectSynchronizerTests.swift in Sources */,
346 | A55FD8FC23075C15003E6220 /* MockExternalObject2.swift in Sources */,
347 | A557762B234B94130027F7A4 /* HDSManager.swift in Sources */,
348 | A557763F234BA1EC0027F7A4 /* XCTestCaseExtensions.swift in Sources */,
349 | A5577633234B94130027F7A4 /* HDSUserDefaultsProxyProtocol.swift in Sources */,
350 | A55FD8F8230737B2003E6220 /* MockUserDefaults.swift in Sources */,
351 | A5577632234B94130027F7A4 /* HDSStoreProxyProtocol.swift in Sources */,
352 | A55FD8FE230B00AC003E6220 /* MockSynchronizer.swift in Sources */,
353 | A5F9C2C22303110300B5A359 /* MockExternalObject.swift in Sources */,
354 | A5577634234B94130027F7A4 /* HDSStoreProxy.swift in Sources */,
355 | A55FD8FA23075460003E6220 /* MockPermissionsManager.swift in Sources */,
356 | A55FD8F023060FE0003E6220 /* MockError.swift in Sources */,
357 | A557762F234B94130027F7A4 /* HDSPermissionsManager.swift in Sources */,
358 | A5577631234B94130027F7A4 /* HDSUserDefaultsProxy.swift in Sources */,
359 | A5F9C2C42303115200B5A359 /* MockStore.swift in Sources */,
360 | A5577629234B94130027F7A4 /* HDSExternalStoreProtocol.swift in Sources */,
361 | A55FD900230B47FF003E6220 /* MockQueryObserverDelegate.swift in Sources */,
362 | A5577630234B94130027F7A4 /* HDSManagerProtocol.swift in Sources */,
363 | );
364 | runOnlyForDeploymentPostprocessing = 0;
365 | };
366 | /* End PBXSourcesBuildPhase section */
367 |
368 | /* Begin XCBuildConfiguration section */
369 | 607FACED1AFB9204008FA782 /* Debug */ = {
370 | isa = XCBuildConfiguration;
371 | buildSettings = {
372 | ALWAYS_SEARCH_USER_PATHS = NO;
373 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
374 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
375 | CLANG_CXX_LIBRARY = "libc++";
376 | CLANG_ENABLE_MODULES = YES;
377 | CLANG_ENABLE_OBJC_ARC = YES;
378 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
379 | CLANG_WARN_BOOL_CONVERSION = YES;
380 | CLANG_WARN_COMMA = YES;
381 | CLANG_WARN_CONSTANT_CONVERSION = YES;
382 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
383 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
384 | CLANG_WARN_EMPTY_BODY = YES;
385 | CLANG_WARN_ENUM_CONVERSION = YES;
386 | CLANG_WARN_INFINITE_RECURSION = YES;
387 | CLANG_WARN_INT_CONVERSION = YES;
388 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
389 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
390 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
391 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
392 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
393 | CLANG_WARN_STRICT_PROTOTYPES = YES;
394 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
395 | CLANG_WARN_UNREACHABLE_CODE = YES;
396 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
397 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
398 | COPY_PHASE_STRIP = NO;
399 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
400 | ENABLE_STRICT_OBJC_MSGSEND = YES;
401 | ENABLE_TESTABILITY = YES;
402 | GCC_C_LANGUAGE_STANDARD = gnu99;
403 | GCC_DYNAMIC_NO_PIC = NO;
404 | GCC_NO_COMMON_BLOCKS = YES;
405 | GCC_OPTIMIZATION_LEVEL = 0;
406 | GCC_PREPROCESSOR_DEFINITIONS = (
407 | "DEBUG=1",
408 | "$(inherited)",
409 | );
410 | GCC_SYMBOLS_PRIVATE_EXTERN = NO;
411 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
412 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
413 | GCC_WARN_UNDECLARED_SELECTOR = YES;
414 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
415 | GCC_WARN_UNUSED_FUNCTION = YES;
416 | GCC_WARN_UNUSED_VARIABLE = YES;
417 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
418 | MTL_ENABLE_DEBUG_INFO = YES;
419 | ONLY_ACTIVE_ARCH = YES;
420 | SDKROOT = iphoneos;
421 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
422 | };
423 | name = Debug;
424 | };
425 | 607FACEE1AFB9204008FA782 /* Release */ = {
426 | isa = XCBuildConfiguration;
427 | buildSettings = {
428 | ALWAYS_SEARCH_USER_PATHS = NO;
429 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
430 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
431 | CLANG_CXX_LIBRARY = "libc++";
432 | CLANG_ENABLE_MODULES = YES;
433 | CLANG_ENABLE_OBJC_ARC = YES;
434 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
435 | CLANG_WARN_BOOL_CONVERSION = YES;
436 | CLANG_WARN_COMMA = YES;
437 | CLANG_WARN_CONSTANT_CONVERSION = YES;
438 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
439 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
440 | CLANG_WARN_EMPTY_BODY = YES;
441 | CLANG_WARN_ENUM_CONVERSION = YES;
442 | CLANG_WARN_INFINITE_RECURSION = YES;
443 | CLANG_WARN_INT_CONVERSION = YES;
444 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
445 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
446 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
447 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
448 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
449 | CLANG_WARN_STRICT_PROTOTYPES = YES;
450 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
451 | CLANG_WARN_UNREACHABLE_CODE = YES;
452 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
453 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
454 | COPY_PHASE_STRIP = NO;
455 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
456 | ENABLE_NS_ASSERTIONS = NO;
457 | ENABLE_STRICT_OBJC_MSGSEND = YES;
458 | GCC_C_LANGUAGE_STANDARD = gnu99;
459 | GCC_NO_COMMON_BLOCKS = YES;
460 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
461 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
462 | GCC_WARN_UNDECLARED_SELECTOR = YES;
463 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
464 | GCC_WARN_UNUSED_FUNCTION = YES;
465 | GCC_WARN_UNUSED_VARIABLE = YES;
466 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
467 | MTL_ENABLE_DEBUG_INFO = NO;
468 | SDKROOT = iphoneos;
469 | SWIFT_COMPILATION_MODE = wholemodule;
470 | SWIFT_OPTIMIZATION_LEVEL = "-O";
471 | VALIDATE_PRODUCT = YES;
472 | };
473 | name = Release;
474 | };
475 | 607FACF31AFB9204008FA782 /* Debug */ = {
476 | isa = XCBuildConfiguration;
477 | buildSettings = {
478 | CLANG_ENABLE_MODULES = YES;
479 | DEVELOPMENT_TEAM = UBF8T346G9;
480 | FRAMEWORK_SEARCH_PATHS = "$(inherited)";
481 | GCC_PREPROCESSOR_DEFINITIONS = (
482 | "DEBUG=1",
483 | "$(inherited)",
484 | );
485 | INFOPLIST_FILE = Tests/Info.plist;
486 | LD_RUNPATH_SEARCH_PATHS = (
487 | "$(inherited)",
488 | "@executable_path/Frameworks",
489 | "@loader_path/Frameworks",
490 | );
491 | PRODUCT_BUNDLE_IDENTIFIER = "com.microsoft.$(PRODUCT_NAME:rfc1034identifier)";
492 | PRODUCT_NAME = "$(TARGET_NAME)";
493 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
494 | SWIFT_VERSION = 5.0;
495 | };
496 | name = Debug;
497 | };
498 | 607FACF41AFB9204008FA782 /* Release */ = {
499 | isa = XCBuildConfiguration;
500 | buildSettings = {
501 | CLANG_ENABLE_MODULES = YES;
502 | DEVELOPMENT_TEAM = UBF8T346G9;
503 | FRAMEWORK_SEARCH_PATHS = "$(inherited)";
504 | INFOPLIST_FILE = Tests/Info.plist;
505 | LD_RUNPATH_SEARCH_PATHS = (
506 | "$(inherited)",
507 | "@executable_path/Frameworks",
508 | "@loader_path/Frameworks",
509 | );
510 | PRODUCT_BUNDLE_IDENTIFIER = "com.microsoft.$(PRODUCT_NAME:rfc1034identifier)";
511 | PRODUCT_NAME = "$(TARGET_NAME)";
512 | SWIFT_VERSION = 5.0;
513 | };
514 | name = Release;
515 | };
516 | /* End XCBuildConfiguration section */
517 |
518 | /* Begin XCConfigurationList section */
519 | 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "HealthDataSync" */ = {
520 | isa = XCConfigurationList;
521 | buildConfigurations = (
522 | 607FACED1AFB9204008FA782 /* Debug */,
523 | 607FACEE1AFB9204008FA782 /* Release */,
524 | );
525 | defaultConfigurationIsVisible = 0;
526 | defaultConfigurationName = Release;
527 | };
528 | 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "HealthDataSync_Tests" */ = {
529 | isa = XCConfigurationList;
530 | buildConfigurations = (
531 | 607FACF31AFB9204008FA782 /* Debug */,
532 | 607FACF41AFB9204008FA782 /* Release */,
533 | );
534 | defaultConfigurationIsVisible = 0;
535 | defaultConfigurationName = Release;
536 | };
537 | /* End XCConfigurationList section */
538 |
539 | /* Begin XCRemoteSwiftPackageReference section */
540 | A5577638234B94840027F7A4 /* XCRemoteSwiftPackageReference "Quick" */ = {
541 | isa = XCRemoteSwiftPackageReference;
542 | repositoryURL = "https://github.com/Quick/Quick";
543 | requirement = {
544 | kind = upToNextMajorVersion;
545 | minimumVersion = 2.2.0;
546 | };
547 | };
548 | A557763B234B94A60027F7A4 /* XCRemoteSwiftPackageReference "Nimble" */ = {
549 | isa = XCRemoteSwiftPackageReference;
550 | repositoryURL = "https://github.com/Quick/Nimble";
551 | requirement = {
552 | kind = exactVersion;
553 | version = 8.0.2;
554 | };
555 | };
556 | /* End XCRemoteSwiftPackageReference section */
557 |
558 | /* Begin XCSwiftPackageProductDependency section */
559 | A5577639234B94840027F7A4 /* Quick */ = {
560 | isa = XCSwiftPackageProductDependency;
561 | package = A5577638234B94840027F7A4 /* XCRemoteSwiftPackageReference "Quick" */;
562 | productName = Quick;
563 | };
564 | A557763C234B94A60027F7A4 /* Nimble */ = {
565 | isa = XCSwiftPackageProductDependency;
566 | package = A557763B234B94A60027F7A4 /* XCRemoteSwiftPackageReference "Nimble" */;
567 | productName = Nimble;
568 | };
569 | /* End XCSwiftPackageProductDependency section */
570 | };
571 | rootObject = 607FACC81AFB9204008FA782 /* Project object */;
572 | }
573 |
--------------------------------------------------------------------------------