Skip to content

Commit 8e62c42

Browse files
Bharat Medirattaps2
authored andcommitted
Create a lock screen widget (aka Today Extension) (#281)
* Initial pass at creating a lock screen widget (aka. "Today Extension") Changes to Loop: 1. Created a TodayExtensionDataManager which keeps an eye on DeviceDataManager and stores data in TodayExtensionContext 2. Added Loop to a shared app group Changes Loop TodayExtension: 1. New target 2. Reuses the HUD view from StatusTableViewController 3. Deserializes TodayExtensionContext data from share app group defaults 4. Displays basic data (currently only the current BG) in the lock view * Use MAIN_APP_BUNDLE_IDENTIFIER instead of hardcoding com.loudnate * Rename the widget to just "Loop" and remove the iOS 10.1 install requirement * Add basal, reservoir and battery data to the today extension. In addition, inject some debug data if we're running on a simulator so that we can easily test the experience. * Improve the way that TodayExtensionContext handles optional sections * Clean up the UI - wrap the StackView in a UIView - set the dimensions properly - fix all constraint issues * Add support for net basal rates in the lock screen widget Along the way, refactor the lastTempBasal visual calculations out of StatusViewController and into LoopManager so that it can be shared with TodayExtensionDataManager. * Pass sensor info along to the extension. This makes the alert icon do the right thing. Do some refactors along the way to clean things up. * Plumb the "eventual glucose" value through to a subtitle in the widget. * Internationalize the eventual glucose string properly * Clean up TodayExtensionContext innards 1. Move the store/retrieve semantics into a NSUserDefaults extension 2. Change TodayExtensionContext to implement RawRepresenable and generally follow the semantics in WatchContext 3. Fixed all cases where we were storing non-conformant data, which was resulting in serialization errors related: fixed a minor bug in TodayViewController where we were not calling completionHandler() reliably. * Rename the widget and all references to 'Loop Status Extension' * Update version number to 1.1.2 to match container. * Rename scheme to match new extension name * Remove boilerplate comments * Remove vestigial lastContext code * Change eventual glucose to be a double value instead of a string, and plumb the preferred unit through the context as well. * Squish a few small bugs in context data transfer and with preferred units. * Correctly deserialize GlucoseTrend. * Fix busted merge. * Remove unnecessary imports and frameworks to minimize exposure to frameworks that do not support application extensions (see #292) * Change GlucoseHUDView.set() to stop taking GlucoseValue This is in preparation for a future change where StatusExtensionContext will stop having knowledge of GlucoseValue so that it can stop pulling in the LoopKit framework, which is not extension-API-safe. * Elminate all framework dependencies from Loop Status Extension LoopKit and InsulinKit frameworks are not extension API safe. Switch to using common data structures that don't reference those frameworks. * Resolve blown merge. * Add HKUnit extension to get preferredMinimumFractionDigits That extension to HKUnit comes from LoopKit, which is no longer included with the Loop Status Extension. Not sure how this compiled without this fix in the past. * Back out erroneous DEVELOPMENT_TEAM values. * Fix API nit * Rely on MAIN_APP_BUNDLE_IDENTIFIER * Rename .shared to .appGroup and make it a class var (modeling .standard) * Move NetBasal calculations into its own class * Move the app group suite name into a per-target Bundle extension In addition, add the MAIN_APP_BUNDLE_IDENTIFIER into the Info.plist so that each target can access it. * Refactor the Bundle.appGroupSuiteName extension code for clarity Eliminate duplicate code where we're choosing the appropriate bundle inside the extension; instead let the caller choose the right bundle and have common code for .appGroupSuiteName. * Switch to a struct; use let instead of var * Make all context struct members constant This requires a slight restructuring of the way we initialize GlucoseContext so that we create the SensorDisplayable first if it exists. * Set the Loop Status Extension build to CURRENT_PROJECT_VERSION
1 parent 370bea1 commit 8e62c42

18 files changed

+1205
-40
lines changed

Loop Status Extension/Base.lproj/MainInterface.storyboard

Lines changed: 334 additions & 0 deletions
Large diffs are not rendered by default.

Loop Status Extension/HKUnit.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// HKUnit.swift
3+
// Loop
4+
//
5+
// Created by Bharat Mediratta on 12/2/16.
6+
// Copyright © 2016 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import HealthKit
10+
11+
public extension HKUnit {
12+
// A formatting helper for determining the preferred decimal style for a given unit
13+
// This is similar to the LoopKit HKUnit extension, but copied here so that we can
14+
// avoid a dependency on LoopKit from the Loop Status Extension.
15+
var preferredMinimumFractionDigits: Int {
16+
if self.unitString == "mg/dL" {
17+
return 0
18+
} else {
19+
return 1
20+
}
21+
}
22+
}

Loop Status Extension/Info.plist

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleDevelopmentRegion</key>
6+
<string>en</string>
7+
<key>CFBundleDisplayName</key>
8+
<string>Loop</string>
9+
<key>CFBundleExecutable</key>
10+
<string>$(EXECUTABLE_NAME)</string>
11+
<key>CFBundleIdentifier</key>
12+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
13+
<key>CFBundleInfoDictionaryVersion</key>
14+
<string>6.0</string>
15+
<key>CFBundleName</key>
16+
<string>$(PRODUCT_NAME)</string>
17+
<key>CFBundlePackageType</key>
18+
<string>XPC!</string>
19+
<key>CFBundleShortVersionString</key>
20+
<string>1.1.2</string>
21+
<key>CFBundleVersion</key>
22+
<string>$(CURRENT_PROJECT_VERSION)</string>
23+
<key>MainAppBundleIdentifier</key>
24+
<string>$(MAIN_APP_BUNDLE_IDENTIFIER)</string>
25+
<key>NSExtension</key>
26+
<dict>
27+
<key>NSExtensionMainStoryboard</key>
28+
<string>MainInterface</string>
29+
<key>NSExtensionPointIdentifier</key>
30+
<string>com.apple.widget-extension</string>
31+
</dict>
32+
</dict>
33+
</plist>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.application-groups</key>
6+
<array>
7+
<string>group.$(MAIN_APP_BUNDLE_IDENTIFIER)</string>
8+
</array>
9+
</dict>
10+
</plist>
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
//
2+
// StatusExtensionContext.swift
3+
// Loop Status Extension
4+
//
5+
// Created by Bharat Mediratta on 11/25/16.
6+
// Copyright © 2016 LoopKit Authors. All rights reserved.
7+
//
8+
// This class allows Loop to pass context data to the Loop Status Extension.
9+
10+
import Foundation
11+
import HealthKit
12+
13+
struct ReservoirContext {
14+
let startDate: Date
15+
let unitVolume: Double
16+
let capacity: Int
17+
}
18+
19+
struct LoopContext {
20+
let dosingEnabled: Bool
21+
let lastCompleted: Date?
22+
}
23+
24+
struct NetBasalContext {
25+
let rate: Double
26+
let percentage: Double
27+
let startDate: Date
28+
}
29+
30+
struct SensorDisplayableContext: SensorDisplayable {
31+
let isStateValid: Bool
32+
let stateDescription: String
33+
let trendType: GlucoseTrend?
34+
let isLocal: Bool
35+
}
36+
37+
struct GlucoseContext {
38+
let quantity: Double
39+
let startDate: Date
40+
let sensor: SensorDisplayable?
41+
}
42+
43+
final class StatusExtensionContext: NSObject, RawRepresentable {
44+
typealias RawValue = [String: Any]
45+
private let version = 1
46+
47+
var preferredUnitDisplayString: String?
48+
var latestGlucose: GlucoseContext?
49+
var reservoir: ReservoirContext?
50+
var loop: LoopContext?
51+
var netBasal: NetBasalContext?
52+
var batteryPercentage: Double?
53+
var eventualGlucose: Double?
54+
55+
override init() {
56+
super.init()
57+
}
58+
59+
required init?(rawValue: RawValue) {
60+
super.init()
61+
let raw = rawValue
62+
63+
if let preferredString = raw["preferredUnitDisplayString"] as? String,
64+
let latestValue = raw["latestGlucose_value"] as? Double,
65+
let startDate = raw["latestGlucose_startDate"] as? Date {
66+
67+
var sensor: SensorDisplayableContext? = nil
68+
if let state = raw["latestGlucose_sensor_isStateValid"] as? Bool,
69+
let desc = raw["latestGlucose_sensor_stateDescription"] as? String,
70+
let local = raw["latestGlucose_sensor_isLocal"] as? Bool {
71+
72+
var glucoseTrend: GlucoseTrend?
73+
if let trendType = raw["latestGlucose_sensor_trendType"] as? Int {
74+
glucoseTrend = GlucoseTrend(rawValue: trendType)
75+
}
76+
77+
sensor = SensorDisplayableContext(
78+
isStateValid: state,
79+
stateDescription: desc,
80+
trendType: glucoseTrend,
81+
isLocal: local)
82+
}
83+
84+
preferredUnitDisplayString = preferredString
85+
latestGlucose = GlucoseContext(
86+
quantity: latestValue,
87+
startDate: startDate,
88+
sensor: sensor)
89+
}
90+
91+
batteryPercentage = raw["batteryPercentage"] as? Double
92+
93+
if let startDate = raw["reservoir_startDate"] as? Date,
94+
let unitVolume = raw["reservoir_unitVolume"] as? Double,
95+
let capacity = raw["reservoir_capacity"] as? Int {
96+
reservoir = ReservoirContext(startDate: startDate, unitVolume: unitVolume, capacity: capacity)
97+
}
98+
99+
if let dosingEnabled = raw["loop_dosingEnabled"] as? Bool,
100+
let lastCompleted = raw["loop_lastCompleted"] as? Date {
101+
loop = LoopContext(dosingEnabled: dosingEnabled, lastCompleted: lastCompleted)
102+
}
103+
104+
if let rate = raw["netBasal_rate"] as? Double,
105+
let percentage = raw["netBasal_percentage"] as? Double,
106+
let startDate = raw["netBasal_startDate"] as? Date {
107+
netBasal = NetBasalContext(rate: rate, percentage: percentage, startDate: startDate)
108+
}
109+
110+
eventualGlucose = raw["eventualGlucose"] as? Double
111+
}
112+
113+
var rawValue: RawValue {
114+
var raw: RawValue = [
115+
"version": version
116+
]
117+
118+
raw["preferredUnitDisplayString"] = preferredUnitDisplayString
119+
120+
if let glucose = latestGlucose,
121+
preferredUnitDisplayString != nil {
122+
raw["latestGlucose_value"] = glucose.quantity
123+
raw["latestGlucose_startDate"] = glucose.startDate
124+
}
125+
126+
if let sensor = latestGlucose?.sensor {
127+
raw["latestGlucose_sensor_isStateValid"] = sensor.isStateValid
128+
raw["latestGlucose_sensor_stateDescription"] = sensor.stateDescription
129+
raw["latestGlucose_sensor_isLocal"] = sensor.isLocal
130+
131+
if let trendType = sensor.trendType {
132+
raw["latestGlucose_sensor_trendType"] = trendType.rawValue
133+
}
134+
}
135+
136+
if let batteryPercentage = batteryPercentage {
137+
raw["batteryPercentage"] = batteryPercentage
138+
}
139+
140+
if let reservoir = reservoir {
141+
raw["reservoir_startDate"] = reservoir.startDate
142+
raw["reservoir_unitVolume"] = reservoir.unitVolume
143+
raw["reservoir_capacity"] = reservoir.capacity
144+
}
145+
146+
if let loop = loop {
147+
raw["loop_dosingEnabled"] = loop.dosingEnabled
148+
raw["loop_lastCompleted"] = loop.lastCompleted
149+
}
150+
151+
if let netBasal = netBasal {
152+
raw["netBasal_rate"] = netBasal.rate
153+
raw["netBasal_percentage"] = netBasal.percentage
154+
raw["netBasal_startDate"] = netBasal.startDate
155+
}
156+
157+
if let eventualGlucose = eventualGlucose {
158+
raw["eventualGlucose"] = eventualGlucose
159+
}
160+
161+
return raw
162+
}
163+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//
2+
// StatusViewController.swift
3+
// Loop Status Extension
4+
//
5+
// Created by Bharat Mediratta on 11/25/16.
6+
// Copyright © 2016 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import UIKit
10+
import NotificationCenter
11+
import HealthKit
12+
import CoreData
13+
14+
class StatusViewController: UIViewController, NCWidgetProviding {
15+
16+
@IBOutlet weak var loopCompletionHUD: LoopCompletionHUDView!
17+
@IBOutlet weak var glucoseHUD: GlucoseHUDView!
18+
@IBOutlet weak var basalRateHUD: BasalRateHUDView!
19+
@IBOutlet weak var reservoirVolumeHUD: ReservoirVolumeHUDView!
20+
@IBOutlet weak var batteryHUD: BatteryLevelHUDView!
21+
@IBOutlet weak var subtitleLabel: UILabel!
22+
23+
override func viewDidLoad() {
24+
super.viewDidLoad()
25+
subtitleLabel.alpha = 0
26+
subtitleLabel.textColor = UIColor.secondaryLabelColor
27+
}
28+
29+
override func didReceiveMemoryWarning() {
30+
super.didReceiveMemoryWarning()
31+
}
32+
33+
func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) {
34+
guard
35+
let context = UserDefaults(suiteName: Bundle.main.appGroupSuiteName)?.statusExtensionContext
36+
else {
37+
completionHandler(NCUpdateResult.failed)
38+
return
39+
}
40+
41+
// We should never have the case where there's glucose values but no preferred
42+
// unit. However, if that case were to happen we might show quantities against
43+
// the wrong units and that could be very harmful. So unless there's a preferred
44+
// unit, assume that none of the rest of the data is reliable.
45+
guard
46+
let preferredUnitDisplayString = context.preferredUnitDisplayString
47+
else {
48+
completionHandler(NCUpdateResult.failed)
49+
return
50+
}
51+
52+
if let glucose = context.latestGlucose {
53+
glucoseHUD.set(glucoseQuantity: glucose.quantity,
54+
at: glucose.startDate,
55+
unitDisplayString: preferredUnitDisplayString,
56+
from: glucose.sensor)
57+
}
58+
59+
if let batteryPercentage = context.batteryPercentage {
60+
batteryHUD.batteryLevel = Double(batteryPercentage)
61+
}
62+
63+
if let reservoir = context.reservoir {
64+
reservoirVolumeHUD.reservoirLevel = min(1, max(0, Double(reservoir.unitVolume / Double(reservoir.capacity))))
65+
reservoirVolumeHUD.setReservoirVolume(volume: reservoir.unitVolume, at: reservoir.startDate)
66+
}
67+
68+
if let netBasal = context.netBasal {
69+
basalRateHUD.setNetBasalRate(netBasal.rate, percent: netBasal.percentage, at: netBasal.startDate)
70+
}
71+
72+
if let loop = context.loop {
73+
loopCompletionHUD.dosingEnabled = loop.dosingEnabled
74+
loopCompletionHUD.lastLoopCompleted = loop.lastCompleted
75+
}
76+
77+
if let eventualGlucose = context.eventualGlucose {
78+
let quantity = HKQuantity(unit: HKUnit(from: preferredUnitDisplayString),
79+
doubleValue: eventualGlucose.rounded())
80+
subtitleLabel.text = String(
81+
format: NSLocalizedString(
82+
"Eventually %@",
83+
comment: "The subtitle format describing eventual glucose. (1: localized glucose value description)"),
84+
String(describing: quantity))
85+
subtitleLabel.alpha = 1
86+
} else {
87+
subtitleLabel.alpha = 0
88+
}
89+
90+
// Right now we always act as if there's new data.
91+
// TODO: keep track of data changes and return .noData if necessary
92+
completionHandler(NCUpdateResult.newData)
93+
}
94+
95+
}

0 commit comments

Comments
 (0)