|
1 | 1 | using System; |
2 | 2 | using System.Collections.Generic; |
3 | | -using WatchKit; |
4 | | -using Foundation; |
| 3 | + |
5 | 4 | using CoreFoundation; |
| 5 | +using Foundation; |
6 | 6 | using HealthKit; |
7 | | -using ObjCRuntime; |
8 | | - |
| 7 | +using WatchKit; |
9 | 8 |
|
10 | 9 | namespace ActivityRings.ActivityRingsWatchAppExtension |
11 | 10 | { |
12 | 11 | public partial class InterfaceController : WKInterfaceController, IHKWorkoutSessionDelegate |
13 | 12 | { |
14 | | - |
15 | 13 | public HKHealthStore HealthStore { get; set; } = new HKHealthStore(); |
16 | 14 |
|
17 | 15 | public HKWorkoutSession CurrentWorkoutSession { get; set; } |
| 16 | + |
18 | 17 | public DateTime WorkoutBeginDate { get; set; } |
| 18 | + |
19 | 19 | public DateTime WorkoutEndDate { get; set; } |
| 20 | + |
20 | 21 | public bool IsWorkoutRunning { get; set; } = false; |
21 | | - public HKQuery CurrentQuery { get; set; } |
22 | | - public List<HKSample> ActiveEnergySamples { get; set; } = new List<HKSample>(); |
23 | 22 |
|
24 | | - // Start with a zero quantity. |
25 | | - public HKQuantity CurrentActiveEnergyQuantity { get; set; } = HKQuantity.FromQuantity(HKUnit.Kilocalorie, 0.0); |
| 23 | + public HKQuery CurrentQuery { get; set; } |
26 | 24 |
|
| 25 | + public List<HKSample> ActiveEnergySamples { get; set; } = new List<HKSample> (); |
27 | 26 |
|
| 27 | + // Start with a zero quantity. |
| 28 | + public HKQuantity CurrentActiveEnergyQuantity { get; set; } = HKQuantity.FromQuantity (HKUnit.Kilocalorie, 0.0); |
28 | 29 |
|
29 | | - protected InterfaceController(IntPtr handle) : base(handle) |
| 30 | + protected InterfaceController (IntPtr handle) : base (handle) |
30 | 31 | { |
31 | 32 | // Note: this .ctor should not contain any initialization logic. |
32 | 33 | } |
33 | 34 |
|
34 | | - |
35 | | - public override void WillActivate() |
| 35 | + public override void WillActivate () |
36 | 36 | { |
37 | | - // This method is called when the watch view controller is about to be visible to the user. |
38 | | - |
39 | 37 | // Only proceed if health data is available. |
40 | | - if (!HKHealthStore.IsHealthDataAvailable) { return; }; |
| 38 | + if (!HKHealthStore.IsHealthDataAvailable) |
| 39 | + return; |
41 | 40 |
|
42 | 41 | // We need to be able to write workouts, so they display as a standalone workout in the Activity app on iPhone. |
43 | 42 | // We also need to be able to write Active Energy Burned to write samples to HealthKit to later associating with our app. |
44 | 43 |
|
45 | | - var typesToShare = new NSSet(HKQuantityType.Create(HKQuantityTypeIdentifier.ActiveEnergyBurned), HKObjectType.GetWorkoutType()); |
46 | | - |
47 | | - var typesToRead = new NSSet(HKQuantityType.Create(HKQuantityTypeIdentifier.ActiveEnergyBurned)); |
| 44 | + var typesToShare = new NSSet (HKQuantityType.Create (HKQuantityTypeIdentifier.ActiveEnergyBurned), HKObjectType.GetWorkoutType ()); |
| 45 | + var typesToRead = new NSSet (HKQuantityType.Create (HKQuantityTypeIdentifier.ActiveEnergyBurned)); |
48 | 46 |
|
49 | | - HealthStore.RequestAuthorizationToShare(typesToShare, typesToRead, (bool success, NSError error) => |
50 | | - { |
| 47 | + HealthStore.RequestAuthorizationToShare (typesToShare, typesToRead, (bool success, NSError error) => { |
51 | 48 | if (error != null && !success) |
52 | | - { |
53 | | - Console.WriteLine($"You didn't allow HealthKit to access these read/write data types. In your app, try to handle this error gracefully when a user decides not to provide access. The error was: {error.LocalizedDescription}. If you're using a simulator, try it on a device."); |
54 | | - } |
| 49 | + Console.WriteLine ("You didn't allow HealthKit to access these read/write data types. " + |
| 50 | + "In your app, try to handle this error gracefully when a user decides not to provide access. " + |
| 51 | + $"The error was: {error.LocalizedDescription}. If you're using a simulator, try it on a device."); |
55 | 52 | }); |
56 | 53 | } |
57 | 54 |
|
58 | | - |
59 | | - partial void ToggleWorkout() |
| 55 | + partial void ToggleWorkout () |
60 | 56 | { |
61 | | - if (IsWorkoutRunning){ |
62 | | - if (CurrentWorkoutSession != null) |
63 | | - { |
64 | | - HealthStore.EndWorkoutSession(CurrentWorkoutSession); |
65 | | - IsWorkoutRunning = false; |
66 | | - } |
67 | | - |
68 | | - }else { |
| 57 | + if (IsWorkoutRunning && CurrentWorkoutSession != null) { |
| 58 | + HealthStore.EndWorkoutSession (CurrentWorkoutSession); |
| 59 | + IsWorkoutRunning = false; |
| 60 | + } else { |
69 | 61 | // Begin workout. |
70 | 62 | IsWorkoutRunning = true; |
71 | 63 |
|
72 | 64 | // Clear the local Active Energy Burned quantity when beginning a workout session. |
73 | | - CurrentActiveEnergyQuantity = HKQuantity.FromQuantity(HKUnit.Kilocalorie, 0.0); |
| 65 | + CurrentActiveEnergyQuantity = HKQuantity.FromQuantity (HKUnit.Kilocalorie, 0.0); |
74 | 66 |
|
75 | 67 | CurrentQuery = null; |
76 | | - ActiveEnergySamples = new List<HKSample>(); |
| 68 | + ActiveEnergySamples = new List<HKSample> (); |
77 | 69 |
|
78 | 70 | // An indoor walk workout session. There are other activity and location types available to you. |
79 | 71 |
|
80 | 72 | // Create a workout configuration |
81 | | - var configuration = new HKWorkoutConfiguration() |
82 | | - { |
| 73 | + var configuration = new HKWorkoutConfiguration { |
83 | 74 | ActivityType = HKWorkoutActivityType.Walking, |
84 | 75 | LocationType = HKWorkoutSessionLocationType.Indoor |
85 | 76 | }; |
86 | 77 |
|
87 | | - |
88 | 78 | NSError error = null; |
89 | | - CurrentWorkoutSession = new HKWorkoutSession(configuration, out error) |
90 | | - { |
| 79 | + CurrentWorkoutSession = new HKWorkoutSession (configuration, out error) { |
91 | 80 | Delegate = this |
92 | 81 | }; |
93 | | - HealthStore.StartWorkoutSession(CurrentWorkoutSession); |
94 | 82 |
|
| 83 | + HealthStore.StartWorkoutSession(CurrentWorkoutSession); |
95 | 84 | } |
96 | | - |
97 | 85 | } |
98 | 86 |
|
99 | | - |
100 | | - |
101 | | - public void SaveWorkout() |
| 87 | + public void SaveWorkout () |
102 | 88 | { |
103 | 89 | // Obtain the `HKObjectType` for active energy burned. |
104 | | - var activeEnergyType = HKQuantityType.Create(HKQuantityTypeIdentifier.ActiveEnergyBurned); |
| 90 | + var activeEnergyType = HKQuantityType.Create (HKQuantityTypeIdentifier.ActiveEnergyBurned); |
105 | 91 | if (activeEnergyType == null) return; |
106 | 92 |
|
107 | 93 | var beginDate = WorkoutBeginDate; |
108 | 94 | var endDate = WorkoutEndDate; |
109 | 95 |
|
110 | | - TimeSpan timeDifference = endDate.Subtract(beginDate); |
| 96 | + var timeDifference = endDate.Subtract (beginDate); |
111 | 97 | double duration = timeDifference.TotalSeconds; |
112 | 98 | NSDictionary metadata = null; |
113 | 99 |
|
114 | | - var workout = HKWorkout.Create(HKWorkoutActivityType.Walking, |
| 100 | + var workout = HKWorkout.Create (HKWorkoutActivityType.Walking, |
115 | 101 | (NSDate)beginDate, |
116 | 102 | (NSDate)endDate, |
117 | 103 | duration, |
118 | 104 | CurrentActiveEnergyQuantity, |
119 | | - HKQuantity.FromQuantity(HKUnit.Mile, 0.0), |
| 105 | + HKQuantity.FromQuantity (HKUnit.Mile, 0.0), |
120 | 106 | metadata); |
121 | 107 |
|
122 | 108 | var finalActiveEnergySamples = ActiveEnergySamples; |
123 | 109 |
|
124 | 110 | if (HealthStore.GetAuthorizationStatus(activeEnergyType) != HKAuthorizationStatus.SharingAuthorized || |
125 | | - HealthStore.GetAuthorizationStatus(HKObjectType.GetWorkoutType()) != HKAuthorizationStatus.SharingAuthorized |
126 | | - ) |
127 | | - { |
| 111 | + HealthStore.GetAuthorizationStatus(HKObjectType.GetWorkoutType()) != HKAuthorizationStatus.SharingAuthorized) |
128 | 112 | return; |
129 | | - } |
130 | 113 |
|
131 | | - HealthStore.SaveObject(workout, (success, error) => |
132 | | - { |
133 | | - if (!success) |
134 | | - { |
135 | | - Console.WriteLine($"An error occured saving the workout. In your app, try to handle this gracefully. The error was: {error.ToString()}."); |
| 114 | + HealthStore.SaveObject(workout, (success, error) => { |
| 115 | + if (!success) { |
| 116 | + Console.WriteLine ($"An error occured saving the workout. In your app, try to handle this gracefully. The error was: {error}."); |
136 | 117 | return; |
137 | 118 | } |
138 | 119 |
|
139 | | - if (finalActiveEnergySamples.Count > 0) |
140 | | - { |
141 | | - HealthStore.AddSamples(finalActiveEnergySamples.ToArray(), workout, (addSuccess, addError) => |
142 | | - { |
| 120 | + if (finalActiveEnergySamples.Count > 0) { |
| 121 | + HealthStore.AddSamples (finalActiveEnergySamples.ToArray (), workout, (addSuccess, addError) => { |
143 | 122 | // Handle any errors |
144 | 123 | if (addError != null) |
145 | | - Console.WriteLine($"An error occurred adding the samples. In your app, try to handle this gracefully. The error was: {error.ToString()}."); |
146 | | - |
| 124 | + Console.WriteLine ($"An error occurred adding the samples. In your app, try to handle this gracefully. The error was: {error.ToString()}."); |
147 | 125 | }); |
148 | 126 | } |
149 | | - |
150 | 127 | }); |
151 | | - |
152 | 128 | } |
153 | 129 |
|
154 | | - |
155 | | - public void BeginWorkout(DateTime beginDate) |
| 130 | + public void BeginWorkout (DateTime beginDate) |
156 | 131 | { |
157 | 132 | // Obtain the `HKObjectType` for active energy burned and the `HKUnit` for kilocalories. |
158 | | - var activeEnergyType = HKQuantityType.Create(HKQuantityTypeIdentifier.ActiveEnergyBurned); |
159 | | - if (activeEnergyType == null) return; |
| 133 | + var activeEnergyType = HKQuantityType.Create (HKQuantityTypeIdentifier.ActiveEnergyBurned); |
| 134 | + if (activeEnergyType == null) |
| 135 | + return; |
160 | 136 |
|
161 | 137 | var energyUnit = HKUnit.Kilocalorie; |
162 | 138 |
|
163 | 139 | // Update properties. |
164 | 140 | WorkoutBeginDate = beginDate; |
165 | | - workoutButton.SetTitle("End Workout"); |
| 141 | + workoutButton.SetTitle ("End Workout"); |
166 | 142 |
|
167 | 143 | // Set up a predicate to obtain only samples from the local device starting from `beginDate`. |
168 | 144 |
|
169 | | - var datePredicate = HKQuery.GetPredicateForSamples((NSDate)beginDate, null, HKQueryOptions.None); |
| 145 | + var datePredicate = HKQuery.GetPredicateForSamples ((NSDate)beginDate, null, HKQueryOptions.None); |
170 | 146 |
|
171 | | - var devices = new NSSet<HKDevice>(new HKDevice[] { HKDevice.LocalDevice }); |
| 147 | + var devices = new NSSet<HKDevice> (new HKDevice[] { HKDevice.LocalDevice }); |
172 | 148 | var devicePredicate = HKQuery.GetPredicateForObjectsFromDevices(devices); |
173 | | - var predicate = NSCompoundPredicate.CreateAndPredicate(new NSPredicate[] { datePredicate, devicePredicate }); |
174 | | - |
175 | | - |
| 149 | + var predicate = NSCompoundPredicate.CreateAndPredicate (new NSPredicate[] { datePredicate, devicePredicate }); |
176 | 150 |
|
177 | 151 | //Create a results handler to recreate the samples generated by a query of active energy samples so that they can be associated with this app in the move graph.It should be noted that if your app has different heuristics for active energy burned you can generate your own quantities rather than rely on those from the watch.The sum of your sample's quantity values should equal the energy burned value provided for the workout |
178 | | - |
179 | 152 | Action <List<HKSample>> sampleHandler; |
180 | | - sampleHandler = (List<HKSample> samples) => |
181 | | - { |
182 | | - DispatchQueue.MainQueue.DispatchAsync(delegate |
183 | | - { |
184 | | - List<HKQuantitySample> accumulatedSamples = new List<HKQuantitySample>(); |
185 | | - |
186 | | - var initialActivityEnergy = CurrentActiveEnergyQuantity.GetDoubleValue(energyUnit); |
| 153 | + sampleHandler = (List<HKSample> samples) => { |
| 154 | + DispatchQueue.MainQueue.DispatchAsync (delegate { |
| 155 | + var accumulatedSamples = new List<HKQuantitySample> (); |
187 | 156 |
|
| 157 | + var initialActivityEnergy = CurrentActiveEnergyQuantity.GetDoubleValue (energyUnit); |
188 | 158 | double accumulatedValue = initialActivityEnergy; |
189 | | - foreach (HKQuantitySample sample in samples) |
190 | | - { |
191 | | - accumulatedValue = accumulatedValue + sample.Quantity.GetDoubleValue(energyUnit); |
192 | | - var ourSample = HKQuantitySample.FromType(activeEnergyType, sample.Quantity, sample.StartDate, sample.EndDate); |
193 | | - accumulatedSamples.Add(ourSample); |
194 | | - |
| 159 | + foreach (HKQuantitySample sample in samples) { |
| 160 | + accumulatedValue = accumulatedValue + sample.Quantity.GetDoubleValue (energyUnit); |
| 161 | + var ourSample = HKQuantitySample.FromType (activeEnergyType, sample.Quantity, sample.StartDate, sample.EndDate); |
| 162 | + accumulatedSamples.Add (ourSample); |
195 | 163 | } |
196 | 164 |
|
197 | 165 | // Update the UI. |
198 | | - CurrentActiveEnergyQuantity = HKQuantity.FromQuantity(energyUnit, accumulatedValue); |
199 | | - activeEnergyBurnedLabel.SetText(String.Format("{0}", accumulatedValue)); |
| 166 | + CurrentActiveEnergyQuantity = HKQuantity.FromQuantity (energyUnit, accumulatedValue); |
| 167 | + activeEnergyBurnedLabel.SetText ($"{accumulatedValue}"); |
200 | 168 |
|
201 | 169 | // Update our samples. |
202 | | - ActiveEnergySamples.AddRange(accumulatedSamples); |
203 | | - |
204 | | - |
| 170 | + ActiveEnergySamples.AddRange (accumulatedSamples); |
205 | 171 | }); |
206 | 172 | }; |
207 | | - |
208 | 173 |
|
209 | 174 | // Create a query to report new Active Energy Burned samples to our app. |
210 | | - var activeEnergyQuery = new HKAnchoredObjectQuery(activeEnergyType, predicate, null, HKSampleQuery.NoLimit, (query, addedObjects, deletedObjects, newAnchor, error) => |
211 | | - { |
212 | | - // Valid? |
213 | | - if (error == null) |
214 | | - { |
| 175 | + var activeEnergyQuery = new HKAnchoredObjectQuery (activeEnergyType, predicate, null,HKSampleQuery.NoLimit, (query, addedObjects, deletedObjects, newAnchor, error) => { |
| 176 | + if (error == null) { |
215 | 177 | // NOTE: `deletedObjects` are not considered in the handler as there is no way to delete samples from the watch during a workout |
216 | 178 | ActiveEnergySamples = new List<HKSample>(addedObjects); |
217 | 179 | sampleHandler(ActiveEnergySamples); |
218 | 180 |
|
| 181 | + } else { |
| 182 | + Console.WriteLine ($"An error occured executing the query. In your app, try to handle this gracefully. The error was: {error}."); |
219 | 183 | } |
220 | | - else { |
221 | | - Console.WriteLine($"An error occured executing the query. In your app, try to handle this gracefully. The error was: {error.ToString()}."); |
222 | | - } |
223 | | - |
224 | 184 | }); |
225 | 185 |
|
226 | 186 | // Assign the same handler to process future samples generated while the query is still active. |
227 | | - activeEnergyQuery.UpdateHandler = (query, addedObjects, deletedObjects, newAnchor, error) => |
228 | | - { |
229 | | - |
230 | | - if (error == null) |
231 | | - { |
232 | | - |
233 | | - ActiveEnergySamples = new List<HKSample>(addedObjects); |
| 187 | + activeEnergyQuery.UpdateHandler = (query, addedObjects, deletedObjects, newAnchor, error) => { |
| 188 | + if (error == null) { |
| 189 | + ActiveEnergySamples = new List<HKSample> (addedObjects); |
234 | 190 | sampleHandler(ActiveEnergySamples); |
235 | | - |
236 | | - } |
237 | | - else { |
238 | | - Console.WriteLine($"An error occured executing the query. In your app, try to handle this gracefully. The error was: {error.ToString()}."); |
| 191 | + } else { |
| 192 | + Console.WriteLine ($"An error occured executing the query. In your app, try to handle this gracefully. The error was: {error}."); |
239 | 193 | } |
240 | | - |
241 | 194 | }; |
242 | 195 |
|
243 | 196 | // Start Query |
244 | 197 | CurrentQuery = activeEnergyQuery; |
245 | | - HealthStore.ExecuteQuery(activeEnergyQuery); |
246 | | - |
| 198 | + HealthStore.ExecuteQuery (activeEnergyQuery); |
247 | 199 | } |
248 | 200 |
|
249 | | - |
250 | | - |
251 | | - public void EndWorkout(DateTime endDate) |
| 201 | + public void EndWorkout (DateTime endDate) |
252 | 202 | { |
253 | 203 | WorkoutEndDate = endDate; |
254 | | - workoutButton.SetTitle("Begin Workout"); |
255 | | - activeEnergyBurnedLabel.SetText("0.0"); |
256 | | - if (CurrentQuery != null) |
257 | | - { |
| 204 | + workoutButton.SetTitle ("Begin Workout"); |
| 205 | + activeEnergyBurnedLabel.SetText ("0.0"); |
| 206 | + if (CurrentQuery != null) { |
258 | 207 | var query = CurrentQuery; |
259 | 208 | HealthStore.StopQuery(query); |
260 | 209 | } |
261 | | - SaveWorkout(); |
262 | | - } |
263 | 210 |
|
| 211 | + SaveWorkout (); |
| 212 | + } |
264 | 213 |
|
265 | | - public void DidChangeToState(HKWorkoutSession workoutSession, HKWorkoutSessionState toState, HKWorkoutSessionState fromState, NSDate date) |
| 214 | + public void DidChangeToState (HKWorkoutSession workoutSession, HKWorkoutSessionState toState, HKWorkoutSessionState fromState, NSDate date) |
266 | 215 | { |
267 | | - DispatchQueue.MainQueue.DispatchAsync(delegate |
268 | | - { |
| 216 | + DispatchQueue.MainQueue.DispatchAsync (delegate { |
269 | 217 | // Take action based on the change in state |
270 | | - switch (toState) |
271 | | - { |
272 | | - |
| 218 | + switch (toState) { |
273 | 219 | case HKWorkoutSessionState.Running: |
274 | | - BeginWorkout((DateTime)date); |
| 220 | + BeginWorkout ((DateTime)date); |
275 | 221 | break; |
276 | 222 | case HKWorkoutSessionState.Ended: |
277 | | - EndWorkout((DateTime)date); |
| 223 | + EndWorkout ((DateTime)date); |
278 | 224 | break; |
279 | 225 | default: |
280 | | - Console.WriteLine($"Unexpected workout session: {toState}."); |
| 226 | + Console.WriteLine ($"Unexpected workout session: {toState}."); |
281 | 227 | break; |
282 | 228 | } |
283 | 229 | }); |
284 | | - |
285 | 230 | } |
286 | 231 |
|
287 | | - public void DidFail(HKWorkoutSession workoutSession, NSError error) |
| 232 | + public void DidFail (HKWorkoutSession workoutSession, NSError error) |
288 | 233 | { |
289 | | - Console.WriteLine($"An error occured with the workout session. In your app, try to handle this gracefully. The error was: {error}."); |
| 234 | + Console.WriteLine ($"An error occured with the workout session. In your app, try to handle this gracefully. The error was: {error}."); |
290 | 235 | } |
291 | 236 | } |
292 | 237 | } |
0 commit comments