├── 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 |
22 |
23 |
24 |
25 |
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 |
--------------------------------------------------------------------------------