├── Core Bluetooth HRM.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ ├── simon.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ │ └── softwaretesting.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcuserdata │ ├── simon.xcuserdatad │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ └── softwaretesting.xcuserdatad │ │ ├── xcschemes │ │ └── xcschememanagement.plist │ │ └── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist └── project.pbxproj ├── Core Bluetooth HRM ├── Core Bluetooth HRM.entitlements ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── AppDelegate.swift ├── HealthKitInterface Gender Final.swift ├── HealthKitInterface.swift ├── HealthKitInterface Gender.swift ├── HealthKitInterface Heart Rate Draft.swift └── HeartRateMonitorViewController.swift └── README.md /Core Bluetooth HRM.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Core Bluetooth HRM.xcodeproj/project.xcworkspace/xcuserdata/simon.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appcoda/HealthKit-Demo/HEAD/Core Bluetooth HRM.xcodeproj/project.xcworkspace/xcuserdata/simon.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Core Bluetooth HRM.xcodeproj/project.xcworkspace/xcuserdata/softwaretesting.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appcoda/HealthKit-Demo/HEAD/Core Bluetooth HRM.xcodeproj/project.xcworkspace/xcuserdata/softwaretesting.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Core Bluetooth HRM/Core Bluetooth HRM.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.healthkit 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Core Bluetooth HRM.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Core Bluetooth HRM.xcodeproj/xcuserdata/simon.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Core Bluetooth HRM.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Core Bluetooth HRM.xcodeproj/xcuserdata/softwaretesting.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Core Bluetooth HRM.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A HealthKit demo that works with Core Bluetooth 2 | 3 | In this tutorial, we are going to walk you through a few stuff: 4 | 5 | * Explain the concepts behind HealthKit. 6 | * Show you how HealthKit can give your career a boost. 7 | * Discuss the privacy implications of developing apps that can read/write a user's very personal health data. 8 | * Show you how to prepare an Xcode project for integration with HealthKit. 9 | * Walk you through some Swift 4 code I wrote for reading from and writing to the HealthKit data store. 10 | * Show you the output from my code as it works with health data. 11 | * And, finally, give some hints as where you can go next with HealthKit. 12 | 13 | For the full tutorial, please refer to: 14 | 15 | https://www.appcoda.com/healthkit 16 | -------------------------------------------------------------------------------- /Core Bluetooth HRM.xcodeproj/xcuserdata/softwaretesting.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Core Bluetooth HRM/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Core Bluetooth HRM/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSHealthShareUsageDescription 24 | Reason my app reads your health data 25 | NSHealthUpdateUsageDescription 26 | Reason my app makes changes to your health data 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /Core Bluetooth HRM/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /Core Bluetooth HRM/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Core Bluetooth HRM 4 | // 5 | // Created by Andrew Jaffee on 4/6/18. 6 | // 7 | /* 8 | 9 | Copyright (c) 2018 Andrew L. Jaffee, microIT Infrastructure, LLC, and iosbrain.com. 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | */ 18 | 19 | import UIKit 20 | 21 | @UIApplicationMain 22 | class AppDelegate: UIResponder, UIApplicationDelegate { 23 | 24 | var window: UIWindow? 25 | 26 | 27 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 28 | // Override point for customization after application launch. 29 | return true 30 | } 31 | 32 | func applicationWillResignActive(_ application: UIApplication) { 33 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 34 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 35 | } 36 | 37 | func applicationDidEnterBackground(_ application: UIApplication) { 38 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 39 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 40 | } 41 | 42 | func applicationWillEnterForeground(_ application: UIApplication) { 43 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 44 | } 45 | 46 | func applicationDidBecomeActive(_ application: UIApplication) { 47 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 48 | } 49 | 50 | func applicationWillTerminate(_ application: UIApplication) { 51 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 52 | } 53 | 54 | 55 | } 56 | 57 | -------------------------------------------------------------------------------- /Core Bluetooth HRM/HealthKitInterface Gender Final.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HealthKitInterface.swift 3 | // Core Bluetooth HRM 4 | // 5 | // Created by Software Testing on 4/13/18. 6 | // Copyright © 2018 Andrew Jaffee. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // STEP 1: MUST import HealthKit 12 | import HealthKit 13 | 14 | class HealthKitInterface 15 | { 16 | 17 | // STEP 2: a placeholder for a conduit to all HealthKit data 18 | let healthKitDataStore: HKHealthStore? 19 | 20 | // STEP 3: get a user's physical property that won't change 21 | let genderCharacteristic = HKCharacteristicType.characteristicType(forIdentifier: HKCharacteristicTypeIdentifier.biologicalSex) 22 | 23 | // STEP 4: for flexibility, the API allows us to ask for 24 | // multiple characteristics at once 25 | let readableHKCharacteristicTypes: Set? 26 | 27 | init() { 28 | 29 | // STEP 5: make sure HealthKit is available 30 | if HKHealthStore.isHealthDataAvailable() { 31 | 32 | // STEP 6: create one instance of the HealthKit store 33 | // per app; it's the conduit to all HealthKit data 34 | self.healthKitDataStore = HKHealthStore() 35 | 36 | // STEP 7: I create a Set of one as that's what the call wants 37 | readableHKCharacteristicTypes = [genderCharacteristic!] 38 | 39 | // STEP 8: request user permission to read gender and 40 | // then read the value asynchronously 41 | healthKitDataStore?.requestAuthorization(toShare: nil, 42 | read: readableHKCharacteristicTypes, 43 | completion: { (success, error) -> Void in 44 | if success { 45 | print("Successful authorization.") 46 | // STEP 9.1: read gender data (see below) 47 | self.readGenderType() 48 | } else { 49 | print(error.debugDescription) 50 | } 51 | }) 52 | 53 | } // end if HKHealthStore.isHealthDataAvailable() 54 | 55 | else { 56 | 57 | self.healthKitDataStore = nil 58 | readableHKCharacteristicTypes = nil 59 | 60 | } 61 | 62 | } // end init() 63 | 64 | // STEP 9.2: actual code to read gender data 65 | func readGenderType() -> Void { 66 | 67 | do { 68 | 69 | let genderType = try self.healthKitDataStore?.biologicalSex() 70 | 71 | if genderType?.biologicalSex == .female { 72 | print("Gender is female.") 73 | } 74 | else if genderType?.biologicalSex == .male { 75 | print("Gender is male.") 76 | } 77 | else { 78 | print("Gender is unspecified.") 79 | } 80 | 81 | } 82 | catch { 83 | print("Error looking up gender.") 84 | } 85 | 86 | } // end func readGenderType 87 | 88 | } // end class HealthKitInterface 89 | -------------------------------------------------------------------------------- /Core Bluetooth HRM/HealthKitInterface.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HealthKitInterface.swift 3 | // Core Bluetooth HRM 4 | // 5 | // Created by Software Testing on 4/13/18. 6 | // 7 | /* 8 | 9 | Copyright (c) 2018 Andrew L. Jaffee, microIT Infrastructure, LLC, and iosbrain.com. 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | */ 18 | 19 | import Foundation 20 | 21 | // STEP 1: MUST import HealthKit 22 | import HealthKit 23 | 24 | class HealthKitInterface 25 | { 26 | 27 | // STEP 2: a placeholder for a conduit to all HealthKit data 28 | let healthKitDataStore: HKHealthStore? 29 | 30 | // STEP 3: create member properties that we'll use to ask 31 | // if we can read and write heart rate data 32 | let readableHKQuantityTypes: Set? 33 | let writeableHKQuantityTypes: Set? 34 | 35 | init() { 36 | 37 | // STEP 4: make sure HealthKit is available 38 | if HKHealthStore.isHealthDataAvailable() { 39 | 40 | // STEP 5: create one instance of the HealthKit store 41 | // per app; it's the conduit to all HealthKit data 42 | self.healthKitDataStore = HKHealthStore() 43 | 44 | // STEP 6: create two Sets of HKQuantityTypes representing 45 | // heart rate data; one for reading, one for writing 46 | readableHKQuantityTypes = [HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate)!] 47 | writeableHKQuantityTypes = [HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate)!] 48 | 49 | // STEP 7: ask user for permission to read and write 50 | // heart rate data 51 | healthKitDataStore?.requestAuthorization(toShare: writeableHKQuantityTypes, 52 | read: readableHKQuantityTypes, 53 | completion: { (success, error) -> Void in 54 | if success { 55 | print("Successful authorization.") 56 | } else { 57 | print(error.debugDescription) 58 | } 59 | }) 60 | 61 | } // end if HKHealthStore.isHealthDataAvailable() 62 | 63 | else { 64 | 65 | self.healthKitDataStore = nil 66 | self.readableHKQuantityTypes = nil 67 | self.writeableHKQuantityTypes = nil 68 | 69 | } 70 | 71 | } // end init() 72 | 73 | // STEP 8.0: this is my wrapper for writing one heart 74 | // rate sample at a time to the HKHealthStore 75 | func writeHeartRateData( heartRate: Int ) -> Void { 76 | 77 | // STEP 8.1: "Count units are used to represent raw scalar values. They are often used to represent the number of times an event occurs" 78 | let heartRateCountUnit = HKUnit.count() 79 | // STEP 8.2: "HealthKit uses quantity objects to store numerical data. When you create a quantity, you provide both the quantity’s value and unit." 80 | // beats per minute = heart beats / minute 81 | let beatsPerMinuteQuantity = HKQuantity(unit: heartRateCountUnit.unitDivided(by: HKUnit.minute()), doubleValue: Double(heartRate)) 82 | // STEP 8.3: "HealthKit uses quantity types to create samples that store a numerical value. Use quantity type instances to create quantity samples that you can save in the HealthKit store." 83 | // Short-hand for HKQuantityTypeIdentifier.heartRate 84 | let beatsPerMinuteType = HKQuantityType.quantityType(forIdentifier: .heartRate)! 85 | // STEP 8.4: "you can use a quantity sample to record ... the user's current heart rate..." 86 | let heartRateSampleData = HKQuantitySample(type: beatsPerMinuteType, quantity: beatsPerMinuteQuantity, start: Date(), end: Date()) 87 | 88 | // STEP 8.5: "Saves an array of objects to the HealthKit store." 89 | healthKitDataStore?.save([heartRateSampleData]) { (success: Bool, error: Error?) in 90 | print("Heart rate \(heartRate) saved.") 91 | } 92 | 93 | } // end func writeHeartRateData 94 | 95 | // STEP 9.0: this is my wrapper for reading all "recent" 96 | // heart rate samples from the HKHealthStore 97 | func readHeartRateData() -> Void { 98 | 99 | // STEP 9.1: just as in STEP 6, we're telling the `HealthKitStore` 100 | // that we're interested in reading heart rate data 101 | let heartRateType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate)! 102 | 103 | // STEP 9.2: define a query for "recent" heart rate data; 104 | // in pseudo-SQL, this would look like: 105 | // 106 | // SELECT bpm FROM HealthKitStore WHERE qtyTypeID = '.heartRate'; 107 | let query = HKAnchoredObjectQuery(type: heartRateType, predicate: nil, anchor: nil, limit: HKObjectQueryNoLimit) { 108 | (query, samplesOrNil, deletedObjectsOrNil, newAnchor, errorOrNil) in 109 | 110 | if let samples = samplesOrNil { 111 | 112 | for heartRateSamples in samples { 113 | print(heartRateSamples) 114 | } 115 | 116 | } 117 | else { 118 | print("No heart rate sample available.") 119 | } 120 | 121 | } 122 | 123 | // STEP 9.3: execute the query for heart rate data 124 | healthKitDataStore?.execute(query) 125 | 126 | } // end func readHeartRateData 127 | 128 | } // end class HealthKitInterface 129 | -------------------------------------------------------------------------------- /Core Bluetooth HRM/HealthKitInterface Gender.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HealthKitInterface.swift 3 | // Core Bluetooth HRM 4 | // 5 | // Created by Software Testing on 4/13/18. 6 | // Copyright © 2018 Andrew Jaffee. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // STEP 1: MUST import HealthKit 12 | import HealthKit 13 | 14 | class HealthKitInterface 15 | { 16 | 17 | // STEP 2: a placeholder for a conduit to all HealthKit data 18 | let healthKitDataStore: HKHealthStore? 19 | 20 | // STEP 3: get a user's physical property that won't change 21 | let genderCharacteristic = HKCharacteristicType.characteristicType(forIdentifier: HKCharacteristicTypeIdentifier.biologicalSex) 22 | 23 | // STEP 4: for flexibility, the API allows us to ask for 24 | // multiple characteristics at once 25 | let readableHKCharacteristicTypes: Set? 26 | 27 | /* 28 | let readableHKDataTypes: Set? 29 | 30 | let writeableHKDataTypes: Set? 31 | */ 32 | 33 | init() { 34 | 35 | // STEP 5: make sure HealthKit is available 36 | if HKHealthStore.isHealthDataAvailable() { 37 | 38 | // STEP 6: create one instance of the HealthKit store 39 | // per app; it's the conduit to all HealthKit data 40 | self.healthKitDataStore = HKHealthStore() 41 | 42 | //readableHKDataTypes = [HKSampleType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate)!] 43 | //writeableHKDataTypes = [HKSampleType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate)!] 44 | 45 | /* 46 | healthKitDataStore?.requestAuthorization(toShare: writeableHKDataTypes, 47 | read: readableHKDataTypes, 48 | completion: { (success, error) -> Void in 49 | if success { 50 | print("Successful authorization.") 51 | } else { 52 | print(error.debugDescription) 53 | } 54 | }) 55 | 56 | */ 57 | 58 | // STEP 7: I create a Set of one as that's what the call wants 59 | readableHKCharacteristicTypes = [genderCharacteristic!] 60 | 61 | // STEP 8: request user permission to read gender and 62 | // then read the value asynchronously 63 | healthKitDataStore?.requestAuthorization(toShare: nil, 64 | read: readableHKCharacteristicTypes, 65 | completion: { (success, error) -> Void in 66 | if success { 67 | print("Successful authorization.") 68 | // STEP 9.1: read gender data (see below) 69 | self.readGenderType() 70 | } else { 71 | print(error.debugDescription) 72 | } 73 | }) 74 | } 75 | 76 | else { 77 | 78 | self.healthKitDataStore = nil 79 | //self.readableHKDataTypes = nil 80 | //self.writeableHKDataTypes = nil 81 | readableHKCharacteristicTypes = nil 82 | } 83 | 84 | } // end init() 85 | 86 | /* 87 | func writeHeartRateData( heartRate: Int ) -> Void { 88 | 89 | // "Count units are used to represent raw scalar values. They are often used to represent the number of times an event occurs—for example, the number of steps the user has taken or the number of times the user has used his or her inhaler." 90 | let heartRateCountUnit = HKUnit.count() 91 | // "HealthKit uses quantity objects to store numerical data. Quantities store a value for a given unit. You can request the value in any compatible units." 92 | // beats per minutes = heart beats / minute 93 | let beatsPerMinuteQuantity = HKQuantity(unit: heartRateCountUnit.unitDivided(by: HKUnit.minute()), doubleValue: Double(heartRate)) 94 | // "HealthKit uses quantity types to create samples that store a numerical value. Use quantity type instances to create quantity samples that you can save in the HealthKit store." 95 | // Short-hand for HKQuantityTypeIdentifier.heartRate 96 | let beatsPerMinuteType = HKQuantityType.quantityType(forIdentifier: .heartRate)! 97 | // "Each quantity sample instance represent a piece of data with a single numeric value. For example, you can use a quantity sample to record the user’s height, the user’s current heart rate" 98 | let heartRateSampleData = HKQuantitySample(type: beatsPerMinuteType, quantity: beatsPerMinuteQuantity, start: Date(), end: Date()) 99 | 100 | // "Saves an array of objects to the HealthKit store." 101 | healthKitDataStore?.save([heartRateSampleData]) { (success: Bool, error: Error?) in 102 | print("Heart rate \(heartRate) saved.") 103 | } 104 | 105 | } // end func writeHeartRateData 106 | 107 | func readHeartRateData() -> Void { 108 | 109 | let heartRateType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate)! 110 | 111 | let query = HKAnchoredObjectQuery(type: heartRateType, predicate: nil, anchor: nil, limit: HKObjectQueryNoLimit) { 112 | (query, samplesOrNil, deletedObjectsOrNil, newAnchor, errorOrNil) in 113 | 114 | if let samples = samplesOrNil { 115 | 116 | for heartRateSamples in samples { 117 | print(heartRateSamples) 118 | } 119 | 120 | } 121 | else { 122 | print("No heart rate sample available.") 123 | } 124 | 125 | } 126 | 127 | healthKitDataStore?.execute(query) 128 | 129 | } // end func readHeartRateData 130 | */ 131 | 132 | // STEP 9.2: actual code to read gender data 133 | func readGenderType() -> Void { 134 | 135 | do { 136 | 137 | let genderType = try self.healthKitDataStore?.biologicalSex() 138 | 139 | if genderType?.biologicalSex == .female { 140 | print("Gender is female.") 141 | } 142 | else if genderType?.biologicalSex == .male { 143 | print("Gender is male.") 144 | } 145 | else { 146 | print("Gender is unspecified.") 147 | } 148 | 149 | } 150 | catch { 151 | print("Error looking up gender.") 152 | } 153 | 154 | } // end func readGenderType 155 | 156 | } // end class HealthKitInterface 157 | -------------------------------------------------------------------------------- /Core Bluetooth HRM/HealthKitInterface Heart Rate Draft.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HealthKitInterface.swift 3 | // Core Bluetooth HRM 4 | // 5 | // Created by Software Testing on 4/13/18. 6 | // Copyright © 2018 Andrew Jaffee. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // STEP 1: MUST import HealthKit 12 | import HealthKit 13 | 14 | class HealthKitInterface 15 | { 16 | 17 | // STEP 2: a placeholder for a conduit to all HealthKit data 18 | let healthKitDataStore: HKHealthStore? 19 | 20 | // STEP 3: get a user's physical property that won't change 21 | let genderCharacteristic = HKCharacteristicType.characteristicType(forIdentifier: HKCharacteristicTypeIdentifier.biologicalSex) 22 | 23 | // STEP 4: for flexibility, the API allows us to ask for 24 | // multiple characteristics at once 25 | let readableHKCharacteristicTypes: Set? 26 | 27 | /* 28 | let readableHKDataTypes: Set? 29 | 30 | let writeableHKDataTypes: Set? 31 | */ 32 | 33 | init() { 34 | 35 | // STEP 5: make sure HealthKit is available 36 | if HKHealthStore.isHealthDataAvailable() { 37 | 38 | // STEP 6: create one instance of the HealthKit store 39 | // per app; it's the conduit to all HealthKit data 40 | self.healthKitDataStore = HKHealthStore() 41 | 42 | //readableHKDataTypes = [HKSampleType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate)!] 43 | //writeableHKDataTypes = [HKSampleType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate)!] 44 | 45 | /* 46 | healthKitDataStore?.requestAuthorization(toShare: writeableHKDataTypes, 47 | read: readableHKDataTypes, 48 | completion: { (success, error) -> Void in 49 | if success { 50 | print("Successful authorization.") 51 | } else { 52 | print(error.debugDescription) 53 | } 54 | }) 55 | 56 | */ 57 | 58 | // STEP 7: I create a Set of one as that's what the call wants 59 | readableHKCharacteristicTypes = [genderCharacteristic!] 60 | 61 | // STEP 8: request user permission to read gender and 62 | // then read the value asynchronously 63 | healthKitDataStore?.requestAuthorization(toShare: nil, 64 | read: readableHKCharacteristicTypes, 65 | completion: { (success, error) -> Void in 66 | if success { 67 | print("Successful authorization.") 68 | // STEP 9.1: read gender data (see below) 69 | self.readGenderType() 70 | } else { 71 | print(error.debugDescription) 72 | } 73 | }) 74 | } 75 | 76 | else { 77 | 78 | self.healthKitDataStore = nil 79 | //self.readableHKDataTypes = nil 80 | //self.writeableHKDataTypes = nil 81 | readableHKCharacteristicTypes = nil 82 | } 83 | 84 | } // end init() 85 | 86 | /* 87 | func writeHeartRateData( heartRate: Int ) -> Void { 88 | 89 | // "Count units are used to represent raw scalar values. They are often used to represent the number of times an event occurs—for example, the number of steps the user has taken or the number of times the user has used his or her inhaler." 90 | let heartRateCountUnit = HKUnit.count() 91 | // "HealthKit uses quantity objects to store numerical data. Quantities store a value for a given unit. You can request the value in any compatible units." 92 | // beats per minutes = heart beats / minute 93 | let beatsPerMinuteQuantity = HKQuantity(unit: heartRateCountUnit.unitDivided(by: HKUnit.minute()), doubleValue: Double(heartRate)) 94 | // "HealthKit uses quantity types to create samples that store a numerical value. Use quantity type instances to create quantity samples that you can save in the HealthKit store." 95 | // Short-hand for HKQuantityTypeIdentifier.heartRate 96 | let beatsPerMinuteType = HKQuantityType.quantityType(forIdentifier: .heartRate)! 97 | // "Each quantity sample instance represent a piece of data with a single numeric value. For example, you can use a quantity sample to record the user’s height, the user’s current heart rate" 98 | let heartRateSampleData = HKQuantitySample(type: beatsPerMinuteType, quantity: beatsPerMinuteQuantity, start: Date(), end: Date()) 99 | 100 | // "Saves an array of objects to the HealthKit store." 101 | healthKitDataStore?.save([heartRateSampleData]) { (success: Bool, error: Error?) in 102 | print("Heart rate \(heartRate) saved.") 103 | } 104 | 105 | } // end func writeHeartRateData 106 | 107 | func readHeartRateData() -> Void { 108 | 109 | let heartRateType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate)! 110 | 111 | let query = HKAnchoredObjectQuery(type: heartRateType, predicate: nil, anchor: nil, limit: HKObjectQueryNoLimit) { 112 | (query, samplesOrNil, deletedObjectsOrNil, newAnchor, errorOrNil) in 113 | 114 | if let samples = samplesOrNil { 115 | 116 | for heartRateSamples in samples { 117 | print(heartRateSamples) 118 | } 119 | 120 | } 121 | else { 122 | print("No heart rate sample available.") 123 | } 124 | 125 | } 126 | 127 | healthKitDataStore?.execute(query) 128 | 129 | } // end func readHeartRateData 130 | */ 131 | 132 | // STEP 9.2: actual code to read gender data 133 | func readGenderType() -> Void { 134 | 135 | do { 136 | 137 | let genderType = try self.healthKitDataStore?.biologicalSex() 138 | 139 | if genderType?.biologicalSex == .female { 140 | print("Gender is female.") 141 | } 142 | else if genderType?.biologicalSex == .male { 143 | print("Gender is male.") 144 | } 145 | else { 146 | print("Gender is unspecified.") 147 | } 148 | 149 | } 150 | catch { 151 | print("Error looking up gender.") 152 | } 153 | 154 | } // end func readGenderType 155 | 156 | } // end class HealthKitInterface 157 | -------------------------------------------------------------------------------- /Core Bluetooth HRM/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 64 | 65 | 66 | 67 | 73 | 79 | 80 | 81 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /Core Bluetooth HRM.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 026A0DB02081788E00824BE8 /* HealthKitInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026A0DAF2081788E00824BE8 /* HealthKitInterface.swift */; }; 11 | 026A0DB420817B1F00824BE8 /* HealthKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 026A0DB320817B1F00824BE8 /* HealthKit.framework */; }; 12 | 02D4A7D120780BAE00708F74 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D4A7D020780BAE00708F74 /* AppDelegate.swift */; }; 13 | 02D4A7D320780BAE00708F74 /* HeartRateMonitorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D4A7D220780BAE00708F74 /* HeartRateMonitorViewController.swift */; }; 14 | 02D4A7D620780BAE00708F74 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 02D4A7D420780BAE00708F74 /* Main.storyboard */; }; 15 | 02D4A7D820780BAE00708F74 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 02D4A7D720780BAE00708F74 /* Assets.xcassets */; }; 16 | 02D4A7DB20780BAE00708F74 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 02D4A7D920780BAE00708F74 /* LaunchScreen.storyboard */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | 026A0DAF2081788E00824BE8 /* HealthKitInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitInterface.swift; sourceTree = ""; }; 21 | 026A0DB120817B1F00824BE8 /* Core Bluetooth HRM.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Core Bluetooth HRM.entitlements"; sourceTree = ""; }; 22 | 026A0DB320817B1F00824BE8 /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = System/Library/Frameworks/HealthKit.framework; sourceTree = SDKROOT; }; 23 | 02D4A7CD20780BAE00708F74 /* Core Bluetooth HRM.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Core Bluetooth HRM.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 24 | 02D4A7D020780BAE00708F74 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 25 | 02D4A7D220780BAE00708F74 /* HeartRateMonitorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeartRateMonitorViewController.swift; sourceTree = ""; }; 26 | 02D4A7D520780BAE00708F74 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 27 | 02D4A7D720780BAE00708F74 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 28 | 02D4A7DA20780BAE00708F74 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 29 | 02D4A7DC20780BAE00708F74 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 30 | /* End PBXFileReference section */ 31 | 32 | /* Begin PBXFrameworksBuildPhase section */ 33 | 02D4A7CA20780BAE00708F74 /* Frameworks */ = { 34 | isa = PBXFrameworksBuildPhase; 35 | buildActionMask = 2147483647; 36 | files = ( 37 | 026A0DB420817B1F00824BE8 /* HealthKit.framework in Frameworks */, 38 | ); 39 | runOnlyForDeploymentPostprocessing = 0; 40 | }; 41 | /* End PBXFrameworksBuildPhase section */ 42 | 43 | /* Begin PBXGroup section */ 44 | 026A0DB220817B1F00824BE8 /* Frameworks */ = { 45 | isa = PBXGroup; 46 | children = ( 47 | 026A0DB320817B1F00824BE8 /* HealthKit.framework */, 48 | ); 49 | name = Frameworks; 50 | sourceTree = ""; 51 | }; 52 | 02D4A7C420780BAE00708F74 = { 53 | isa = PBXGroup; 54 | children = ( 55 | 02D4A7CF20780BAE00708F74 /* Core Bluetooth HRM */, 56 | 02D4A7CE20780BAE00708F74 /* Products */, 57 | 026A0DB220817B1F00824BE8 /* Frameworks */, 58 | ); 59 | sourceTree = ""; 60 | }; 61 | 02D4A7CE20780BAE00708F74 /* Products */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | 02D4A7CD20780BAE00708F74 /* Core Bluetooth HRM.app */, 65 | ); 66 | name = Products; 67 | sourceTree = ""; 68 | }; 69 | 02D4A7CF20780BAE00708F74 /* Core Bluetooth HRM */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | 026A0DB120817B1F00824BE8 /* Core Bluetooth HRM.entitlements */, 73 | 02D4A7D020780BAE00708F74 /* AppDelegate.swift */, 74 | 02D4A7D220780BAE00708F74 /* HeartRateMonitorViewController.swift */, 75 | 026A0DAF2081788E00824BE8 /* HealthKitInterface.swift */, 76 | 02D4A7D420780BAE00708F74 /* Main.storyboard */, 77 | 02D4A7D720780BAE00708F74 /* Assets.xcassets */, 78 | 02D4A7D920780BAE00708F74 /* LaunchScreen.storyboard */, 79 | 02D4A7DC20780BAE00708F74 /* Info.plist */, 80 | ); 81 | path = "Core Bluetooth HRM"; 82 | sourceTree = ""; 83 | }; 84 | /* End PBXGroup section */ 85 | 86 | /* Begin PBXNativeTarget section */ 87 | 02D4A7CC20780BAE00708F74 /* Core Bluetooth HRM */ = { 88 | isa = PBXNativeTarget; 89 | buildConfigurationList = 02D4A7DF20780BAE00708F74 /* Build configuration list for PBXNativeTarget "Core Bluetooth HRM" */; 90 | buildPhases = ( 91 | 02D4A7C920780BAE00708F74 /* Sources */, 92 | 02D4A7CA20780BAE00708F74 /* Frameworks */, 93 | 02D4A7CB20780BAE00708F74 /* Resources */, 94 | ); 95 | buildRules = ( 96 | ); 97 | dependencies = ( 98 | ); 99 | name = "Core Bluetooth HRM"; 100 | productName = "Core Bluetooth HRM"; 101 | productReference = 02D4A7CD20780BAE00708F74 /* Core Bluetooth HRM.app */; 102 | productType = "com.apple.product-type.application"; 103 | }; 104 | /* End PBXNativeTarget section */ 105 | 106 | /* Begin PBXProject section */ 107 | 02D4A7C520780BAE00708F74 /* Project object */ = { 108 | isa = PBXProject; 109 | attributes = { 110 | LastSwiftUpdateCheck = 0920; 111 | LastUpgradeCheck = 0920; 112 | ORGANIZATIONNAME = "Andrew Jaffee"; 113 | TargetAttributes = { 114 | 02D4A7CC20780BAE00708F74 = { 115 | CreatedOnToolsVersion = 9.2; 116 | ProvisioningStyle = Automatic; 117 | SystemCapabilities = { 118 | com.apple.HealthKit = { 119 | enabled = 1; 120 | }; 121 | }; 122 | }; 123 | }; 124 | }; 125 | buildConfigurationList = 02D4A7C820780BAE00708F74 /* Build configuration list for PBXProject "Core Bluetooth HRM" */; 126 | compatibilityVersion = "Xcode 8.0"; 127 | developmentRegion = en; 128 | hasScannedForEncodings = 0; 129 | knownRegions = ( 130 | en, 131 | Base, 132 | ); 133 | mainGroup = 02D4A7C420780BAE00708F74; 134 | productRefGroup = 02D4A7CE20780BAE00708F74 /* Products */; 135 | projectDirPath = ""; 136 | projectRoot = ""; 137 | targets = ( 138 | 02D4A7CC20780BAE00708F74 /* Core Bluetooth HRM */, 139 | ); 140 | }; 141 | /* End PBXProject section */ 142 | 143 | /* Begin PBXResourcesBuildPhase section */ 144 | 02D4A7CB20780BAE00708F74 /* Resources */ = { 145 | isa = PBXResourcesBuildPhase; 146 | buildActionMask = 2147483647; 147 | files = ( 148 | 02D4A7DB20780BAE00708F74 /* LaunchScreen.storyboard in Resources */, 149 | 02D4A7D820780BAE00708F74 /* Assets.xcassets in Resources */, 150 | 02D4A7D620780BAE00708F74 /* Main.storyboard in Resources */, 151 | ); 152 | runOnlyForDeploymentPostprocessing = 0; 153 | }; 154 | /* End PBXResourcesBuildPhase section */ 155 | 156 | /* Begin PBXSourcesBuildPhase section */ 157 | 02D4A7C920780BAE00708F74 /* Sources */ = { 158 | isa = PBXSourcesBuildPhase; 159 | buildActionMask = 2147483647; 160 | files = ( 161 | 026A0DB02081788E00824BE8 /* HealthKitInterface.swift in Sources */, 162 | 02D4A7D320780BAE00708F74 /* HeartRateMonitorViewController.swift in Sources */, 163 | 02D4A7D120780BAE00708F74 /* AppDelegate.swift in Sources */, 164 | ); 165 | runOnlyForDeploymentPostprocessing = 0; 166 | }; 167 | /* End PBXSourcesBuildPhase section */ 168 | 169 | /* Begin PBXVariantGroup section */ 170 | 02D4A7D420780BAE00708F74 /* Main.storyboard */ = { 171 | isa = PBXVariantGroup; 172 | children = ( 173 | 02D4A7D520780BAE00708F74 /* Base */, 174 | ); 175 | name = Main.storyboard; 176 | sourceTree = ""; 177 | }; 178 | 02D4A7D920780BAE00708F74 /* LaunchScreen.storyboard */ = { 179 | isa = PBXVariantGroup; 180 | children = ( 181 | 02D4A7DA20780BAE00708F74 /* Base */, 182 | ); 183 | name = LaunchScreen.storyboard; 184 | sourceTree = ""; 185 | }; 186 | /* End PBXVariantGroup section */ 187 | 188 | /* Begin XCBuildConfiguration section */ 189 | 02D4A7DD20780BAE00708F74 /* Debug */ = { 190 | isa = XCBuildConfiguration; 191 | buildSettings = { 192 | ALWAYS_SEARCH_USER_PATHS = NO; 193 | CLANG_ANALYZER_NONNULL = YES; 194 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 195 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 196 | CLANG_CXX_LIBRARY = "libc++"; 197 | CLANG_ENABLE_MODULES = YES; 198 | CLANG_ENABLE_OBJC_ARC = YES; 199 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 200 | CLANG_WARN_BOOL_CONVERSION = YES; 201 | CLANG_WARN_COMMA = YES; 202 | CLANG_WARN_CONSTANT_CONVERSION = YES; 203 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 204 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 205 | CLANG_WARN_EMPTY_BODY = YES; 206 | CLANG_WARN_ENUM_CONVERSION = YES; 207 | CLANG_WARN_INFINITE_RECURSION = YES; 208 | CLANG_WARN_INT_CONVERSION = YES; 209 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 210 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 211 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 212 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 213 | CLANG_WARN_STRICT_PROTOTYPES = YES; 214 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 215 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 216 | CLANG_WARN_UNREACHABLE_CODE = YES; 217 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 218 | CODE_SIGN_IDENTITY = "iPhone Developer"; 219 | COPY_PHASE_STRIP = NO; 220 | DEBUG_INFORMATION_FORMAT = dwarf; 221 | ENABLE_STRICT_OBJC_MSGSEND = YES; 222 | ENABLE_TESTABILITY = YES; 223 | GCC_C_LANGUAGE_STANDARD = gnu11; 224 | GCC_DYNAMIC_NO_PIC = NO; 225 | GCC_NO_COMMON_BLOCKS = YES; 226 | GCC_OPTIMIZATION_LEVEL = 0; 227 | GCC_PREPROCESSOR_DEFINITIONS = ( 228 | "DEBUG=1", 229 | "$(inherited)", 230 | ); 231 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 232 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 233 | GCC_WARN_UNDECLARED_SELECTOR = YES; 234 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 235 | GCC_WARN_UNUSED_FUNCTION = YES; 236 | GCC_WARN_UNUSED_VARIABLE = YES; 237 | IPHONEOS_DEPLOYMENT_TARGET = 11.2; 238 | MTL_ENABLE_DEBUG_INFO = YES; 239 | ONLY_ACTIVE_ARCH = YES; 240 | SDKROOT = iphoneos; 241 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 242 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 243 | }; 244 | name = Debug; 245 | }; 246 | 02D4A7DE20780BAE00708F74 /* Release */ = { 247 | isa = XCBuildConfiguration; 248 | buildSettings = { 249 | ALWAYS_SEARCH_USER_PATHS = NO; 250 | CLANG_ANALYZER_NONNULL = YES; 251 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 252 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 253 | CLANG_CXX_LIBRARY = "libc++"; 254 | CLANG_ENABLE_MODULES = YES; 255 | CLANG_ENABLE_OBJC_ARC = YES; 256 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 257 | CLANG_WARN_BOOL_CONVERSION = YES; 258 | CLANG_WARN_COMMA = YES; 259 | CLANG_WARN_CONSTANT_CONVERSION = YES; 260 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 261 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 262 | CLANG_WARN_EMPTY_BODY = YES; 263 | CLANG_WARN_ENUM_CONVERSION = YES; 264 | CLANG_WARN_INFINITE_RECURSION = YES; 265 | CLANG_WARN_INT_CONVERSION = YES; 266 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 267 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 268 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 269 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 270 | CLANG_WARN_STRICT_PROTOTYPES = YES; 271 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 272 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 273 | CLANG_WARN_UNREACHABLE_CODE = YES; 274 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 275 | CODE_SIGN_IDENTITY = "iPhone Developer"; 276 | COPY_PHASE_STRIP = NO; 277 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 278 | ENABLE_NS_ASSERTIONS = NO; 279 | ENABLE_STRICT_OBJC_MSGSEND = YES; 280 | GCC_C_LANGUAGE_STANDARD = gnu11; 281 | GCC_NO_COMMON_BLOCKS = YES; 282 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 283 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 284 | GCC_WARN_UNDECLARED_SELECTOR = YES; 285 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 286 | GCC_WARN_UNUSED_FUNCTION = YES; 287 | GCC_WARN_UNUSED_VARIABLE = YES; 288 | IPHONEOS_DEPLOYMENT_TARGET = 11.2; 289 | MTL_ENABLE_DEBUG_INFO = NO; 290 | SDKROOT = iphoneos; 291 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 292 | VALIDATE_PRODUCT = YES; 293 | }; 294 | name = Release; 295 | }; 296 | 02D4A7E020780BAE00708F74 /* Debug */ = { 297 | isa = XCBuildConfiguration; 298 | buildSettings = { 299 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 300 | CODE_SIGN_ENTITLEMENTS = "Core Bluetooth HRM/Core Bluetooth HRM.entitlements"; 301 | CODE_SIGN_STYLE = Automatic; 302 | DEVELOPMENT_TEAM = ""; 303 | INFOPLIST_FILE = "Core Bluetooth HRM/Info.plist"; 304 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 305 | PRODUCT_BUNDLE_IDENTIFIER = "com.joseph.Core-Bluetooth-HRM"; 306 | PRODUCT_NAME = "$(TARGET_NAME)"; 307 | SWIFT_VERSION = 4.0; 308 | TARGETED_DEVICE_FAMILY = "1,2"; 309 | }; 310 | name = Debug; 311 | }; 312 | 02D4A7E120780BAE00708F74 /* Release */ = { 313 | isa = XCBuildConfiguration; 314 | buildSettings = { 315 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 316 | CODE_SIGN_ENTITLEMENTS = "Core Bluetooth HRM/Core Bluetooth HRM.entitlements"; 317 | CODE_SIGN_STYLE = Automatic; 318 | DEVELOPMENT_TEAM = ""; 319 | INFOPLIST_FILE = "Core Bluetooth HRM/Info.plist"; 320 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 321 | PRODUCT_BUNDLE_IDENTIFIER = "com.joseph.Core-Bluetooth-HRM"; 322 | PRODUCT_NAME = "$(TARGET_NAME)"; 323 | SWIFT_VERSION = 4.0; 324 | TARGETED_DEVICE_FAMILY = "1,2"; 325 | }; 326 | name = Release; 327 | }; 328 | /* End XCBuildConfiguration section */ 329 | 330 | /* Begin XCConfigurationList section */ 331 | 02D4A7C820780BAE00708F74 /* Build configuration list for PBXProject "Core Bluetooth HRM" */ = { 332 | isa = XCConfigurationList; 333 | buildConfigurations = ( 334 | 02D4A7DD20780BAE00708F74 /* Debug */, 335 | 02D4A7DE20780BAE00708F74 /* Release */, 336 | ); 337 | defaultConfigurationIsVisible = 0; 338 | defaultConfigurationName = Release; 339 | }; 340 | 02D4A7DF20780BAE00708F74 /* Build configuration list for PBXNativeTarget "Core Bluetooth HRM" */ = { 341 | isa = XCConfigurationList; 342 | buildConfigurations = ( 343 | 02D4A7E020780BAE00708F74 /* Debug */, 344 | 02D4A7E120780BAE00708F74 /* Release */, 345 | ); 346 | defaultConfigurationIsVisible = 0; 347 | defaultConfigurationName = Release; 348 | }; 349 | /* End XCConfigurationList section */ 350 | }; 351 | rootObject = 02D4A7C520780BAE00708F74 /* Project object */; 352 | } 353 | -------------------------------------------------------------------------------- /Core Bluetooth HRM/HeartRateMonitorViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeartRateMonitorViewController.swift 3 | // Core Bluetooth HRM 4 | // 5 | // Created by Andrew L. Jaffee on 4/6/18. 6 | // 7 | /* 8 | 9 | Copyright (c) 2018 Andrew L. Jaffee, microIT Infrastructure, LLC, and iosbrain.com. 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | */ 18 | 19 | import UIKit 20 | 21 | // STEP 0.00: MUST include the CoreBluetooth framework 22 | import CoreBluetooth 23 | 24 | // STEP 0.0: specify GATT "Assigned Numbers" as 25 | // constants so they're readable and updatable 26 | 27 | // MARK: - Core Bluetooth service IDs 28 | let BLE_Heart_Rate_Service_CBUUID = CBUUID(string: "0x180D") 29 | 30 | // MARK: - Core Bluetooth characteristic IDs 31 | let BLE_Heart_Rate_Measurement_Characteristic_CBUUID = CBUUID(string: "0x2A37") 32 | let BLE_Body_Sensor_Location_Characteristic_CBUUID = CBUUID(string: "0x2A38") 33 | 34 | // STEP 0.1: this class adopts both the central and peripheral delegates 35 | // and therefore must conform to these protocols' requirements 36 | class HeartRateMonitorViewController: UIViewController, CBCentralManagerDelegate, CBPeripheralDelegate { 37 | 38 | // MARK: - Core Bluetooth class member variables 39 | 40 | // STEP 0.2: create instance variables of the 41 | // CBCentralManager and CBPeripheral so they 42 | // persist for the duration of the app's life 43 | var centralManager: CBCentralManager? 44 | var peripheralHeartRateMonitor: CBPeripheral? 45 | 46 | // MARK: - UI outlets / member variables 47 | 48 | @IBOutlet weak var connectingActivityIndicator: UIActivityIndicatorView! 49 | @IBOutlet weak var connectionStatusView: UIView! 50 | @IBOutlet weak var brandNameTextField: UITextField! 51 | @IBOutlet weak var sensorLocationTextField: UITextField! 52 | @IBOutlet weak var beatsPerMinuteLabel: UILabel! 53 | @IBOutlet weak var bluetoothOffLabel: UILabel! 54 | 55 | // HealthKit setup 56 | let healthKitInterface = HealthKitInterface() 57 | 58 | // MARK: - UIViewController delegate 59 | 60 | override func viewDidLoad() { 61 | super.viewDidLoad() 62 | // Do any additional setup after loading the view, typically from a nib. 63 | 64 | // initially, we're scanning and not connected 65 | connectingActivityIndicator.backgroundColor = UIColor.white 66 | connectingActivityIndicator.startAnimating() 67 | connectionStatusView.backgroundColor = UIColor.red 68 | brandNameTextField.text = "----" 69 | sensorLocationTextField.text = "----" 70 | beatsPerMinuteLabel.text = "---" 71 | // just in case Bluetooth is turned off 72 | bluetoothOffLabel.alpha = 0.0 73 | 74 | // STEP 1: create a concurrent background queue for the central 75 | let centralQueue: DispatchQueue = DispatchQueue(label: "com.iosbrain.centralQueueName", attributes: .concurrent) 76 | // STEP 2: create a central to scan for, connect to, 77 | // manage, and collect data from peripherals 78 | centralManager = CBCentralManager(delegate: self, queue: centralQueue) 79 | 80 | // read heart rate data from HKHealthStore 81 | // healthKitInterface.readHeartRateData() 82 | 83 | // read gender type from HKHealthStore 84 | // healthKitInterface.readGenderType() 85 | } 86 | 87 | override func didReceiveMemoryWarning() { 88 | super.didReceiveMemoryWarning() 89 | // Dispose of any resources that can be recreated. 90 | } 91 | 92 | // MARK: - CBCentralManagerDelegate methods 93 | 94 | // STEP 3.1: this method is called based on 95 | // the device's Bluetooth state; we can ONLY 96 | // scan for peripherals if Bluetooth is .poweredOn 97 | func centralManagerDidUpdateState(_ central: CBCentralManager) { 98 | 99 | switch central.state { 100 | 101 | case .unknown: 102 | print("Bluetooth status is UNKNOWN") 103 | bluetoothOffLabel.alpha = 1.0 104 | case .resetting: 105 | print("Bluetooth status is RESETTING") 106 | bluetoothOffLabel.alpha = 1.0 107 | case .unsupported: 108 | print("Bluetooth status is UNSUPPORTED") 109 | bluetoothOffLabel.alpha = 1.0 110 | case .unauthorized: 111 | print("Bluetooth status is UNAUTHORIZED") 112 | bluetoothOffLabel.alpha = 1.0 113 | case .poweredOff: 114 | print("Bluetooth status is POWERED OFF") 115 | bluetoothOffLabel.alpha = 1.0 116 | case .poweredOn: 117 | print("Bluetooth status is POWERED ON") 118 | 119 | DispatchQueue.main.async { () -> Void in 120 | self.bluetoothOffLabel.alpha = 0.0 121 | self.connectingActivityIndicator.startAnimating() 122 | } 123 | 124 | // STEP 3.2: scan for peripherals that we're interested in 125 | centralManager?.scanForPeripherals(withServices: [BLE_Heart_Rate_Service_CBUUID]) 126 | 127 | } // END switch 128 | 129 | } // END func centralManagerDidUpdateState 130 | 131 | // STEP 4.1: discover what peripheral devices OF INTEREST 132 | // are available for this app to connect to 133 | func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { 134 | 135 | print(peripheral.name!) 136 | decodePeripheralState(peripheralState: peripheral.state) 137 | // STEP 4.2: MUST store a reference to the peripheral in 138 | // class instance variable 139 | peripheralHeartRateMonitor = peripheral 140 | // STEP 4.3: since HeartRateMonitorViewController 141 | // adopts the CBPeripheralDelegate protocol, 142 | // the peripheralHeartRateMonitor must set its 143 | // delegate property to HeartRateMonitorViewController 144 | // (self) 145 | peripheralHeartRateMonitor?.delegate = self 146 | 147 | // STEP 5: stop scanning to preserve battery life; 148 | // re-scan if disconnected 149 | centralManager?.stopScan() 150 | 151 | // STEP 6: connect to the discovered peripheral of interest 152 | centralManager?.connect(peripheralHeartRateMonitor!) 153 | 154 | } // END func centralManager(... didDiscover peripheral 155 | 156 | // STEP 7: "Invoked when a connection is successfully created with a peripheral." 157 | // we can only move forwards when we know the connection 158 | // to the peripheral succeeded 159 | func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { 160 | 161 | DispatchQueue.main.async { () -> Void in 162 | 163 | self.brandNameTextField.text = peripheral.name! 164 | self.connectionStatusView.backgroundColor = UIColor.green 165 | self.beatsPerMinuteLabel.text = "---" 166 | self.sensorLocationTextField.text = "----" 167 | self.connectingActivityIndicator.stopAnimating() 168 | 169 | } 170 | 171 | // STEP 8: look for services of interest on peripheral 172 | peripheralHeartRateMonitor?.discoverServices([BLE_Heart_Rate_Service_CBUUID]) 173 | 174 | } // END func centralManager(... didConnect peripheral 175 | 176 | // STEP 15: when a peripheral disconnects, take 177 | // use-case-appropriate action 178 | func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { 179 | 180 | // print("Disconnected!") 181 | 182 | DispatchQueue.main.async { () -> Void in 183 | 184 | self.brandNameTextField.text = "----" 185 | self.connectionStatusView.backgroundColor = UIColor.red 186 | self.beatsPerMinuteLabel.text = "---" 187 | self.sensorLocationTextField.text = "----" 188 | self.connectingActivityIndicator.startAnimating() 189 | 190 | } 191 | 192 | // STEP 16: in this use-case, start scanning 193 | // for the same peripheral or another, as long 194 | // as they're HRMs, to come back online 195 | centralManager?.scanForPeripherals(withServices: [BLE_Heart_Rate_Service_CBUUID]) 196 | 197 | } // END func centralManager(... didDisconnectPeripheral peripheral 198 | 199 | // MARK: - CBPeripheralDelegate methods 200 | 201 | func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { 202 | 203 | for service in peripheral.services! { 204 | 205 | if service.uuid == BLE_Heart_Rate_Service_CBUUID { 206 | 207 | print("Service: \(service)") 208 | 209 | // STEP 9: look for characteristics of interest 210 | // within services of interest 211 | peripheral.discoverCharacteristics(nil, for: service) 212 | 213 | } 214 | 215 | } 216 | 217 | } // END func peripheral(... didDiscoverServices 218 | 219 | // STEP 10: confirm we've discovered characteristics 220 | // of interest within services of interest 221 | func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { 222 | 223 | for characteristic in service.characteristics! { 224 | print(characteristic) 225 | 226 | if characteristic.uuid == BLE_Body_Sensor_Location_Characteristic_CBUUID { 227 | 228 | // STEP 11: subscribe to a single notification 229 | // for characteristic of interest; 230 | // "When you call this method to read 231 | // the value of a characteristic, the peripheral 232 | // calls ... peripheral:didUpdateValueForCharacteristic:error: 233 | // 234 | // Read Mandatory 235 | // 236 | peripheral.readValue(for: characteristic) 237 | 238 | } 239 | 240 | if characteristic.uuid == BLE_Heart_Rate_Measurement_Characteristic_CBUUID { 241 | 242 | // STEP 11: subscribe to regular notifications 243 | // for characteristic of interest; 244 | // "When you enable notifications for the 245 | // characteristic’s value, the peripheral calls 246 | // ... peripheral(_:didUpdateValueFor:error:) 247 | // 248 | // Notify Mandatory 249 | // 250 | peripheral.setNotifyValue(true, for: characteristic) 251 | 252 | } 253 | 254 | } // END for 255 | 256 | } // END func peripheral(... didDiscoverCharacteristicsFor service 257 | 258 | // STEP 12: we're notified whenever a characteristic 259 | // value updates regularly or posts once; read and 260 | // decipher the characteristic value(s) that we've 261 | // subscribed to 262 | func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { 263 | 264 | if characteristic.uuid == BLE_Heart_Rate_Measurement_Characteristic_CBUUID { 265 | 266 | // STEP 13: we generally have to decode BLE 267 | // data into human readable format 268 | let heartRate = deriveBeatsPerMinute(using: characteristic) 269 | 270 | DispatchQueue.main.async { () -> Void in 271 | 272 | UIView.animate(withDuration: 1.0, animations: { 273 | self.beatsPerMinuteLabel.alpha = 1.0 274 | self.beatsPerMinuteLabel.text = String(heartRate) 275 | }, completion: { (true) in 276 | self.beatsPerMinuteLabel.alpha = 0.0 277 | }) 278 | 279 | } // END DispatchQueue.main.async... 280 | 281 | } // END if characteristic.uuid ==... 282 | 283 | if characteristic.uuid == BLE_Body_Sensor_Location_Characteristic_CBUUID { 284 | 285 | // STEP 14: we generally have to decode BLE 286 | // data into human readable format 287 | let sensorLocation = readSensorLocation(using: characteristic) 288 | 289 | DispatchQueue.main.async { () -> Void in 290 | self.sensorLocationTextField.text = sensorLocation 291 | } 292 | } // END if characteristic.uuid ==... 293 | 294 | } // END func peripheral(... didUpdateValueFor characteristic 295 | 296 | // MARK: - Utilities 297 | 298 | func deriveBeatsPerMinute(using heartRateMeasurementCharacteristic: CBCharacteristic) -> Int { 299 | 300 | let heartRateValue = heartRateMeasurementCharacteristic.value! 301 | // convert to an array of unsigned 8-bit integers 302 | let buffer = [UInt8](heartRateValue) 303 | 304 | // UInt8: "An 8-bit unsigned integer value type." 305 | 306 | // the first byte (8 bits) in the buffer is flags 307 | // (meta data governing the rest of the packet); 308 | // if the least significant bit (LSB) is 0, 309 | // the heart rate (bpm) is UInt8, if LSB is 1, BPM is UInt16 310 | if ((buffer[0] & 0x01) == 0) { 311 | // second byte: "Heart Rate Value Format is set to UINT8." 312 | print("BPM is UInt8") 313 | // write heart rate to HKHealthStore 314 | // healthKitInterface.writeHeartRateData(heartRate: Int(buffer[1])) 315 | return Int(buffer[1]) 316 | } else { // I've never seen this use case, so I'll 317 | // leave it to theoroticians to argue 318 | // 2nd and 3rd bytes: "Heart Rate Value Format is set to UINT16." 319 | print("BPM is UInt16") 320 | return -1 321 | } 322 | 323 | } // END func deriveBeatsPerMinute 324 | 325 | func readSensorLocation(using sensorLocationCharacteristic: CBCharacteristic) -> String { 326 | 327 | let sensorLocationValue = sensorLocationCharacteristic.value! 328 | // convert to an array of unsigned 8-bit integers 329 | let buffer = [UInt8](sensorLocationValue) 330 | var sensorLocation = "" 331 | 332 | // look at just 8 bits 333 | if buffer[0] == 1 334 | { 335 | sensorLocation = "Chest" 336 | } 337 | else if buffer[0] == 2 338 | { 339 | sensorLocation = "Wrist" 340 | } 341 | else 342 | { 343 | sensorLocation = "N/A" 344 | } 345 | 346 | return sensorLocation 347 | 348 | } // END func readSensorLocation 349 | 350 | func decodePeripheralState(peripheralState: CBPeripheralState) { 351 | 352 | switch peripheralState { 353 | case .disconnected: 354 | print("Peripheral state: disconnected") 355 | case .connected: 356 | print("Peripheral state: connected") 357 | case .connecting: 358 | print("Peripheral state: connecting") 359 | case .disconnecting: 360 | print("Peripheral state: disconnecting") 361 | } 362 | 363 | } // END func decodePeripheralState(peripheralState 364 | 365 | } // END class HeartRateMonitorViewController 366 | 367 | --------------------------------------------------------------------------------