├── 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 | [![Build Status](https://microsofthealth.visualstudio.com/Health/_apis/build/status/POET/HealthDataSync_Daily?branchName=master)](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 | --------------------------------------------------------------------------------