Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Live Activity #2191

Open
wants to merge 42 commits into
base: dev
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
f917bb3
start: Start live activity project
bastiaanv Jun 25, 2024
74d437f
style: Rework live activity
bastiaanv Jul 1, 2024
8a8cdd8
wip: Wip live activity
bastiaanv Jul 4, 2024
6301811
wip: Update live activity
bastiaanv Jul 5, 2024
e7fe0a8
wip: Continue live activity. TODO's expanded view di, localization
bastiaanv Jul 6, 2024
0c9793f
Merge branch 'LoopKit:dev' into feat/live-activity
bastiaanv Jul 6, 2024
1a85166
fix: Allow configurable chart limits
bastiaanv Jul 7, 2024
a836f7c
feat: Add stale state, minor chart fixes & fix delta
bastiaanv Jul 7, 2024
1d0b069
fix: Minor fixes
bastiaanv Jul 7, 2024
91795ab
fix: Minor hotfixes
bastiaanv Jul 9, 2024
ec50153
chore: Cleanup
bastiaanv Jul 9, 2024
85406a5
style: Minor styling fixes
bastiaanv Jul 9, 2024
aa439e4
fix: fix delta calculations & optimize booting sequence of LA
bastiaanv Jul 11, 2024
9d3a25a
Merge pull request #2 from LoopKit/dev
bastiaanv Jul 15, 2024
44e915d
fix: minor fix & cleanup
bastiaanv Jul 15, 2024
69a235a
style: Fix dynamic island expanded view
bastiaanv Jul 15, 2024
325d7a5
fix: Fix truncating text in expanded Dynamic Island
bastiaanv Jul 25, 2024
c68d743
feat: Add glucose targets & preset targets to Live Activity
bastiaanv Jul 25, 2024
3a2ab67
fix: Fix preMeal override & remove glucoseTarget while override is ac…
bastiaanv Jul 27, 2024
95f6f13
wip
bastiaanv Jul 31, 2024
c69db56
merge
bastiaanv Jul 31, 2024
985a1e4
fix: Restruct preset code
bastiaanv Jul 31, 2024
b72ab22
feat: Allow to disable colored chart
bastiaanv Aug 1, 2024
ae359a5
fix: Minor fixes for BG coloring
bastiaanv Aug 1, 2024
61d66c0
feat: Add carbohydrate history
bastiaanv Aug 4, 2024
0beeddc
feat: Add small mode & minor rework bottom row
bastiaanv Aug 8, 2024
7aaa57b
chore: Minor code cleanup
bastiaanv Aug 8, 2024
87602ff
revert: Revert unrelated commit for LA
bastiaanv Aug 9, 2024
8dc97d9
fix: Process feedback
bastiaanv Aug 14, 2024
81a5ef3
style: Add save button
bastiaanv Aug 15, 2024
92fb65f
style: Minor bg coloring fix
bastiaanv Aug 15, 2024
078b605
fix: Fix save with routing back and forth
bastiaanv Sep 12, 2024
9e88542
style: Removed auto scaling chart
bastiaanv Sep 12, 2024
fad3e9f
optimize
bastiaanv Sep 12, 2024
88ba91c
hotfix
bastiaanv Sep 12, 2024
03d5489
hotfix: yAxis scale
bastiaanv Sep 13, 2024
6d90d0c
Merge branch 'LoopKit:dev' into feat/live-activity
bastiaanv Oct 12, 2024
9b99558
feat: Allow LA to be hidden when nothing is happening or to be always on
bastiaanv Oct 12, 2024
e656e52
revert: Always on feature
bastiaanv Dec 19, 2024
cc8ee11
chore: Save button only enabled when state is dirty
bastiaanv Dec 19, 2024
4a3ed0e
fix: small configuration fixes
bastiaanv Mar 23, 2025
75c68ee
Merge branch 'LoopKit:dev' into feat/live-activity
bastiaanv Mar 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: Add glucose targets & preset targets to Live Activity
bastiaanv committed Jul 25, 2024
commit c68d7439b6e65e6c8e832b7ed0194b19deacfd9d
73 changes: 55 additions & 18 deletions Loop Widget Extension/Live Activity/ChartView.swift
Original file line number Diff line number Diff line change
@@ -13,8 +13,10 @@ import Charts
struct ChartView: View {
private let glucoseSampleData: [ChartValues]
private let predicatedData: [ChartValues]
private let glucoseRanges: [GlucoseRangeValue]
private let preset: Preset?

init(glucoseSamples: [GlucoseSampleAttributes], predicatedGlucose: [Double], predicatedStartDate: Date?, predicatedInterval: TimeInterval?, lowerLimit: Double, upperLimit: Double) {
init(glucoseSamples: [GlucoseSampleAttributes], predicatedGlucose: [Double], predicatedStartDate: Date?, predicatedInterval: TimeInterval?, lowerLimit: Double, upperLimit: Double, glucoseRanges: [GlucoseRangeValue], preset: Preset?) {
self.glucoseSampleData = ChartValues.convert(data: glucoseSamples, lowerLimit: lowerLimit, upperLimit: upperLimit)
self.predicatedData = ChartValues.convert(
data: predicatedGlucose,
@@ -23,30 +25,57 @@ struct ChartView: View {
lowerLimit: lowerLimit,
upperLimit: upperLimit
)
self.preset = preset
self.glucoseRanges = glucoseRanges
}

init(glucoseSamples: [GlucoseSampleAttributes], lowerLimit: Double, upperLimit: Double) {
init(glucoseSamples: [GlucoseSampleAttributes], lowerLimit: Double, upperLimit: Double, glucoseRanges: [GlucoseRangeValue], preset: Preset?) {
self.glucoseSampleData = ChartValues.convert(data: glucoseSamples, lowerLimit: lowerLimit, upperLimit: upperLimit)
self.predicatedData = []
self.preset = preset
self.glucoseRanges = glucoseRanges
}

var body: some View {
Chart {
ForEach(glucoseSampleData) { item in
PointMark (x: .value("Date", item.x),
y: .value("Glucose level", item.y)
)
.symbolSize(20)
.foregroundStyle(by: .value("Color", item.color))
}

ForEach(predicatedData) { item in
LineMark (x: .value("Date", item.x),
y: .value("Glucose level", item.y)
)
.lineStyle(StrokeStyle(lineWidth: 3, dash: [2, 3]))
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)){
Chart {
if let preset = self.preset, predicatedData.count > 0 {
RectangleMark(
xStart: .value("Start", Date.now),
xEnd: .value("End", preset.endDate),
yStart: .value("Preset override", preset.minValue),
yEnd: .value("Preset override", preset.maxValue)
)
.foregroundStyle(.primary)
.opacity(0.6)
}

ForEach(glucoseRanges) { item in
RectangleMark(
xStart: .value("Start", item.startDate),
xEnd: .value("End", item.endDate),
yStart: .value("Glucose range", item.minValue),
yEnd: .value("Glucose range", item.maxValue)
)
.foregroundStyle(.primary)
.opacity(0.3)
}

ForEach(glucoseSampleData) { item in
PointMark (x: .value("Date", item.x),
y: .value("Glucose level", item.y)
)
.symbolSize(20)
.foregroundStyle(by: .value("Color", item.color))
}

ForEach(predicatedData) { item in
LineMark (x: .value("Date", item.x),
y: .value("Glucose level", item.y)
)
.lineStyle(StrokeStyle(lineWidth: 3, dash: [2, 3]))
}
}
}
.chartForegroundStyleScale([
"Good": .green,
"High": .orange,
@@ -58,7 +87,7 @@ struct ChartView: View {
.chartLegend(.hidden)
.chartYScale(domain: .automatic(includesZero: false))
.chartYAxis {
AxisMarks(position: .trailing) { _ in
AxisMarks(position: .leading) { _ in
AxisValueLabel().foregroundStyle(Color.primary)
AxisGridLine(stroke: .init(lineWidth: 0.1, dash: [2, 3]))
.foregroundStyle(Color.primary)
@@ -72,6 +101,14 @@ struct ChartView: View {
.foregroundStyle(Color.primary)
}
}

if let preset = self.preset {
Text(preset.title)
.font(.footnote)
.padding(.trailing, 5)
.padding(.top, 5)
}
}
}
}

Original file line number Diff line number Diff line change
@@ -39,14 +39,18 @@ struct GlucoseLiveActivityConfiguration: Widget {
predicatedStartDate: context.state.predicatedStartDate,
predicatedInterval: context.state.predicatedInterval,
lowerLimit: context.state.isMmol ? context.attributes.lowerLimitChartMmol : context.attributes.lowerLimitChartMg,
upperLimit: context.state.isMmol ? context.attributes.upperLimitChartMmol : context.attributes.upperLimitChartMg
upperLimit: context.state.isMmol ? context.attributes.upperLimitChartMmol : context.attributes.upperLimitChartMg,
glucoseRanges: context.state.glucoseRanges,
preset: context.state.preset
)
.frame(height: 85)
} else {
ChartView(
glucoseSamples: context.state.glucoseSamples,
lowerLimit: context.state.isMmol ? context.attributes.lowerLimitChartMmol : context.attributes.lowerLimitChartMg,
upperLimit: context.state.isMmol ? context.attributes.upperLimitChartMmol : context.attributes.upperLimitChartMg
upperLimit: context.state.isMmol ? context.attributes.upperLimitChartMmol : context.attributes.upperLimitChartMg,
glucoseRanges: context.state.glucoseRanges,
preset: context.state.preset
)
.frame(height: 85)
}
@@ -135,14 +139,18 @@ struct GlucoseLiveActivityConfiguration: Widget {
predicatedStartDate: context.state.predicatedStartDate,
predicatedInterval: context.state.predicatedInterval,
lowerLimit: context.state.isMmol ? context.attributes.lowerLimitChartMmol : context.attributes.lowerLimitChartMg,
upperLimit: context.state.isMmol ? context.attributes.upperLimitChartMmol : context.attributes.upperLimitChartMg
upperLimit: context.state.isMmol ? context.attributes.upperLimitChartMmol : context.attributes.upperLimitChartMg,
glucoseRanges: context.state.glucoseRanges,
preset: context.state.preset
)
.frame(height: 75)
} else {
ChartView(
glucoseSamples: context.state.glucoseSamples,
lowerLimit: context.state.isMmol ? context.attributes.lowerLimitChartMmol : context.attributes.lowerLimitChartMg,
upperLimit: context.state.isMmol ? context.attributes.upperLimitChartMmol : context.attributes.upperLimitChartMg
upperLimit: context.state.isMmol ? context.attributes.upperLimitChartMmol : context.attributes.upperLimitChartMg,
glucoseRanges: context.state.glucoseRanges,
preset: context.state.preset
)
.frame(height: 75)
}
17 changes: 17 additions & 0 deletions Loop/Managers/Live Activity/GlucoseActivityAttributes.swift
Original file line number Diff line number Diff line change
@@ -16,6 +16,8 @@ public struct GlucoseActivityAttributes: ActivityAttributes {
// Meta data
public let date: Date
public let ended: Bool
public let preset: Preset?
public let glucoseRanges: [GlucoseRangeValue]

// Dynamic island data
public let currentGlucose: Double
@@ -44,6 +46,21 @@ public struct GlucoseActivityAttributes: ActivityAttributes {
public let lowerLimitChartMg: Double
}

public struct Preset: Codable, Hashable {
public let title: String
public let endDate: Date
public let minValue: Double
public let maxValue: Double
}

public struct GlucoseRangeValue: Identifiable, Codable, Hashable {
public let id: UUID
public let minValue: Double
public let maxValue: Double
public let startDate: Date
public let endDate: Date
}

public struct BottomRowItem: Codable, Hashable {
public enum BottomRowType: Codable, Hashable {
case generic
66 changes: 64 additions & 2 deletions Loop/Managers/Live Activity/GlucoseActivityManager.swift
Original file line number Diff line number Diff line change
@@ -25,6 +25,8 @@ class GlucoseActivityManager {

private let glucoseStore: GlucoseStoreProtocol
private let doseStore: DoseStoreProtocol
private let glucoseRangeSchedule: GlucoseRangeSchedule?
private var preset: TemporaryScheduleOverride?

private var startDate: Date = Date.now
private var settings: LiveActivitySettings = UserDefaults.standard.liveActivity ?? LiveActivitySettings()
@@ -49,13 +51,15 @@ class GlucoseActivityManager {
return dateFormatter
}()

init?(glucoseStore: GlucoseStoreProtocol, doseStore: DoseStoreProtocol) {
init?(glucoseStore: GlucoseStoreProtocol, doseStore: DoseStoreProtocol, glucoseRangeSchedule: GlucoseRangeSchedule?, preset: TemporaryScheduleOverride?) {
guard self.activityInfo.areActivitiesEnabled else {
return nil
}

self.glucoseStore = glucoseStore
self.doseStore = doseStore
self.glucoseRangeSchedule = glucoseRangeSchedule
self.preset = preset

// Ensure settings exist
if UserDefaults.standard.liveActivity == nil {
@@ -120,10 +124,42 @@ class GlucoseActivityManager {
if let samples = statusContext?.predictedGlucose?.values, settings.addPredictiveLine {
predicatedGlucose = samples
}

var endDateChart: Date? = nil
if predicatedGlucose.count == 0 {
endDateChart = glucoseSamples.last?.startDate
} else if let predictedGlucose = statusContext?.predictedGlucose {
endDateChart = predictedGlucose.startDate.addingTimeInterval(.hours(4))
}

var presetContext: Preset? = nil
if let preset = self.preset, let endDateChart = endDateChart {
presetContext = Preset(
title: preset.getTitle(),
endDate: preset.duration.isInfinite ? endDateChart : min(Date.now + preset.duration.timeInterval, endDateChart),
minValue: preset.settings.targetRange?.lowerBound.doubleValue(for: unit) ?? 0,
maxValue: preset.settings.targetRange?.upperBound.doubleValue(for: unit) ?? 0
)
}

var glucoseRanges: [GlucoseRangeValue] = []
if let glucoseRangeSchedule = self.glucoseRangeSchedule, let start = glucoseSamples.first?.startDate, let end = endDateChart {
for item in glucoseRangeSchedule.quantityBetween(start: start, end: end) {
glucoseRanges.append(GlucoseRangeValue(
id: UUID(),
minValue: item.value.lowerBound.doubleValue(for: unit),
maxValue: item.value.upperBound.doubleValue(for: unit),
startDate: max(item.startDate, start),
endDate: min(item.endDate, end)
))
}
}

let state = GlucoseActivityAttributes.ContentState(
date: currentGlucose.startDate,
ended: false,
preset: presetContext,
glucoseRanges: glucoseRanges,
currentGlucose: current,
trendType: statusContext?.glucoseDisplay?.trendType,
delta: delta,
@@ -226,7 +262,6 @@ class GlucoseActivityManager {
}
self.startDate = Date.now
} catch {}

}

private func needsRecreation() -> Bool {
@@ -339,6 +374,8 @@ class GlucoseActivityManager {
let dynamicState = GlucoseActivityAttributes.ContentState(
date: Date.now,
ended: true,
preset: nil,
glucoseRanges: [],
currentGlucose: 0,
trendType: nil,
delta: "",
@@ -365,4 +402,29 @@ class GlucoseActivityManager {
)
} catch {}
}

func presetActivated(preset: TemporaryScheduleOverride) {
self.preset = preset
self.update()
}

func presetDeactivated() {
self.preset = nil
self.update()
}
}

extension TemporaryScheduleOverride {
func getTitle() -> String {
switch (self.context) {
case .preset(let preset):
return "\(preset.symbol) \(preset.name)"
case .custom:
return NSLocalizedString("Custom preset", comment: "The title of the cell indicating a generic custom preset is enabled")
case .preMeal:
return NSLocalizedString(" Pre-meal Preset", comment: "Status row title for premeal override enabled (leading space is to separate from symbol)")
case .legacyWorkout:
return ""
}
}
}
15 changes: 13 additions & 2 deletions Loop/Managers/LoopDataManager.swift
Original file line number Diff line number Diff line change
@@ -127,7 +127,12 @@ final class LoopDataManager {

self.trustedTimeOffset = trustedTimeOffset

self.liveActivityManager = GlucoseActivityManager(glucoseStore: self.glucoseStore, doseStore: self.doseStore)
self.liveActivityManager = GlucoseActivityManager(
glucoseStore: self.glucoseStore,
doseStore: self.doseStore,
glucoseRangeSchedule: settings.glucoseTargetRangeSchedule,
preset: self.settings.scheduleOverride
)

overrideIntentObserver = UserDefaults.appGroup?.observe(\.intentExtensionOverrideToSet, options: [.new], changeHandler: {[weak self] (defaults, change) in
guard let name = change.newValue??.lowercased(), let appGroup = UserDefaults.appGroup else {
@@ -147,13 +152,17 @@ final class LoopDataManager {
observer.presetDeactivated(context: oldPreset.context)
}
}
self?.liveActivityManager?.presetDeactivated()
}
settings.scheduleOverride = preset.createOverride(enactTrigger: .remote("Siri"))
if let observers = self?.presetActivationObservers {
for observer in observers {
observer.presetActivated(context: .preset(preset), duration: preset.duration)
}
}
if let override = settings.scheduleOverride {
self?.liveActivityManager?.presetActivated(preset: override)
}
}
// Remove the override from UserDefaults so we don't set it multiple times
appGroup.intentExtensionOverrideToSet = nil
@@ -263,12 +272,14 @@ final class LoopDataManager {
for observer in self.presetActivationObservers {
observer.presetDeactivated(context: oldPreset.context)
}

self.liveActivityManager?.presetDeactivated()
}
if let newPreset = newValue.scheduleOverride {
for observer in self.presetActivationObservers {
observer.presetActivated(context: newPreset.context, duration: newPreset.duration)
}

self.liveActivityManager?.presetActivated(preset: newPreset)
}

// Invalidate cached effects affected by the override