diff --git a/Loop Complications/ComplicationSnapshot.swift b/Loop Complications/ComplicationSnapshot.swift new file mode 100644 index 0000000000..0e01b44f5e --- /dev/null +++ b/Loop Complications/ComplicationSnapshot.swift @@ -0,0 +1,133 @@ +// +// ComplicationSnapshot.swift +// Loop Complications +// +// What the watch's complications show, published by the watch app into the shared app group and +// read by the Loop Complications widget extension. Compiled into both (and into LoopTests). Strings +// arrive already formatted and localized by the app — the extension links nothing of Loop's — so +// the widget only decides, per moment, which of them to show. +// + +import Foundation + +struct ComplicationSnapshot: Codable, Equatable { + /// The reading, formatted for the display unit, or the sensor's condition ("LOW", "HIGH"). + var glucoseText: String? + var trendSymbol: String? + /// Shown in place of the reading once it is stale (stock's localized "---"). + var staleGlucoseText: String = "---" + /// After this, the reading is stale (stock: reading date + LoopAlgorithm.inputDataRecencyInterval). + var glucoseStaleAt: Date? + var eventualText: String? + /// The wide inline line in stock's localized "UtilitarianLargeFlat" format + /// ("106↗ 108 8:47 PM"), and the same line with the reading stale. + var inlineText: String? + var inlineStaleText: String? + /// When the loop last completed, and where its colour changes (LoopCompletionFreshness: fresh ≤ 6 min, + /// aging ≤ 16 min, then stale) — computed by the app so the constants live in one place. + var loopDate: Date? + var freshUntil: Date? + var agingUntil: Date? + /// When the glucose graph image beside this snapshot was rendered (nil: no graph). + var graphRenderedAt: Date? + + enum Freshness { case fresh, aging, stale } + + func freshness(at date: Date) -> Freshness { + guard loopDate != nil else { return .stale } + if let fresh = freshUntil, date <= fresh { return .fresh } + if let aging = agingUntil, date <= aging { return .aging } + return .stale + } + + func glucoseIsStale(at date: Date) -> Bool { + glucoseStaleAt.map { date > $0 } ?? true + } + + /// The reading as shown at `date`: the value, or the stale marker. + func glucose(at date: Date) -> String { + glucoseIsStale(at: date) ? staleGlucoseText : (glucoseText ?? staleGlucoseText) + } + + /// The trend symbol, blank once the reading is stale (as stock). + func trend(at date: Date) -> String { + glucoseIsStale(at: date) ? "" : (trendSymbol ?? "") + } + + func inline(at date: Date) -> String { + (glucoseIsStale(at: date) ? inlineStaleText : inlineText) ?? glucose(at: date) + } + + /// The moments after `date` at which something visible changes: the reading goes stale, the loop + /// colour turns. The widget's timeline carries one entry for each, so no reload is needed for them. + func changeMoments(after date: Date) -> [Date] { + [glucoseStaleAt.map { $0.addingTimeInterval(1) }, freshUntil.map { $0.addingTimeInterval(1) }, + agingUntil.map { $0.addingTimeInterval(1) }] + .compactMap { $0 }.filter { $0 > date }.sorted() + } + + // MARK: - Shared storage + + static let defaultsKey = "ComplicationSnapshot" + static let graphFileName = "ComplicationGlucoseGraph.png" + + /// The app group both targets share, from each one's own Info.plist. + static var appGroupIdentifier: String? { + Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as? String + } + + static var sharedDefaults: UserDefaults? { appGroupIdentifier.flatMap { UserDefaults(suiteName: $0) } } + + static var graphURL: URL? { + appGroupIdentifier + .flatMap { FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: $0) }? + .appendingPathComponent(graphFileName) + } + + static func load(from defaults: UserDefaults? = sharedDefaults) -> ComplicationSnapshot? { + defaults?.data(forKey: defaultsKey).flatMap { try? JSONDecoder().decode(Self.self, from: $0) } + } + + func save(to defaults: UserDefaults? = Self.sharedDefaults) { + if let data = try? JSONEncoder().encode(self) { defaults?.set(data, forKey: Self.defaultsKey) } + } + + static var sample: ComplicationSnapshot { + let now = Date() + return ComplicationSnapshot(glucoseText: "106", trendSymbol: "↗", glucoseStaleAt: now.addingTimeInterval(15 * 60), + eventualText: "108", inlineText: "106↗ 108", inlineStaleText: "--- 108", + loopDate: now, freshUntil: now.addingTimeInterval(6 * 60), + agingUntil: now.addingTimeInterval(16 * 60), graphRenderedAt: nil) + } +} + +/// The widget kinds, shared with the watch app (its ClockKit migrator maps old complications to them). +enum LoopComplicationKind { + static let loopStatus = "LoopStatus" + static let glucoseGraph = "GlucoseGraph" + + static let all = [loopStatus, glucoseGraph] +} + +/// When the watch app should ask WidgetKit for a reload. Reloads are budgeted by the system, so one is +/// requested only when the snapshot changed and at most every `minimumInterval`; a change inside the +/// window is not lost — the owed reload is paid on the next offer once the window has passed. +struct ComplicationReloadThrottle { + var minimumInterval: TimeInterval = 5 * 60 + private(set) var lastPublished: Snapshot? + private(set) var lastReloadAt = Date.distantPast + private(set) var reloadOwed = false + + /// `save`: the snapshot changed and should be written. `reload`: ask WidgetKit now. + mutating func offer(_ snapshot: Snapshot, now: Date) -> (save: Bool, reload: Bool) { + let changed = snapshot != lastPublished + if changed { + lastPublished = snapshot + reloadOwed = true + } + guard reloadOwed, now.timeIntervalSince(lastReloadAt) >= minimumInterval else { return (changed, false) } + reloadOwed = false + lastReloadAt = now + return (changed, true) + } +} diff --git a/Loop Complications/Info.plist b/Loop Complications/Info.plist new file mode 100644 index 0000000000..d812295954 --- /dev/null +++ b/Loop Complications/Info.plist @@ -0,0 +1,31 @@ + + + + + AppGroupIdentifier + $(APP_GROUP_IDENTIFIER) + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Loop + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + $(LOOP_MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/Loop Complications/LoopComplications.entitlements b/Loop Complications/LoopComplications.entitlements new file mode 100644 index 0000000000..d9849a816d --- /dev/null +++ b/Loop Complications/LoopComplications.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + $(APP_GROUP_IDENTIFIER) + + + diff --git a/Loop Complications/LoopComplications.swift b/Loop Complications/LoopComplications.swift new file mode 100644 index 0000000000..f1d23db6f9 --- /dev/null +++ b/Loop Complications/LoopComplications.swift @@ -0,0 +1,163 @@ +// +// LoopComplications.swift +// Loop Complications (watchOS widget extension) +// +// Loop's watch complications on WidgetKit, replacing the ClockKit ones (ClockKit is deprecated and +// current watchOS no longer takes new ClockKit complications). The same two as before: +// "Loop Status" — the reading, its trend and the loop's freshness — and "Glucose Graph" — the +// reading over the chart of recent and predicted glucose. The watch app publishes a +// ComplicationSnapshot (and the graph image) to the shared app group and asks WidgetKit to reload; +// this extension only reads and lays it out. +// + +import SwiftUI +import WidgetKit + +struct ComplicationEntry: TimelineEntry { + let date: Date + let snapshot: ComplicationSnapshot? +} + +struct ComplicationProvider: TimelineProvider { + func placeholder(in context: Context) -> ComplicationEntry { + ComplicationEntry(date: Date(), snapshot: .sample) + } + + func getSnapshot(in context: Context, completion: @escaping (ComplicationEntry) -> Void) { + completion(ComplicationEntry(date: Date(), snapshot: context.isPreview ? .sample : ComplicationSnapshot.load())) + } + + /// Now, then each moment something visible changes on its own (the reading goes stale, the loop's + /// colour turns). New data arrives by the watch app asking for a reload. + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + let now = Date() + let snapshot = ComplicationSnapshot.load() + let moments = [now] + (snapshot?.changeMoments(after: now) ?? []) + completion(Timeline(entries: moments.map { ComplicationEntry(date: $0, snapshot: snapshot) }, policy: .never)) + } +} + +// MARK: - Shared look + +extension ComplicationSnapshot.Freshness { + /// The watch app's colours: its "tint" asset, the warning yellow, the HIG red. + var color: Color { + switch self { + case .fresh: return Color(red: 0.298, green: 0.851, blue: 0.392) + case .aging: return .yellow + case .stale: return Color(red: 1, green: 59 / 255, blue: 48 / 255) + } + } +} + +extension View { + /// watchOS 10+ draws a placeholder in place of any widget that does not declare a container + /// background — every family except inline. + func complicationBackground() -> some View { + containerBackground(for: .widget) { Color.clear } + } +} + +// MARK: - Loop Status + +struct LoopStatusView: View { + let entry: ComplicationEntry + @Environment(\.widgetFamily) private var family + + private var snapshot: ComplicationSnapshot { entry.snapshot ?? ComplicationSnapshot() } + private var glucose: String { snapshot.glucose(at: entry.date) } + private var trend: String { snapshot.trend(at: entry.date) } + private var freshness: ComplicationSnapshot.Freshness { snapshot.freshness(at: entry.date) } + + var body: some View { + content.complicationBackground() + } + + @ViewBuilder private var content: some View { + switch family { + case .accessoryInline: + Text(snapshot.inline(at: entry.date)) + case .accessoryCorner: + // The face curves the label along the bezel; the tip carries the loop's ring. + Circle() + .strokeBorder(freshness.color, lineWidth: 3) + .widgetAccentable() + .widgetLabel { Text(glucose + trend) } + default: + // Circular: stock's open gauge in the loop's colour, the reading inside, the trend below. + ZStack { + AccessoryWidgetBackground() + Circle() + .strokeBorder(freshness.color, lineWidth: 3) + .widgetAccentable() + VStack(spacing: -2) { + Text(glucose) + .font(.system(size: 18, weight: .semibold, design: .rounded)) + .minimumScaleFactor(0.5).lineLimit(1) + Text(trend).font(.system(size: 12, weight: .medium)) + } + .padding(4) + } + } + } +} + +struct LoopStatusWidget: Widget { + var body: some WidgetConfiguration { + StaticConfiguration(kind: LoopComplicationKind.loopStatus, provider: ComplicationProvider()) { entry in + LoopStatusView(entry: entry) + } + // Plain Strings: an interpolated literal is formatted text, which WidgetKit rejects here. + .configurationDisplayName(NSLocalizedString("Loop Status", comment: "Watch complication name: reading, trend and loop freshness")) + .description(NSLocalizedString("Your glucose, its trend and how recently Loop ran.", comment: "Watch complication description: Loop Status")) + .supportedFamilies([.accessoryInline, .accessoryCorner, .accessoryCircular]) + } +} + +// MARK: - Glucose Graph + +struct GlucoseGraphView: View { + let entry: ComplicationEntry + + private var snapshot: ComplicationSnapshot { entry.snapshot ?? ComplicationSnapshot() } + + var body: some View { + VStack(alignment: .leading, spacing: 1) { + HStack(spacing: 4) { + Text(snapshot.glucose(at: entry.date) + snapshot.trend(at: entry.date)) + .font(.headline) + .widgetAccentable() + if let loopDate = snapshot.loopDate { + Text(loopDate, style: .relative) + .font(.caption2) + .foregroundStyle(snapshot.freshness(at: entry.date).color) + } + } + if snapshot.graphRenderedAt != nil, let url = ComplicationSnapshot.graphURL, + let image = UIImage(contentsOfFile: url.path) { + Image(uiImage: image).resizable().aspectRatio(contentMode: .fit) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .complicationBackground() + } +} + +struct GlucoseGraphWidget: Widget { + var body: some WidgetConfiguration { + StaticConfiguration(kind: LoopComplicationKind.glucoseGraph, provider: ComplicationProvider()) { entry in + GlucoseGraphView(entry: entry) + } + .configurationDisplayName(NSLocalizedString("Glucose Graph", comment: "Watch complication name: glucose chart")) + .description(NSLocalizedString("Your glucose over the last two hours and Loop's prediction.", comment: "Watch complication description: Glucose Graph")) + .supportedFamilies([.accessoryRectangular]) + } +} + +@main +struct LoopComplicationsBundle: WidgetBundle { + var body: some Widget { + LoopStatusWidget() + GlucoseGraphWidget() + } +} diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 3c96457524..a21617bbb8 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -7,9 +7,10 @@ objects = { /* Begin PBXBuildFile section */ - 01414522FC033F2B7A9B223A /* Loop/Resources/Sounds/crying.caf in Resources */ = {isa = PBXBuildFile; fileRef = 85203550670F2E9BE92F27D8 /* Loop/Resources/Sounds/crying.caf */; }; - 0A02E77AD7536C322933D49B /* Loop/Resources/Sounds/minimal.caf in Resources */ = {isa = PBXBuildFile; fileRef = 74145BE3C46270883D295A1F /* Loop/Resources/Sounds/minimal.caf */; }; - 124B300527D84BE2A1233A91 /* Loop/Resources/Sounds/urgent_low.caf in Resources */ = {isa = PBXBuildFile; fileRef = C9EA3F48338B49BD9C6BD0F9 /* Loop/Resources/Sounds/urgent_low.caf */; }; + 01414522FC033F2B7A9B223A /* crying.caf in Resources */ = {isa = PBXBuildFile; fileRef = 85203550670F2E9BE92F27D8 /* crying.caf */; }; + 0956C3313D5165B71E491FFA /* ComplicationSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCF57BFE5DDB939285839898 /* ComplicationSnapshot.swift */; }; + 0A02E77AD7536C322933D49B /* minimal.caf in Resources */ = {isa = PBXBuildFile; fileRef = 74145BE3C46270883D295A1F /* minimal.caf */; }; + 124B300527D84BE2A1233A91 /* urgent_low.caf in Resources */ = {isa = PBXBuildFile; fileRef = C9EA3F48338B49BD9C6BD0F9 /* urgent_low.caf */; }; 1419606928D9554E00BA86E0 /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 437AFEE6203688CF008C4892 /* LoopKitUI.framework */; }; 1419606A28D955BC00BA86E0 /* MockKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C101947127DD473C004E7EB8 /* MockKitUI.framework */; }; 142CB7592A60BF2E0075748A /* EditMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142CB7582A60BF2E0075748A /* EditMode.swift */; }; @@ -26,8 +27,6 @@ 147EFE8E2A8BCC5500272438 /* DefaultAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 147EFE8D2A8BCC5500272438 /* DefaultAssets.xcassets */; }; 147EFE902A8BCD8000272438 /* DerivedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 147EFE8F2A8BCD8000272438 /* DerivedAssets.xcassets */; }; 147EFE922A8BCD8A00272438 /* DerivedAssetsBase.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 147EFE912A8BCD8A00272438 /* DerivedAssetsBase.xcassets */; }; - 1F0000012FBA000100000001 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 1F0000002FBA000100000001 /* AppIcon.icon */; }; - 1F0000022FBA000100000001 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 1F0000002FBA000100000001 /* AppIcon.icon */; }; 1481F9BB28DA26F4004C5AEB /* LoopUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F75288B1DFE1DC600C322D6 /* LoopUI.framework */; }; 149A28BB2A853E5100052EDF /* CarbEntryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149A28BA2A853E5100052EDF /* CarbEntryViewModel.swift */; }; 149A28BD2A853E6C00052EDF /* CarbEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149A28BC2A853E6C00052EDF /* CarbEntryView.swift */; }; @@ -61,7 +60,7 @@ 14C970862C5C358C00E8A01B /* FavoriteFoodInsightsChartsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C970852C5C358C00E8A01B /* FavoriteFoodInsightsChartsView.swift */; }; 14D906F42A846510006EB79A /* FavoriteFoodsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D906F32A846510006EB79A /* FavoriteFoodsViewModel.swift */; }; 14ED83F62C6421F9008B4A5C /* FavoriteFoodInsightsCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14ED83F52C6421F9008B4A5C /* FavoriteFoodInsightsCardView.swift */; }; - 16E5A3BC4D931CE6AED18518 /* Loop/Resources/Sounds/trill.caf in Resources */ = {isa = PBXBuildFile; fileRef = 21F13EB666EBFDD03F99CB7E /* Loop/Resources/Sounds/trill.caf */; }; + 16E5A3BC4D931CE6AED18518 /* trill.caf in Resources */ = {isa = PBXBuildFile; fileRef = 21F13EB666EBFDD03F99CB7E /* trill.caf */; }; 1D05219B2469E9DF000EBBDE /* StoredAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D05219A2469E9DF000EBBDE /* StoredAlert.swift */; }; 1D05219D2469F1F5000EBBDE /* AlertStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D05219C2469F1F5000EBBDE /* AlertStore.swift */; }; 1D080CBD2473214A00356610 /* AlertStore.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1D080CBB2473214A00356610 /* AlertStore.xcdatamodeld */; }; @@ -88,9 +87,13 @@ 1DDE273E24AEA4B000796622 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE09BA824A3E23F009EE9F9 /* SettingsView.swift */; }; 1DDE274024AEA4F200796622 /* NotificationsCriticalAlertPermissionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA46B5F2492E2E300D71A63 /* NotificationsCriticalAlertPermissionsView.swift */; }; 1DFE9E172447B6270082C280 /* UserNotificationAlertSchedulerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFE9E162447B6270082C280 /* UserNotificationAlertSchedulerTests.swift */; }; - 230BE095719B8D8969B32A9C /* Loop/Resources/Sounds/bloop.caf in Resources */ = {isa = PBXBuildFile; fileRef = AF16A5EDEB9DF756890E0C09 /* Loop/Resources/Sounds/bloop.caf */; }; - 2C694BFD5806E573AFAB5E2E /* Loop/Resources/Sounds/dings.caf in Resources */ = {isa = PBXBuildFile; fileRef = 62F7CB5D5DC337E705032AC7 /* Loop/Resources/Sounds/dings.caf */; }; - 33644743F20B3706E3107FAC /* Loop/Resources/Sounds/mood_synth.caf in Resources */ = {isa = PBXBuildFile; fileRef = BEC0BA802BE613F144AC3A70 /* Loop/Resources/Sounds/mood_synth.caf */; }; + 1F0000012FBA000100000001 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 1F0000002FBA000100000001 /* AppIcon.icon */; }; + 1F0000022FBA000100000001 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 1F0000002FBA000100000001 /* AppIcon.icon */; }; + 230BE095719B8D8969B32A9C /* bloop.caf in Resources */ = {isa = PBXBuildFile; fileRef = AF16A5EDEB9DF756890E0C09 /* bloop.caf */; }; + 2615C162995594E8CF144CB2 /* ComplicationSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8AF2728D70D69B00A4F38E /* ComplicationSnapshotTests.swift */; }; + 2C694BFD5806E573AFAB5E2E /* dings.caf in Resources */ = {isa = PBXBuildFile; fileRef = 62F7CB5D5DC337E705032AC7 /* dings.caf */; }; + 33644743F20B3706E3107FAC /* mood_synth.caf in Resources */ = {isa = PBXBuildFile; fileRef = BEC0BA802BE613F144AC3A70 /* mood_synth.caf */; }; + 3E1A1B91ED0E8C8F5663988E /* LoopComplications.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C8C0BEB6A6B9A65E0D56AF /* LoopComplications.swift */; }; 3ED3198A2EB659E600820BCF /* GlucoseLiveActivityConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ED319882EB659E600820BCF /* GlucoseLiveActivityConfiguration.swift */; }; 3ED3198B2EB659E600820BCF /* ChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ED319872EB659E600820BCF /* ChartView.swift */; }; 3ED3198C2EB659E600820BCF /* BasalViewActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ED319862EB659E600820BCF /* BasalViewActivity.swift */; }; @@ -106,6 +109,7 @@ 3ED3199F2EB65AFE00820BCF /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ED3199E2EB65AFE00820BCF /* LocalizedString.swift */; }; 3ED319A12EB65B4100820BCF /* Bootstrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ED319A02EB65B4100820BCF /* Bootstrap.swift */; }; 3ED319A32EB65DA800820BCF /* LiveActivityManagerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ED319A22EB65DA300820BCF /* LiveActivityManagerProxy.swift */; }; + 40ACF455B8FB3B38F2D8C0CA /* ComplicationSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCF57BFE5DDB939285839898 /* ComplicationSnapshot.swift */; }; 43027F0F1DFE0EC900C51989 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F526D5E1DF2459000A04910 /* HKUnit.swift */; }; 4302F4E11D4E9C8900F0FCAF /* TextFieldTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4302F4E01D4E9C8900F0FCAF /* TextFieldTableViewController.swift */; }; 4302F4E31D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */; }; @@ -233,9 +237,11 @@ 4FC8C8011DEB93E400A1452E /* NSUserDefaults+StatusExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC8C8001DEB93E400A1452E /* NSUserDefaults+StatusExtension.swift */; }; 4FDDD23720DC51DF00D04B16 /* LoopDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FDDD23620DC51DF00D04B16 /* LoopDataManager.swift */; }; 4FF4D0F81E1725B000846527 /* NibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434F54561D287FDB002A9274 /* NibLoadable.swift */; }; + 59C09A9513767F582B35F7F9 /* Loop Complications.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = AC0853072224AE8CC4C388E7 /* Loop Complications.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 6C089A6A6A8AB8F5D835C9F1 /* ComplicationSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCF57BFE5DDB939285839898 /* ComplicationSnapshot.swift */; }; 7D23667D21250C7E0028B67D /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D23667C21250C7E0028B67D /* LocalizedString.swift */; }; 7E69CFFC2A16A77E00203CBD /* ResetLoopManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E69CFFB2A16A77E00203CBD /* ResetLoopManager.swift */; }; - 824A01CBE97A4F7A9D32F7D6 /* Loop/Resources/Sounds/clear_chimes.caf in Resources */ = {isa = PBXBuildFile; fileRef = 4A645E0E2DDA534A635C9201 /* Loop/Resources/Sounds/clear_chimes.caf */; }; + 824A01CBE97A4F7A9D32F7D6 /* clear_chimes.caf in Resources */ = {isa = PBXBuildFile; fileRef = 4A645E0E2DDA534A635C9201 /* clear_chimes.caf */; }; 841306BB2F7F0D9C00AF0320 /* ReferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841306BA2F7F0D9C00AF0320 /* ReferencesView.swift */; }; 84213C752D932F0F00642E78 /* InsulinDeliveryLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84213C742D932F0F00642E78 /* InsulinDeliveryLog.swift */; }; 84213C892D94941000642E78 /* InsulinDeliveryLogEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84213C882D94941000642E78 /* InsulinDeliveryLogEvent.swift */; }; @@ -331,12 +337,13 @@ 89F9119424358E4500ECCAF3 /* CarbAbsorptionTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89F9119324358E4500ECCAF3 /* CarbAbsorptionTime.swift */; }; 89F9119624358E6900ECCAF3 /* BolusPickerValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89F9119524358E6900ECCAF3 /* BolusPickerValues.swift */; }; 89FE21AD24AC57E30033F501 /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89FE21AC24AC57E30033F501 /* Collection.swift */; }; - 9960C6964DD51D81093FAD5D /* Loop/Resources/Sounds/chime.caf in Resources */ = {isa = PBXBuildFile; fileRef = 3F7A1D72697082DC3A14587E /* Loop/Resources/Sounds/chime.caf */; }; - 9B8FCBDD6E3A9E603C4AABAD /* Loop/Resources/Sounds/bright_alarm.caf in Resources */ = {isa = PBXBuildFile; fileRef = 270F994CD56D566028B3CB25 /* Loop/Resources/Sounds/bright_alarm.caf */; }; + 9960C6964DD51D81093FAD5D /* chime.caf in Resources */ = {isa = PBXBuildFile; fileRef = 3F7A1D72697082DC3A14587E /* chime.caf */; }; + 9B8FCBDD6E3A9E603C4AABAD /* bright_alarm.caf in Resources */ = {isa = PBXBuildFile; fileRef = 270F994CD56D566028B3CB25 /* bright_alarm.caf */; }; A1B2C3D40000000000000002 /* GlucoseAlertManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D40000000000000001 /* GlucoseAlertManagerTests.swift */; }; - A2779286E117BD3627DA0618 /* Loop/Resources/Sounds/simple.caf in Resources */ = {isa = PBXBuildFile; fileRef = 5181B60CBF3CB2DA537F0475 /* Loop/Resources/Sounds/simple.caf */; }; - A413B305CB4C4009DA09BE57 /* Loop/Resources/Sounds/bloom.caf in Resources */ = {isa = PBXBuildFile; fileRef = 8FB2394478FF679F436E8EF1 /* Loop/Resources/Sounds/bloom.caf */; }; - A4574F416AD62A1FCDE36D7D /* Loop/Resources/Sounds/high_chimes.caf in Resources */ = {isa = PBXBuildFile; fileRef = 491F8F0D89972E9D8903CFA7 /* Loop/Resources/Sounds/high_chimes.caf */; }; + A2779286E117BD3627DA0618 /* simple.caf in Resources */ = {isa = PBXBuildFile; fileRef = 5181B60CBF3CB2DA537F0475 /* simple.caf */; }; + A413B305CB4C4009DA09BE57 /* bloom.caf in Resources */ = {isa = PBXBuildFile; fileRef = 8FB2394478FF679F436E8EF1 /* bloom.caf */; }; + A44984BCF1848A652E02CE8C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A198971FAA313AF6CDD1ACDF /* Foundation.framework */; }; + A4574F416AD62A1FCDE36D7D /* high_chimes.caf in Resources */ = {isa = PBXBuildFile; fileRef = 491F8F0D89972E9D8903CFA7 /* high_chimes.caf */; }; A91D2A3F26CF0FF80023B075 /* IconTitleSubtitleTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91D2A3E26CF0FF80023B075 /* IconTitleSubtitleTableViewCell.swift */; }; A91E4C2124F867A700BE9213 /* StoredAlertTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91E4C2024F867A700BE9213 /* StoredAlertTests.swift */; }; A91E4C2324F86F1000BE9213 /* CriticalEventLogExportManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91E4C2224F86F1000BE9213 /* CriticalEventLogExportManagerTests.swift */; }; @@ -590,17 +597,17 @@ C9A47B0180AAB1FC572D3EFE /* CriticalAlertAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A47B0175B13A9543390BC5 /* CriticalAlertAudioPlayer.swift */; }; C9A47B0280AAB1FC572D3EFF /* GlucoseAlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A47B0275B13A9543390BC6 /* GlucoseAlertManager.swift */; }; C9A47B0380AAB1FC572D3F00 /* GlucoseAlertSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A47B0375B13A9543390BC7 /* GlucoseAlertSettingsView.swift */; }; - C9A47B0480AAB1FC572D3F01 /* Loop/Resources/Sounds/critical.caf in Resources */ = {isa = PBXBuildFile; fileRef = C9A47B0475B13A9543390BC8 /* Loop/Resources/Sounds/critical.caf */; }; + C9A47B0480AAB1FC572D3F01 /* critical.caf in Resources */ = {isa = PBXBuildFile; fileRef = C9A47B0475B13A9543390BC8 /* critical.caf */; }; C9SCENED0000000000000001 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9SCENED0000000000000002 /* SceneDelegate.swift */; }; - CB1AF8155B1761A30ACA5591 /* Loop/Resources/Sounds/synth.caf in Resources */ = {isa = PBXBuildFile; fileRef = 83081FFB0497A4596F597830 /* Loop/Resources/Sounds/synth.caf */; }; - D6E28676C2C7751C77C7F2FB /* Loop/Resources/Sounds/spring.caf in Resources */ = {isa = PBXBuildFile; fileRef = 9471E156FC10D6D0FF219C77 /* Loop/Resources/Sounds/spring.caf */; }; + CB1AF8155B1761A30ACA5591 /* synth.caf in Resources */ = {isa = PBXBuildFile; fileRef = 83081FFB0497A4596F597830 /* synth.caf */; }; + D6E28676C2C7751C77C7F2FB /* spring.caf in Resources */ = {isa = PBXBuildFile; fileRef = 9471E156FC10D6D0FF219C77 /* spring.caf */; }; DD3DBD292A33AFE9000F8B5B /* IntegralRetrospectiveCorrectionSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3DBD282A33AFE9000F8B5B /* IntegralRetrospectiveCorrectionSelectionView.swift */; }; DDC389F62A2B61750066E2E8 /* ApplicationFactorStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC389F52A2B61750066E2E8 /* ApplicationFactorStrategy.swift */; }; DDC389F82A2B620B0066E2E8 /* GlucoseBasedApplicationFactorStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC389F72A2B620B0066E2E8 /* GlucoseBasedApplicationFactorStrategy.swift */; }; DDC389FA2A2B62470066E2E8 /* ConstantApplicationFactorStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC389F92A2B62470066E2E8 /* ConstantApplicationFactorStrategy.swift */; }; DDC389FC2A2BC6670066E2E8 /* SettingsView+algorithmExperimentsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC389FB2A2BC6670066E2E8 /* SettingsView+algorithmExperimentsSection.swift */; }; DDC389FE2A2C4C830066E2E8 /* GlucoseBasedApplicationFactorSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC389FD2A2C4C830066E2E8 /* GlucoseBasedApplicationFactorSelectionView.swift */; }; - DE965737FE2DAE9FE692769E /* Loop/Resources/Sounds/alarm.caf in Resources */ = {isa = PBXBuildFile; fileRef = 0FC34B523FE5A50C920506D5 /* Loop/Resources/Sounds/alarm.caf */; }; + DE965737FE2DAE9FE692769E /* alarm.caf in Resources */ = {isa = PBXBuildFile; fileRef = 0FC34B523FE5A50C920506D5 /* alarm.caf */; }; E93E865424DB6CBA00FF40C8 /* retrospective_output.json in Resources */ = {isa = PBXBuildFile; fileRef = E93E865324DB6CBA00FF40C8 /* retrospective_output.json */; }; E93E865624DB731900FF40C8 /* predicted_glucose_without_retrospective.json in Resources */ = {isa = PBXBuildFile; fileRef = E93E865524DB731900FF40C8 /* predicted_glucose_without_retrospective.json */; }; E93E865824DB75BE00FF40C8 /* predicted_glucose_very_negative.json in Resources */ = {isa = PBXBuildFile; fileRef = E93E865724DB75BD00FF40C8 /* predicted_glucose_very_negative.json */; }; @@ -645,13 +652,14 @@ E9C58A7E24DB529A00487A17 /* dynamic_glucose_effect_partially_observed.json in Resources */ = {isa = PBXBuildFile; fileRef = E9C58A7924DB529A00487A17 /* dynamic_glucose_effect_partially_observed.json */; }; E9C58A7F24DB529A00487A17 /* counteraction_effect_falling_glucose.json in Resources */ = {isa = PBXBuildFile; fileRef = E9C58A7A24DB529A00487A17 /* counteraction_effect_falling_glucose.json */; }; E9C58A8024DB529A00487A17 /* insulin_effect.json in Resources */ = {isa = PBXBuildFile; fileRef = E9C58A7B24DB529A00487A17 /* insulin_effect.json */; }; - FA37F6EC10FF02D4F985D724 /* Loop/Resources/Sounds/honk.caf in Resources */ = {isa = PBXBuildFile; fileRef = 6D5FCC7207E50ACC682E9E6E /* Loop/Resources/Sounds/honk.caf */; }; + EB125CD137FD31CD1EFAD0E0 /* ComplicationPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6515A16900D12040FF31E279 /* ComplicationPublisher.swift */; }; + FA37F6EC10FF02D4F985D724 /* honk.caf in Resources */ = {isa = PBXBuildFile; fileRef = 6D5FCC7207E50ACC682E9E6E /* honk.caf */; }; FADE0001000000000000DE02 /* DevelopmentBranchAlerter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADE0001000000000000DE01 /* DevelopmentBranchAlerter.swift */; }; FEEDB0000000000000000000 /* GlucoseStatistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEDF0000000000000000000 /* GlucoseStatistics.swift */; }; FEEDB0000000000000000001 /* StatisticsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEDF0000000000000000001 /* StatisticsViewModel.swift */; }; - FEEDB0000000000000000002 /* Statistics/StatisticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEDF0000000000000000002 /* Statistics/StatisticsView.swift */; }; - FEEDB0000000000000000003 /* Statistics/AGPChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEDF0000000000000000003 /* Statistics/AGPChartView.swift */; }; - FEEDB0000000000000000004 /* Statistics/TimeInRangeBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEDF0000000000000000004 /* Statistics/TimeInRangeBar.swift */; }; + FEEDB0000000000000000002 /* StatisticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEDF0000000000000000002 /* StatisticsView.swift */; }; + FEEDB0000000000000000003 /* AGPChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEDF0000000000000000003 /* AGPChartView.swift */; }; + FEEDB0000000000000000004 /* TimeInRangeBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEDF0000000000000000004 /* TimeInRangeBar.swift */; }; FEEDB0000000000000000005 /* GlucoseStatisticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEDF0000000000000000005 /* GlucoseStatisticsTests.swift */; }; /* End PBXBuildFile section */ @@ -670,6 +678,13 @@ remoteGlobalIDString = 14B1735B28AED9EC006CCD7C; remoteInfo = SmallStatusWidgetExtension; }; + 1B21307ABB67B1D1DACE2D25 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 43776F841B8022E90074EA36 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 27B118E7F17401410F70FACA; + remoteInfo = "Loop Complications"; + }; 43A943921B926B7B0051FA24 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 43776F841B8022E90074EA36 /* Project object */; @@ -787,10 +802,21 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + ECA50E3AF74E45D0C77CA79C /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 59C09A9513767F582B35F7F9 /* Loop Complications.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 0FC34B523FE5A50C920506D5 /* Loop/Resources/Sounds/alarm.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/alarm.caf; sourceTree = SOURCE_ROOT; }; + 0FC34B523FE5A50C920506D5 /* alarm.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/alarm.caf; sourceTree = SOURCE_ROOT; }; 142CB7582A60BF2E0075748A /* EditMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditMode.swift; sourceTree = ""; }; 142CB75A2A60BFC30075748A /* FavoriteFoodsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteFoodsView.swift; sourceTree = ""; }; 1452F4A82A851C9400F8B9E4 /* FavoriteFoodAddEditViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteFoodAddEditViewModel.swift; sourceTree = ""; }; @@ -803,7 +829,6 @@ 147EFE8D2A8BCC5500272438 /* DefaultAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DefaultAssets.xcassets; sourceTree = ""; }; 147EFE8F2A8BCD8000272438 /* DerivedAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DerivedAssets.xcassets; sourceTree = ""; }; 147EFE912A8BCD8A00272438 /* DerivedAssetsBase.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DerivedAssetsBase.xcassets; sourceTree = ""; }; - 1F0000002FBA000100000001 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = ""; }; 149A28BA2A853E5100052EDF /* CarbEntryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbEntryViewModel.swift; sourceTree = ""; }; 149A28BC2A853E6C00052EDF /* CarbEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbEntryView.swift; sourceTree = ""; }; 149A28E32A8A63A700052EDF /* FavoriteFoodDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteFoodDetailView.swift; sourceTree = ""; }; @@ -851,8 +876,10 @@ 1DC63E7325351BDF004605DA /* TrueTime.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TrueTime.framework; path = Carthage/Build/iOS/TrueTime.framework; sourceTree = ""; }; 1DE09BA824A3E23F009EE9F9 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 1DFE9E162447B6270082C280 /* UserNotificationAlertSchedulerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationAlertSchedulerTests.swift; sourceTree = ""; }; - 21F13EB666EBFDD03F99CB7E /* Loop/Resources/Sounds/trill.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/trill.caf; sourceTree = SOURCE_ROOT; }; - 270F994CD56D566028B3CB25 /* Loop/Resources/Sounds/bright_alarm.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/bright_alarm.caf; sourceTree = SOURCE_ROOT; }; + 1F0000002FBA000100000001 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = ""; }; + 21F13EB666EBFDD03F99CB7E /* trill.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/trill.caf; sourceTree = SOURCE_ROOT; }; + 270F994CD56D566028B3CB25 /* bright_alarm.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/bright_alarm.caf; sourceTree = SOURCE_ROOT; }; + 2CA0E8DC01120F1A9593061C /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3ED319862EB659E600820BCF /* BasalViewActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasalViewActivity.swift; sourceTree = ""; }; 3ED319872EB659E600820BCF /* ChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartView.swift; sourceTree = ""; }; 3ED319882EB659E600820BCF /* GlucoseLiveActivityConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseLiveActivityConfiguration.swift; sourceTree = ""; }; @@ -866,7 +893,7 @@ 3ED3199E2EB65AFE00820BCF /* LocalizedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedString.swift; sourceTree = ""; }; 3ED319A02EB65B4100820BCF /* Bootstrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bootstrap.swift; sourceTree = ""; }; 3ED319A22EB65DA300820BCF /* LiveActivityManagerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManagerProxy.swift; sourceTree = ""; }; - 3F7A1D72697082DC3A14587E /* Loop/Resources/Sounds/chime.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/chime.caf; sourceTree = SOURCE_ROOT; }; + 3F7A1D72697082DC3A14587E /* chime.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/chime.caf; sourceTree = SOURCE_ROOT; }; 4302F4E01D4E9C8900F0FCAF /* TextFieldTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldTableViewController.swift; sourceTree = ""; }; 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsulinDeliveryTableViewController.swift; sourceTree = ""; }; 430B29892041F54A00BA9F93 /* NSUserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSUserDefaults.swift; sourceTree = ""; }; @@ -1001,8 +1028,8 @@ 43F89CA222BDFBBC006BB54E /* UIActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIActivityIndicatorView.swift; sourceTree = ""; }; 43FCEEA8221A615B0013DD30 /* StatusChartsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusChartsManager.swift; sourceTree = ""; }; 43FCEEAC221A66780013DD30 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = ""; }; - 491F8F0D89972E9D8903CFA7 /* Loop/Resources/Sounds/high_chimes.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/high_chimes.caf; sourceTree = SOURCE_ROOT; }; - 4A645E0E2DDA534A635C9201 /* Loop/Resources/Sounds/clear_chimes.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/clear_chimes.caf; sourceTree = SOURCE_ROOT; }; + 491F8F0D89972E9D8903CFA7 /* high_chimes.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/high_chimes.caf; sourceTree = SOURCE_ROOT; }; + 4A645E0E2DDA534A635C9201 /* clear_chimes.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/clear_chimes.caf; sourceTree = SOURCE_ROOT; }; 4D3B40021D4A9DFE00BC6334 /* G4ShareSpy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = G4ShareSpy.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4F08DE8E1E7BB871006741EA /* CollectionType+Loop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CollectionType+Loop.swift"; sourceTree = ""; }; 4F11D3BF20DCBEEC006E072C /* GlucoseBackfillRequestUserInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseBackfillRequestUserInfo.swift; sourceTree = ""; }; @@ -1026,10 +1053,12 @@ 4FC8C8001DEB93E400A1452E /* NSUserDefaults+StatusExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSUserDefaults+StatusExtension.swift"; sourceTree = ""; }; 4FDDD23620DC51DF00D04B16 /* LoopDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopDataManager.swift; sourceTree = ""; }; 4FF4D0FF1E18374700846527 /* WatchContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchContext.swift; sourceTree = ""; }; - 5181B60CBF3CB2DA537F0475 /* Loop/Resources/Sounds/simple.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/simple.caf; sourceTree = SOURCE_ROOT; }; - 62F7CB5D5DC337E705032AC7 /* Loop/Resources/Sounds/dings.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/dings.caf; sourceTree = SOURCE_ROOT; }; - 6D5FCC7207E50ACC682E9E6E /* Loop/Resources/Sounds/honk.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/honk.caf; sourceTree = SOURCE_ROOT; }; - 74145BE3C46270883D295A1F /* Loop/Resources/Sounds/minimal.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/minimal.caf; sourceTree = SOURCE_ROOT; }; + 5181B60CBF3CB2DA537F0475 /* simple.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/simple.caf; sourceTree = SOURCE_ROOT; }; + 62F7CB5D5DC337E705032AC7 /* dings.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/dings.caf; sourceTree = SOURCE_ROOT; }; + 6515A16900D12040FF31E279 /* ComplicationPublisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ComplicationPublisher.swift; sourceTree = ""; }; + 6D5FCC7207E50ACC682E9E6E /* honk.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/honk.caf; sourceTree = SOURCE_ROOT; }; + 74145BE3C46270883D295A1F /* minimal.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/minimal.caf; sourceTree = SOURCE_ROOT; }; + 7575B76A6837658308376B90 /* LoopComplications.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = LoopComplications.entitlements; sourceTree = ""; }; 7D23667C21250C7E0028B67D /* LocalizedString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LocalizedString.swift; path = LoopUI/Common/LocalizedString.swift; sourceTree = SOURCE_ROOT; }; 7D9BEF132335EC4B005DCFD6 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Intents.strings; sourceTree = ""; }; 7D9BEF292335EC58005DCFD6 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Intents.strings"; sourceTree = ""; }; @@ -1039,7 +1068,7 @@ 7D9BEF812335EC8B005DCFD6 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Intents.strings; sourceTree = ""; }; 7D9BF13A23370E8B005DCFD6 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Intents.strings; sourceTree = ""; }; 7E69CFFB2A16A77E00203CBD /* ResetLoopManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetLoopManager.swift; sourceTree = ""; }; - 83081FFB0497A4596F597830 /* Loop/Resources/Sounds/synth.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/synth.caf; sourceTree = SOURCE_ROOT; }; + 83081FFB0497A4596F597830 /* synth.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/synth.caf; sourceTree = SOURCE_ROOT; }; 840A2F0D2C0F978E003D5E90 /* LoopKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 841306BA2F7F0D9C00AF0320 /* ReferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferencesView.swift; sourceTree = ""; }; 84213C742D932F0F00642E78 /* InsulinDeliveryLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinDeliveryLog.swift; sourceTree = ""; }; @@ -1083,7 +1112,7 @@ 84EC162D2C9115CA00D220C5 /* DIYLoopUnitTestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = DIYLoopUnitTestPlan.xctestplan; sourceTree = ""; }; 84F20DFC2D0B9C3A0089DF02 /* EditPresetDurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditPresetDurationView.swift; sourceTree = ""; }; 84FA9D322CF7FD0D004162B4 /* PresetsHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresetsHistoryView.swift; sourceTree = ""; }; - 85203550670F2E9BE92F27D8 /* Loop/Resources/Sounds/crying.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/crying.caf; sourceTree = SOURCE_ROOT; }; + 85203550670F2E9BE92F27D8 /* crying.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/crying.caf; sourceTree = SOURCE_ROOT; }; 891B508424342BE1005DA578 /* CarbAndBolusFlowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbAndBolusFlowViewModel.swift; sourceTree = ""; }; 892A5D29222EF60A008961AB /* MockKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; name = MockKit.framework; path = Carthage/Build/iOS/MockKit.framework; sourceTree = SOURCE_ROOT; }; 892A5D2B222EF60A008961AB /* MockKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; name = MockKitUI.framework; path = Carthage/Build/iOS/MockKitUI.framework; sourceTree = SOURCE_ROOT; }; @@ -1141,8 +1170,10 @@ 89F9119324358E4500ECCAF3 /* CarbAbsorptionTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbAbsorptionTime.swift; sourceTree = ""; }; 89F9119524358E6900ECCAF3 /* BolusPickerValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusPickerValues.swift; sourceTree = ""; }; 89FE21AC24AC57E30033F501 /* Collection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = ""; }; - 8FB2394478FF679F436E8EF1 /* Loop/Resources/Sounds/bloom.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/bloom.caf; sourceTree = SOURCE_ROOT; }; - 9471E156FC10D6D0FF219C77 /* Loop/Resources/Sounds/spring.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/spring.caf; sourceTree = SOURCE_ROOT; }; + 8FB2394478FF679F436E8EF1 /* bloom.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/bloom.caf; sourceTree = SOURCE_ROOT; }; + 9471E156FC10D6D0FF219C77 /* spring.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/spring.caf; sourceTree = SOURCE_ROOT; }; + 9F8AF2728D70D69B00A4F38E /* ComplicationSnapshotTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ComplicationSnapshotTests.swift; sourceTree = ""; }; + A198971FAA313AF6CDD1ACDF /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS26.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; A1B2C3D40000000000000001 /* GlucoseAlertManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseAlertManagerTests.swift; sourceTree = ""; }; A900531A28D60862000BC15B /* Loop.shortcut */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop.shortcut; sourceTree = ""; }; A900531B28D608CA000BC15B /* Cancel Override.shortcut */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Cancel Override.shortcut"; sourceTree = ""; }; @@ -1194,7 +1225,8 @@ A9F703742489C9A000C98AD8 /* GlucoseStore+SimulatedCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStore+SimulatedCoreData.swift"; sourceTree = ""; }; A9F703762489D8AA00C98AD8 /* PersistentDeviceLog+SimulatedCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistentDeviceLog+SimulatedCoreData.swift"; sourceTree = ""; }; A9FB75F0252BE320004C7D3F /* BolusDosingDecision.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusDosingDecision.swift; sourceTree = ""; }; - AF16A5EDEB9DF756890E0C09 /* Loop/Resources/Sounds/bloop.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/bloop.caf; sourceTree = SOURCE_ROOT; }; + AC0853072224AE8CC4C388E7 /* Loop Complications.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Loop Complications.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + AF16A5EDEB9DF756890E0C09 /* bloop.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/bloop.caf; sourceTree = SOURCE_ROOT; }; B4001CED28CBBC82002FB414 /* AlertManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertManagementView.swift; sourceTree = ""; }; B40D07C6251A89D500C1C6D7 /* GlucoseDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseDisplay.swift; sourceTree = ""; }; B429CAB22E97C7F300FA988E /* LoopStatusModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStatusModalView.swift; sourceTree = ""; }; @@ -1252,7 +1284,7 @@ B66D1F422E6A5D6600471149 /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/Main.xcstrings; sourceTree = ""; }; B6F22EF72E95A03800CCA05F /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Intents.strings; sourceTree = ""; }; B6F22EF92E95A03C00CCA05F /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Intents.strings; sourceTree = ""; }; - BEC0BA802BE613F144AC3A70 /* Loop/Resources/Sounds/mood_synth.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/mood_synth.caf; sourceTree = SOURCE_ROOT; }; + BEC0BA802BE613F144AC3A70 /* mood_synth.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/mood_synth.caf; sourceTree = SOURCE_ROOT; }; C1004DEF2981F5B700B8CF94 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/InfoPlist.strings; sourceTree = ""; }; C1004DFD2981F67A00B8CF94 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/InfoPlist.strings; sourceTree = ""; }; C1004E052981F6A100B8CF94 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -1395,11 +1427,12 @@ C1FAD5182E7E0C3100F7FAD9 /* ChartPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartPageView.swift; sourceTree = ""; }; C1FB428B217806A300FAB378 /* StateColorPalette.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateColorPalette.swift; sourceTree = ""; }; C1FB428E217921D600FAB378 /* PumpManagerUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpManagerUI.swift; sourceTree = ""; }; + C535A805BFAB88306874F9E2 /* WatchApp.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = WatchApp.entitlements; sourceTree = ""; }; C9A47B0175B13A9543390BC5 /* CriticalAlertAudioPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CriticalAlertAudioPlayer.swift; sourceTree = ""; }; C9A47B0275B13A9543390BC6 /* GlucoseAlertManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseAlertManager.swift; sourceTree = ""; }; C9A47B0375B13A9543390BC7 /* GlucoseAlertSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseAlertSettingsView.swift; sourceTree = ""; }; - C9A47B0475B13A9543390BC8 /* Loop/Resources/Sounds/critical.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/critical.caf; sourceTree = SOURCE_ROOT; }; - C9EA3F48338B49BD9C6BD0F9 /* Loop/Resources/Sounds/urgent_low.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/urgent_low.caf; sourceTree = SOURCE_ROOT; }; + C9A47B0475B13A9543390BC8 /* critical.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/critical.caf; sourceTree = SOURCE_ROOT; }; + C9EA3F48338B49BD9C6BD0F9 /* urgent_low.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Loop/Resources/Sounds/urgent_low.caf; sourceTree = SOURCE_ROOT; }; C9SCENED0000000000000002 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; DD3DBD282A33AFE9000F8B5B /* IntegralRetrospectiveCorrectionSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegralRetrospectiveCorrectionSelectionView.swift; sourceTree = ""; }; DDC389F52A2B61750066E2E8 /* ApplicationFactorStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationFactorStrategy.swift; sourceTree = ""; }; @@ -1451,12 +1484,14 @@ E9C58A7B24DB529A00487A17 /* insulin_effect.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = insulin_effect.json; sourceTree = ""; }; F5D9C01727DABBE0002E48F6 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Intents.strings; sourceTree = ""; }; F5E0BDD327E1D71C0033557E /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Intents.strings; sourceTree = ""; }; + F9C8C0BEB6A6B9A65E0D56AF /* LoopComplications.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LoopComplications.swift; sourceTree = ""; }; FADE0001000000000000DE01 /* DevelopmentBranchAlerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevelopmentBranchAlerter.swift; sourceTree = ""; }; + FCF57BFE5DDB939285839898 /* ComplicationSnapshot.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ComplicationSnapshot.swift; sourceTree = ""; }; FEEDF0000000000000000000 /* GlucoseStatistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseStatistics.swift; sourceTree = ""; }; FEEDF0000000000000000001 /* StatisticsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsViewModel.swift; sourceTree = ""; }; - FEEDF0000000000000000002 /* Statistics/StatisticsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Statistics/StatisticsView.swift; sourceTree = ""; }; - FEEDF0000000000000000003 /* Statistics/AGPChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Statistics/AGPChartView.swift; sourceTree = ""; }; - FEEDF0000000000000000004 /* Statistics/TimeInRangeBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Statistics/TimeInRangeBar.swift; sourceTree = ""; }; + FEEDF0000000000000000002 /* StatisticsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Statistics/StatisticsView.swift; sourceTree = ""; }; + FEEDF0000000000000000003 /* AGPChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Statistics/AGPChartView.swift; sourceTree = ""; }; + FEEDF0000000000000000004 /* TimeInRangeBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Statistics/TimeInRangeBar.swift; sourceTree = ""; }; FEEDF0000000000000000005 /* GlucoseStatisticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseStatisticsTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1538,6 +1573,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + B3EE1E6F6E1FF26E47260BCD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A44984BCF1848A652E02CE8C /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; E9B07F79253BBA6500BAD8F8 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1749,6 +1792,7 @@ 437D9BA11D7B5203007245E8 /* Loop.xcconfig */, A951C5FF23E8AB51003E26DC /* Version.xcconfig */, 84957A16300D5CDF0040E2EF /* Recovered References */, + BBB8D9B1F2665786DA6D9ED8 /* Loop Complications */, ); sourceTree = ""; }; @@ -1763,6 +1807,7 @@ 43D9002A21EB209400AF44BF /* LoopCore.framework */, E9B07F7C253BBA6500BAD8F8 /* Loop Intent Extension.appex */, 14B1735C28AED9EC006CCD7C /* Loop Widget Extension.appex */, + AC0853072224AE8CC4C388E7 /* Loop Complications.appex */, ); name = Products; sourceTree = ""; @@ -1804,6 +1849,7 @@ 43F5C2D61B92A4DC003EB13D /* Info.plist */, A966152823EA5A37005D8B29 /* DefaultAssets.xcassets */, A966152923EA5A37005D8B29 /* DerivedAssets.xcassets */, + C535A805BFAB88306874F9E2 /* WatchApp.entitlements */, ); path = WatchApp; sourceTree = ""; @@ -2047,9 +2093,9 @@ B43B5C532EAFBF110096A6AE /* RecentGlucoseTableViewCell.xib */, C151634D2FA2C9C800FEECE8 /* RequiredVersionUpdateView.swift */, 1DE09BA824A3E23F009EE9F9 /* SettingsView.swift */, - FEEDF0000000000000000004 /* Statistics/TimeInRangeBar.swift */, - FEEDF0000000000000000003 /* Statistics/AGPChartView.swift */, - FEEDF0000000000000000002 /* Statistics/StatisticsView.swift */, + FEEDF0000000000000000004 /* TimeInRangeBar.swift */, + FEEDF0000000000000000003 /* AGPChartView.swift */, + FEEDF0000000000000000002 /* StatisticsView.swift */, E4854073AAE5C2CB97167F63 /* HealthAccessView.swift */, DDC389FB2A2BC6670066E2E8 /* SettingsView+algorithmExperimentsSection.swift */, C1DE5D22251BFC4D00439E49 /* SimpleBolusView.swift */, @@ -2124,6 +2170,7 @@ A9E6DFED246A0460005B1A1C /* Models */, B4BC56362518DE8800373647 /* ViewModels */, 84EC162D2C9115CA00D220C5 /* DIYLoopUnitTestPlan.xctestplan */, + 9F8AF2728D70D69B00A4F38E /* ComplicationSnapshotTests.swift */, ); path = LoopTests; sourceTree = ""; @@ -2210,6 +2257,7 @@ children = ( 4FDDD23620DC51DF00D04B16 /* LoopDataManager.swift */, 898ECA62218ABD21001E9D35 /* ComplicationChartManager.swift */, + 6515A16900D12040FF31E279 /* ComplicationPublisher.swift */, ); path = Managers; sourceTree = ""; @@ -2308,24 +2356,24 @@ 84957A16300D5CDF0040E2EF /* Recovered References */ = { isa = PBXGroup; children = ( - C9A47B0475B13A9543390BC8 /* Loop/Resources/Sounds/critical.caf */, - 0FC34B523FE5A50C920506D5 /* Loop/Resources/Sounds/alarm.caf */, - 8FB2394478FF679F436E8EF1 /* Loop/Resources/Sounds/bloom.caf */, - AF16A5EDEB9DF756890E0C09 /* Loop/Resources/Sounds/bloop.caf */, - 270F994CD56D566028B3CB25 /* Loop/Resources/Sounds/bright_alarm.caf */, - 3F7A1D72697082DC3A14587E /* Loop/Resources/Sounds/chime.caf */, - 4A645E0E2DDA534A635C9201 /* Loop/Resources/Sounds/clear_chimes.caf */, - 85203550670F2E9BE92F27D8 /* Loop/Resources/Sounds/crying.caf */, - 62F7CB5D5DC337E705032AC7 /* Loop/Resources/Sounds/dings.caf */, - 491F8F0D89972E9D8903CFA7 /* Loop/Resources/Sounds/high_chimes.caf */, - 6D5FCC7207E50ACC682E9E6E /* Loop/Resources/Sounds/honk.caf */, - 74145BE3C46270883D295A1F /* Loop/Resources/Sounds/minimal.caf */, - BEC0BA802BE613F144AC3A70 /* Loop/Resources/Sounds/mood_synth.caf */, - 5181B60CBF3CB2DA537F0475 /* Loop/Resources/Sounds/simple.caf */, - 9471E156FC10D6D0FF219C77 /* Loop/Resources/Sounds/spring.caf */, - 83081FFB0497A4596F597830 /* Loop/Resources/Sounds/synth.caf */, - 21F13EB666EBFDD03F99CB7E /* Loop/Resources/Sounds/trill.caf */, - C9EA3F48338B49BD9C6BD0F9 /* Loop/Resources/Sounds/urgent_low.caf */, + C9A47B0475B13A9543390BC8 /* critical.caf */, + 0FC34B523FE5A50C920506D5 /* alarm.caf */, + 8FB2394478FF679F436E8EF1 /* bloom.caf */, + AF16A5EDEB9DF756890E0C09 /* bloop.caf */, + 270F994CD56D566028B3CB25 /* bright_alarm.caf */, + 3F7A1D72697082DC3A14587E /* chime.caf */, + 4A645E0E2DDA534A635C9201 /* clear_chimes.caf */, + 85203550670F2E9BE92F27D8 /* crying.caf */, + 62F7CB5D5DC337E705032AC7 /* dings.caf */, + 491F8F0D89972E9D8903CFA7 /* high_chimes.caf */, + 6D5FCC7207E50ACC682E9E6E /* honk.caf */, + 74145BE3C46270883D295A1F /* minimal.caf */, + BEC0BA802BE613F144AC3A70 /* mood_synth.caf */, + 5181B60CBF3CB2DA537F0475 /* simple.caf */, + 9471E156FC10D6D0FF219C77 /* spring.caf */, + 83081FFB0497A4596F597830 /* synth.caf */, + 21F13EB666EBFDD03F99CB7E /* trill.caf */, + C9EA3F48338B49BD9C6BD0F9 /* urgent_low.caf */, ); name = "Recovered References"; sourceTree = ""; @@ -2593,6 +2641,7 @@ E9B07F86253BBA6500BAD8F8 /* IntentsUI.framework */, 14B1735D28AED9EC006CCD7C /* WidgetKit.framework */, 14B1735F28AED9EC006CCD7C /* SwiftUI.framework */, + CBB18F4A574C9E4041F18D57 /* watchOS */, ); name = Frameworks; sourceTree = ""; @@ -2642,6 +2691,18 @@ path = ViewModels; sourceTree = ""; }; + BBB8D9B1F2665786DA6D9ED8 /* Loop Complications */ = { + isa = PBXGroup; + children = ( + F9C8C0BEB6A6B9A65E0D56AF /* LoopComplications.swift */, + FCF57BFE5DDB939285839898 /* ComplicationSnapshot.swift */, + 2CA0E8DC01120F1A9593061C /* Info.plist */, + 7575B76A6837658308376B90 /* LoopComplications.entitlements */, + ); + name = "Loop Complications"; + path = "Loop Complications"; + sourceTree = ""; + }; C13072B82A76AF0A009A7C58 /* live_capture */ = { isa = PBXGroup; children = ( @@ -2709,6 +2770,14 @@ path = Models; sourceTree = ""; }; + CBB18F4A574C9E4041F18D57 /* watchOS */ = { + isa = PBXGroup; + children = ( + A198971FAA313AF6CDD1ACDF /* Foundation.framework */, + ); + name = watchOS; + sourceTree = ""; + }; E93E86AC24DDE02C00FF40C8 /* Mock Stores */ = { isa = PBXGroup; children = ( @@ -2834,6 +2903,23 @@ productReference = 14B1735C28AED9EC006CCD7C /* Loop Widget Extension.appex */; productType = "com.apple.product-type.app-extension"; }; + 27B118E7F17401410F70FACA /* Loop Complications */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1004EFCBCC83CD0113CAAA67 /* Build configuration list for PBXNativeTarget "Loop Complications" */; + buildPhases = ( + 75E901D28BEE7265730D11E5 /* Sources */, + B3EE1E6F6E1FF26E47260BCD /* Frameworks */, + F70BC2A811EB49CFAF2CA7EE /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Loop Complications"; + productName = "Loop Complications"; + productReference = AC0853072224AE8CC4C388E7 /* Loop Complications.appex */; + productType = "com.apple.product-type.app-extension"; + }; 43776F8B1B8022E90074EA36 /* Loop */ = { isa = PBXNativeTarget; buildConfigurationList = 43776FB61B8022E90074EA36 /* Build configuration list for PBXNativeTarget "Loop" */; @@ -2878,11 +2964,13 @@ C1E9CB59294E67060022387B /* Build Derived Assets */, 43A9437A1B926B7B0051FA24 /* Sources */, 43C667D71C5577280050C674 /* Embed Frameworks */, + ECA50E3AF74E45D0C77CA79C /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( C16E94F92E7DBBA600AA4E6E /* PBXTargetDependency */, + 2F20F936EF7D4968D5351400 /* PBXTargetDependency */, ); name = WatchApp; productName = WatchApp; @@ -3104,6 +3192,7 @@ 43D9001A21EB209400AF44BF /* LoopCore-watchOS */, 4F75288A1DFE1DC600C322D6 /* LoopUI */, 43E2D90A1D20C581004DA55F /* LoopTests */, + 27B118E7F17401410F70FACA /* Loop Complications */, ); }; /* End PBXProject section */ @@ -3129,24 +3218,24 @@ C13255D6223E7BE2008AF50C /* BolusProgressTableViewCell.xib in Resources */, C1EE9E812A38D0FB0064784A /* BuildDetails.plist in Resources */, 43FCBBC21E51710B00343C1B /* LaunchScreen.storyboard in Resources */, - C9A47B0480AAB1FC572D3F01 /* Loop/Resources/Sounds/critical.caf in Resources */, - DE965737FE2DAE9FE692769E /* Loop/Resources/Sounds/alarm.caf in Resources */, - A413B305CB4C4009DA09BE57 /* Loop/Resources/Sounds/bloom.caf in Resources */, - 230BE095719B8D8969B32A9C /* Loop/Resources/Sounds/bloop.caf in Resources */, - 9B8FCBDD6E3A9E603C4AABAD /* Loop/Resources/Sounds/bright_alarm.caf in Resources */, - 9960C6964DD51D81093FAD5D /* Loop/Resources/Sounds/chime.caf in Resources */, - 824A01CBE97A4F7A9D32F7D6 /* Loop/Resources/Sounds/clear_chimes.caf in Resources */, - 01414522FC033F2B7A9B223A /* Loop/Resources/Sounds/crying.caf in Resources */, - 2C694BFD5806E573AFAB5E2E /* Loop/Resources/Sounds/dings.caf in Resources */, - A4574F416AD62A1FCDE36D7D /* Loop/Resources/Sounds/high_chimes.caf in Resources */, - FA37F6EC10FF02D4F985D724 /* Loop/Resources/Sounds/honk.caf in Resources */, - 0A02E77AD7536C322933D49B /* Loop/Resources/Sounds/minimal.caf in Resources */, - 33644743F20B3706E3107FAC /* Loop/Resources/Sounds/mood_synth.caf in Resources */, - A2779286E117BD3627DA0618 /* Loop/Resources/Sounds/simple.caf in Resources */, - D6E28676C2C7751C77C7F2FB /* Loop/Resources/Sounds/spring.caf in Resources */, - CB1AF8155B1761A30ACA5591 /* Loop/Resources/Sounds/synth.caf in Resources */, - 16E5A3BC4D931CE6AED18518 /* Loop/Resources/Sounds/trill.caf in Resources */, - 124B300527D84BE2A1233A91 /* Loop/Resources/Sounds/urgent_low.caf in Resources */, + C9A47B0480AAB1FC572D3F01 /* critical.caf in Resources */, + DE965737FE2DAE9FE692769E /* alarm.caf in Resources */, + A413B305CB4C4009DA09BE57 /* bloom.caf in Resources */, + 230BE095719B8D8969B32A9C /* bloop.caf in Resources */, + 9B8FCBDD6E3A9E603C4AABAD /* bright_alarm.caf in Resources */, + 9960C6964DD51D81093FAD5D /* chime.caf in Resources */, + 824A01CBE97A4F7A9D32F7D6 /* clear_chimes.caf in Resources */, + 01414522FC033F2B7A9B223A /* crying.caf in Resources */, + 2C694BFD5806E573AFAB5E2E /* dings.caf in Resources */, + A4574F416AD62A1FCDE36D7D /* high_chimes.caf in Resources */, + FA37F6EC10FF02D4F985D724 /* honk.caf in Resources */, + 0A02E77AD7536C322933D49B /* minimal.caf in Resources */, + 33644743F20B3706E3107FAC /* mood_synth.caf in Resources */, + A2779286E117BD3627DA0618 /* simple.caf in Resources */, + D6E28676C2C7751C77C7F2FB /* spring.caf in Resources */, + CB1AF8155B1761A30ACA5591 /* synth.caf in Resources */, + 16E5A3BC4D931CE6AED18518 /* trill.caf in Resources */, + 124B300527D84BE2A1233A91 /* urgent_low.caf in Resources */, B405E35A24D2B1A400DD058D /* HUDAssets.xcassets in Resources */, 1F0000012FBA000100000001 /* AppIcon.icon in Resources */, A966152623EA5A26005D8B29 /* DefaultAssets.xcassets in Resources */, @@ -3232,6 +3321,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F70BC2A811EB49CFAF2CA7EE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -3586,9 +3682,9 @@ 1D12D3B92548EFDD00B53E8B /* main.swift in Sources */, A9DCF32A25B0FABF00C89088 /* LoopUIColorPalette+Default.swift in Sources */, 1DDE273E24AEA4B000796622 /* SettingsView.swift in Sources */, - FEEDB0000000000000000004 /* Statistics/TimeInRangeBar.swift in Sources */, - FEEDB0000000000000000003 /* Statistics/AGPChartView.swift in Sources */, - FEEDB0000000000000000002 /* Statistics/StatisticsView.swift in Sources */, + FEEDB0000000000000000004 /* TimeInRangeBar.swift in Sources */, + FEEDB0000000000000000003 /* AGPChartView.swift in Sources */, + FEEDB0000000000000000002 /* StatisticsView.swift in Sources */, BAF043BF0BC70EC99AE79F71 /* HealthAccessView.swift in Sources */, A9B607B0247F000F00792BE4 /* UserNotifications+Loop.swift in Sources */, 43F89CA322BDFBBD006BB54E /* UIActivityIndicatorView.swift in Sources */, @@ -3751,6 +3847,8 @@ 894F6DD3243BCBDB00CCE676 /* Environment+SizeClass.swift in Sources */, C17D52042E7F0578001D2AD2 /* LabelValueRow.swift in Sources */, 895788B2242E69A2002CB114 /* CircularAccessoryButtonStyle.swift in Sources */, + 40ACF455B8FB3B38F2D8C0CA /* ComplicationSnapshot.swift in Sources */, + EB125CD137FD31CD1EFAD0E0 /* ComplicationPublisher.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3879,6 +3977,8 @@ E93E86B224DDE21D00FF40C8 /* MockCarbStore.swift in Sources */, C1DA43592B1A784900CBD33F /* MockDeliveryDelegate.swift in Sources */, C18859A02AF1612B0010F21F /* PersistenceController.swift in Sources */, + 6C089A6A6A8AB8F5D835C9F1 /* ComplicationSnapshot.swift in Sources */, + 2615C162995594E8CF144CB2 /* ComplicationSnapshotTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3926,6 +4026,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 75E901D28BEE7265730D11E5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3E1A1B91ED0E8C8F5663988E /* LoopComplications.swift in Sources */, + 0956C3313D5165B71E491FFA /* ComplicationSnapshot.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; E9B07F78253BBA6500BAD8F8 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -3954,6 +4063,12 @@ target = 14B1735B28AED9EC006CCD7C /* Loop Widget Extension */; targetProxy = 14B1736728AED9EE006CCD7C /* PBXContainerItemProxy */; }; + 2F20F936EF7D4968D5351400 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "Loop Complications"; + target = 27B118E7F17401410F70FACA /* Loop Complications */; + targetProxy = 1B21307ABB67B1D1DACE2D25 /* PBXContainerItemProxy */; + }; 43A943931B926B7B0051FA24 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 43A943711B926B7B0051FA24 /* WatchApp */; @@ -4087,6 +4202,31 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 06B1C4F09B9FA81ADC520CF2 /* Testflight */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = "Loop Complications/LoopComplications.entitlements"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = "$(LOOP_DEVELOPMENT_TEAM)"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Loop Complications/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER).LoopWatch.Complications"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + VALIDATE_PRODUCT = YES; + WATCHOS_DEPLOYMENT_TARGET = 10.6; + }; + name = Testflight; + }; 14B1736A28AED9EE006CCD7C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -4182,6 +4322,30 @@ }; name = Release; }; + 14F587DF4DB6E0D5B2263670 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = "Loop Complications/LoopComplications.entitlements"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = "$(LOOP_DEVELOPMENT_TEAM)"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Loop Complications/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER).LoopWatch.Complications"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 10.6; + }; + name = Debug; + }; 43776FB41B8022E90074EA36 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 437D9BA11D7B5203007245E8 /* Loop.xcconfig */; @@ -4473,6 +4637,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = "$(APPICON_NAME)"; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; + CODE_SIGN_ENTITLEMENTS = WatchApp/WatchApp.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = "$(LOOP_DEVELOPMENT_TEAM)"; @@ -4497,6 +4662,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = "$(APPICON_NAME)"; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; + CODE_SIGN_ENTITLEMENTS = WatchApp/WatchApp.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = "$(LOOP_DEVELOPMENT_TEAM)"; @@ -4718,6 +4884,31 @@ }; name = Release; }; + 7E2439E4800477E4F2DB20F7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = "Loop Complications/LoopComplications.entitlements"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = "$(LOOP_DEVELOPMENT_TEAM)"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Loop Complications/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER).LoopWatch.Complications"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + VALIDATE_PRODUCT = YES; + WATCHOS_DEPLOYMENT_TARGET = 10.6; + }; + name = Release; + }; B4E7CF912AD00A39009B4DF2 /* Testflight */ = { isa = XCBuildConfiguration; baseConfigurationReference = 437D9BA11D7B5203007245E8 /* Loop.xcconfig */; @@ -4865,6 +5056,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = "$(APPICON_NAME)"; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; + CODE_SIGN_ENTITLEMENTS = WatchApp/WatchApp.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = "$(LOOP_DEVELOPMENT_TEAM)"; @@ -5118,6 +5310,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 1004EFCBCC83CD0113CAAA67 /* Build configuration list for PBXNativeTarget "Loop Complications" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7E2439E4800477E4F2DB20F7 /* Release */, + 14F587DF4DB6E0D5B2263670 /* Debug */, + 06B1C4F09B9FA81ADC520CF2 /* Testflight */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 14B1736C28AED9EE006CCD7C /* Build configuration list for PBXNativeTarget "Loop Widget Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/LoopTests/ComplicationSnapshotTests.swift b/LoopTests/ComplicationSnapshotTests.swift new file mode 100644 index 0000000000..86e4e60a78 --- /dev/null +++ b/LoopTests/ComplicationSnapshotTests.swift @@ -0,0 +1,81 @@ +// +// ComplicationSnapshotTests.swift +// LoopTests +// +// The watch complications' snapshot (compiled into this target from Loop Complications): what shows at +// each moment, the timeline's change moments, and when the watch app asks WidgetKit to reload. +// + +import XCTest + +final class ComplicationSnapshotTests: XCTestCase { + + private let now = Date() + + private func snapshot(glucoseAge: TimeInterval = 60, loopAge: TimeInterval = 60) -> ComplicationSnapshot { + let reading = now.addingTimeInterval(-glucoseAge) + let loop = now.addingTimeInterval(-loopAge) + return ComplicationSnapshot(glucoseText: "106", trendSymbol: "↗", staleGlucoseText: "---", + glucoseStaleAt: reading.addingTimeInterval(15 * 60), eventualText: "108", + inlineText: "106↗ 108 8:47 PM", inlineStaleText: "--- 108 8:47 PM", + loopDate: loop, freshUntil: loop.addingTimeInterval(6 * 60), + agingUntil: loop.addingTimeInterval(16 * 60), graphRenderedAt: nil) + } + + func testAFreshReadingShowsWithItsTrend() { + let s = snapshot() + XCTAssertEqual(s.glucose(at: now), "106") + XCTAssertEqual(s.trend(at: now), "↗") + XCTAssertEqual(s.inline(at: now), "106↗ 108 8:47 PM") + } + + /// As the ClockKit templates: a stale reading shows the stale marker and drops the trend. + func testAStaleReadingShowsTheMarkerAndNoTrend() { + let s = snapshot(glucoseAge: 16 * 60) + XCTAssertEqual(s.glucose(at: now), "---") + XCTAssertEqual(s.trend(at: now), "") + XCTAssertEqual(s.inline(at: now), "--- 108 8:47 PM") + } + + func testLoopFreshnessFollowsTheLoopsAge() { + XCTAssertEqual(snapshot(loopAge: 60).freshness(at: now), .fresh) + XCTAssertEqual(snapshot(loopAge: 10 * 60).freshness(at: now), .aging) + XCTAssertEqual(snapshot(loopAge: 20 * 60).freshness(at: now), .stale) + var noLoop = snapshot() + noLoop.loopDate = nil + XCTAssertEqual(noLoop.freshness(at: now), .stale) + } + + /// The timeline carries the moments things change on their own, so none of them needs a reload. + func testTheTimelineMarksEachChangeMoment() { + let s = snapshot(glucoseAge: 60, loopAge: 60) + let moments = s.changeMoments(after: now) + XCTAssertEqual(moments.count, 3, "reading stale, loop aging, loop stale") + XCTAssertEqual(s.freshness(at: moments[0]), .aging, "first: the loop turns at 6 min") + XCTAssertTrue(s.glucoseIsStale(at: moments[1]), "then the reading at 15 min") + XCTAssertEqual(s.freshness(at: moments[2]), .stale, "then the loop at 16 min") + } + + func testTheSnapshotRoundTripsThroughDefaults() { + let defaults = UserDefaults(suiteName: "ComplicationSnapshotTests")! + defaults.removePersistentDomain(forName: "ComplicationSnapshotTests") + snapshot().save(to: defaults) + XCTAssertEqual(ComplicationSnapshot.load(from: defaults), snapshot()) + } + + /// Reloads are budgeted: at most one per window, and a change inside the window is not lost. + func testReloadsAreThrottledButNeverLost() { + var throttle = ComplicationReloadThrottle() + let a = snapshot(), b = snapshot(glucoseAge: 0) + + XCTAssertEqual(throttle.offer(a, now: now).reload, true, "the first value reloads at once") + let unchanged = throttle.offer(a, now: now.addingTimeInterval(60)) + XCTAssertFalse(unchanged.save, "an unchanged value is not rewritten") + XCTAssertFalse(unchanged.reload) + let early = throttle.offer(b, now: now.addingTimeInterval(120)) + XCTAssertTrue(early.save) + XCTAssertFalse(early.reload, "inside the window: saved, reload owed") + XCTAssertTrue(throttle.offer(b, now: now.addingTimeInterval(301)).reload, "the owed reload is paid once the window passes") + XCTAssertFalse(throttle.offer(b, now: now.addingTimeInterval(700)).reload, "nothing owed, nothing reloaded") + } +} diff --git a/WatchApp Extension/ComplicationController.swift b/WatchApp Extension/ComplicationController.swift index 3bacfd933e..beebecaf8b 100644 --- a/WatchApp Extension/ComplicationController.swift +++ b/WatchApp Extension/ComplicationController.swift @@ -239,3 +239,21 @@ final class ComplicationController: NSObject, CLKComplicationDataSource { } } } + +// MARK: - Migration to WidgetKit + +/// Faces that show a ClockKit complication from this app are moved to its WidgetKit replacement when +/// the app updates. Both ClockKit descriptors share the identifier "glucosegraph", so they are told +/// apart by family: the graph is the one offered in the large rectangular slot. +extension ComplicationController: CLKComplicationWidgetMigrator { + + var widgetMigrator: CLKComplicationWidgetMigrator { self } + + func widgetConfiguration(from complicationDescriptor: CLKComplicationDescriptor) async -> CLKComplicationWidgetMigrationConfiguration? { + guard let appBundle = Bundle.main.bundleIdentifier else { return nil } + let kind = complicationDescriptor.supportedFamilies.contains(.graphicRectangular) + ? LoopComplicationKind.glucoseGraph + : LoopComplicationKind.loopStatus + return CLKComplicationStaticWidgetMigrationConfiguration(kind: kind, extensionBundleIdentifier: appBundle + ".Complications") + } +} diff --git a/WatchApp Extension/ExtensionDelegate.swift b/WatchApp Extension/ExtensionDelegate.swift index 033126dca0..14aa7bac5c 100644 --- a/WatchApp Extension/ExtensionDelegate.swift +++ b/WatchApp Extension/ExtensionDelegate.swift @@ -207,6 +207,7 @@ class ExtensionDelegate: NSObject, WKApplicationDelegate { log.default("Reloading complication timeline") server.reloadTimeline(for: complication) } + ComplicationPublisher.publish(from: loopManager) } } diff --git a/WatchApp Extension/Managers/ComplicationPublisher.swift b/WatchApp Extension/Managers/ComplicationPublisher.swift new file mode 100644 index 0000000000..6659826ff3 --- /dev/null +++ b/WatchApp Extension/Managers/ComplicationPublisher.swift @@ -0,0 +1,93 @@ +// +// ComplicationPublisher.swift +// WatchApp Extension +// +// Feeds the Loop Complications widget extension. On every context update the watch app formats what +// the complications show (with the same formatters and localized strings the ClockKit templates +// used), renders the glucose graph with ComplicationChartManager, writes both to the shared app +// group, and asks WidgetKit to reload (ComplicationReloadThrottle decides when). +// + +import Foundation +import LoopAlgorithm +import LoopCore +import LoopKit +import WatchKit +import WidgetKit + +enum ComplicationPublisher { + + private static var throttle = ComplicationReloadThrottle() + private static let chartManager = ComplicationChartManager() + + /// Called on main at each context update. + @MainActor static func publish(from loopManager: LoopDataManager) { + guard let context = loopManager.activeContext else { return } + Task { @MainActor in + chartManager.data = await loopManager.generateChartData() + let graphRendered = renderGraph() + store(snapshot(from: context, graphRenderedAt: graphRendered ? Date() : nil), now: Date()) + } + } + + /// The strings each complication shows, formatted as the ClockKit templates formatted them. + static func snapshot(from context: WatchContext, graphRenderedAt: Date?) -> ComplicationSnapshot { + let unit = context.displayGlucoseUnit ?? .milligramsPerDeciliter + let formatter = NumberFormatter.glucoseFormatter(for: unit) + let staleGlucose = NSLocalizedString("---", comment: "No glucose value representation (3 dashes for mg/dL; no spaces as this will get truncated in the watch complication)") + + let glucoseText: String? = context.glucoseCondition?.localizedDescription + ?? context.glucose.flatMap { formatter.string(from: $0.doubleValue(for: unit)) } + let trendSymbol = context.glucoseTrend?.symbol + let eventualText = context.eventualGlucose.flatMap { formatter.string(from: $0.doubleValue(for: unit)) } + + var inlineText: String? + var inlineStaleText: String? + if let glucoseDate = context.glucoseDate { + let timeFormatter = DateFormatter() + timeFormatter.dateStyle = .none + timeFormatter.timeStyle = .short + // 106↗108 8:47 PM + let format = NSLocalizedString("UtilitarianLargeFlat", tableName: "ckcomplication", comment: "Utilitarian large flat format string (1: Glucose & Trend symbol) (2: Eventual Glucose) (3: Time)") + let time = timeFormatter.string(from: glucoseDate) + inlineText = String(format: format, "\(glucoseText ?? staleGlucose)\(trendSymbol ?? " ")", eventualText ?? "", time) + inlineStaleText = String(format: format, staleGlucose, eventualText ?? "", time) + } + + let loopDate = context.loopLastRunDate + return ComplicationSnapshot( + glucoseText: glucoseText, + trendSymbol: trendSymbol, + staleGlucoseText: staleGlucose, + glucoseStaleAt: context.glucoseDate.map { $0.addingTimeInterval(LoopAlgorithm.inputDataRecencyInterval) }, + eventualText: eventualText, + inlineText: inlineText, + inlineStaleText: inlineStaleText, + loopDate: loopDate, + freshUntil: loopDate.flatMap { date in LoopCompletionFreshness.fresh.maxAge.map { date.addingTimeInterval($0) } }, + agingUntil: loopDate.flatMap { date in LoopCompletionFreshness.aging.maxAge.map { date.addingTimeInterval($0) } }, + graphRenderedAt: graphRenderedAt + ) + } + + /// The graph at the size the ClockKit template used; the widget scales it to its slot. + @MainActor private static func renderGraph() -> Bool { + guard let url = ComplicationSnapshot.graphURL else { return false } + let size: CGSize = WKInterfaceDevice.current().screenBounds.width > 180 + ? CGSize(width: 171, height: 54) // 44 mm and larger + : CGSize(width: 150, height: 47) // 40 mm + guard let image = chartManager.renderChartImage(size: size, scale: WKInterfaceDevice.current().screenScale), + let png = image.pngData() else { + try? FileManager.default.removeItem(at: url) + return false + } + return (try? png.write(to: url, options: .atomic)) != nil + } + + @MainActor private static func store(_ snapshot: ComplicationSnapshot, now: Date) { + let decision = throttle.offer(snapshot, now: now) + if decision.save { snapshot.save() } + // Only these kinds: WidgetKit budgets reloads per widget, and other widgets keep their own. + if decision.reload { LoopComplicationKind.all.forEach { WidgetCenter.shared.reloadTimelines(ofKind: $0) } } + } +} diff --git a/WatchApp/Info.plist b/WatchApp/Info.plist index 3e7b8187eb..3e930f7ff9 100644 --- a/WatchApp/Info.plist +++ b/WatchApp/Info.plist @@ -2,12 +2,16 @@ + AppGroupIdentifier + $(APP_GROUP_IDENTIFIER) CFBundleDevelopmentRegion en CFBundleDisplayName $(MAIN_APP_DISPLAY_NAME) CFBundleExecutable $(EXECUTABLE_NAME) + CFBundleIconName + AppIcon CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion @@ -41,8 +45,6 @@ WKCompanionAppBundleIdentifier $(MAIN_APP_BUNDLE_IDENTIFIER) - CFBundleIconName - AppIcon WKSupportsLiveActivityLaunchAttributeTypes GlucoseActivityAttributes diff --git a/WatchApp/WatchApp.entitlements b/WatchApp/WatchApp.entitlements new file mode 100644 index 0000000000..d9849a816d --- /dev/null +++ b/WatchApp/WatchApp.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + $(APP_GROUP_IDENTIFIER) + + +