Skip to content

Commit 7bf62f5

Browse files
authored
feature: Live Activity (LoopKit#2191)
1 parent 7c05abf commit 7bf62f5

22 files changed

+2174
-12
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//
2+
// Bootstrap.swift
3+
// Loop Widget Extension
4+
//
5+
// Created by Bastiaan Verhaar on 25/06/2024.
6+
// Copyright © 2024 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
class Bootstrap{}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// LocalizedString.swift
3+
// Loop Widget Extension
4+
//
5+
// Created by Bastiaan Verhaar on 25/06/2024.
6+
// Copyright © 2024 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
private class FrameworkBundle {
12+
static let main = Bundle(for: Bootstrap.self)
13+
}
14+
15+
func LocalizedString(_ key: String, tableName: String? = nil, value: String? = nil, comment: String) -> String {
16+
if let value = value {
17+
return NSLocalizedString(key, tableName: tableName, bundle: FrameworkBundle.main, value: value, comment: comment)
18+
} else {
19+
return NSLocalizedString(key, tableName: tableName, bundle: FrameworkBundle.main, comment: comment)
20+
}
21+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
// BasalView.swift
3+
// Loop
4+
//
5+
// Created by Noah Brauner on 8/15/22.
6+
// Copyright © 2022 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import SwiftUI
10+
11+
struct BasalViewActivity: View {
12+
let percent: Double
13+
let rate: Double
14+
15+
var body: some View {
16+
VStack(spacing: 1) {
17+
BasalRateView(percent: percent)
18+
.overlay(
19+
BasalRateView(percent: percent)
20+
.stroke(Color("insulin"), lineWidth: 2)
21+
)
22+
.foregroundColor(Color("insulin").opacity(0.5))
23+
.frame(width: 44, height: 22)
24+
25+
if let rateString = decimalFormatter.string(from: NSNumber(value: rate)) {
26+
Text("\(rateString)U")
27+
.font(.subheadline)
28+
}
29+
else {
30+
Text("-U")
31+
.font(.subheadline)
32+
}
33+
}
34+
}
35+
36+
private let decimalFormatter: NumberFormatter = {
37+
let formatter = NumberFormatter()
38+
formatter.numberStyle = .decimal
39+
formatter.minimumFractionDigits = 1
40+
formatter.minimumIntegerDigits = 1
41+
formatter.positiveFormat = "+0.0##"
42+
formatter.negativeFormat = "-0.0##"
43+
44+
return formatter
45+
}()
46+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
//
2+
// ChartValues.swift
3+
// Loop Widget Extension
4+
//
5+
// Created by Bastiaan Verhaar on 25/06/2024.
6+
// Copyright © 2024 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import SwiftUI
11+
import Charts
12+
13+
@available(iOS 16.2, *)
14+
struct ChartView: View {
15+
private let glucoseSampleData: [ChartValues]
16+
private let predicatedData: [ChartValues]
17+
private let glucoseRanges: [GlucoseRangeValue]
18+
private let preset: Preset?
19+
private let yAxisMarks: [Double]
20+
21+
init(glucoseSamples: [GlucoseSampleAttributes], predicatedGlucose: [Double], predicatedStartDate: Date?, predicatedInterval: TimeInterval?, useLimits: Bool, lowerLimit: Double, upperLimit: Double, glucoseRanges: [GlucoseRangeValue], preset: Preset?, yAxisMarks: [Double]) {
22+
self.glucoseSampleData = ChartValues.convert(data: glucoseSamples, useLimits: useLimits, lowerLimit: lowerLimit, upperLimit: upperLimit)
23+
self.predicatedData = ChartValues.convert(
24+
data: predicatedGlucose,
25+
startDate: predicatedStartDate ?? Date.now,
26+
interval: predicatedInterval ?? .minutes(5),
27+
useLimits: useLimits,
28+
lowerLimit: lowerLimit,
29+
upperLimit: upperLimit
30+
)
31+
self.preset = preset
32+
self.glucoseRanges = glucoseRanges
33+
self.yAxisMarks = yAxisMarks
34+
}
35+
36+
init(glucoseSamples: [GlucoseSampleAttributes], useLimits: Bool, lowerLimit: Double, upperLimit: Double, glucoseRanges: [GlucoseRangeValue], preset: Preset?, yAxisMarks: [Double]) {
37+
self.glucoseSampleData = ChartValues.convert(data: glucoseSamples, useLimits: useLimits, lowerLimit: lowerLimit, upperLimit: upperLimit)
38+
self.predicatedData = []
39+
self.preset = preset
40+
self.glucoseRanges = glucoseRanges
41+
self.yAxisMarks = yAxisMarks
42+
}
43+
44+
var body: some View {
45+
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)){
46+
Chart {
47+
if let preset = self.preset, predicatedData.count > 0, preset.endDate > Date.now.addingTimeInterval(.hours(-6)) {
48+
RectangleMark(
49+
xStart: .value("Start", preset.startDate),
50+
xEnd: .value("End", preset.endDate),
51+
yStart: .value("Preset override", preset.minValue),
52+
yEnd: .value("Preset override", preset.maxValue)
53+
)
54+
.foregroundStyle(.primary)
55+
.opacity(0.6)
56+
}
57+
58+
ForEach(glucoseRanges) { item in
59+
RectangleMark(
60+
xStart: .value("Start", item.startDate),
61+
xEnd: .value("End", item.endDate),
62+
yStart: .value("Glucose range", item.minValue),
63+
yEnd: .value("Glucose range", item.maxValue)
64+
)
65+
.foregroundStyle(.primary)
66+
.opacity(0.3)
67+
}
68+
69+
ForEach(glucoseSampleData) { item in
70+
PointMark (x: .value("Date", item.x),
71+
y: .value("Glucose level", item.y)
72+
)
73+
.symbolSize(10)
74+
.foregroundStyle(by: .value("Color", item.color))
75+
}
76+
77+
ForEach(predicatedData) { item in
78+
LineMark (x: .value("Date", item.x),
79+
y: .value("Glucose level", item.y)
80+
)
81+
.lineStyle(StrokeStyle(lineWidth: 2, dash: [6, 5]))
82+
.foregroundStyle(by: .value("Color", item.color))
83+
}
84+
}
85+
.chartForegroundStyleScale([
86+
"Good": .green,
87+
"High": .orange,
88+
"Low": .red,
89+
"Default": Color("glucose")
90+
])
91+
.chartPlotStyle { plotContent in
92+
plotContent.background(.cyan.opacity(0.15))
93+
}
94+
.chartLegend(.hidden)
95+
.chartYScale(domain: [yAxisMarks.first ?? 0, yAxisMarks.last ?? 0])
96+
.chartYAxis {
97+
AxisMarks(values: yAxisMarks)
98+
}
99+
.chartYAxis {
100+
AxisMarks(position: .leading) { _ in
101+
AxisValueLabel().foregroundStyle(Color.primary)
102+
AxisGridLine(stroke: .init(lineWidth: 0.1, dash: [2, 3]))
103+
.foregroundStyle(Color.primary)
104+
}
105+
}
106+
.chartXAxis {
107+
AxisMarks(position: .automatic, values: .stride(by: .hour)) { _ in
108+
AxisValueLabel(format: .dateTime.hour(.twoDigits(amPM: .narrow)), anchor: .top)
109+
.foregroundStyle(Color.primary)
110+
AxisGridLine(stroke: .init(lineWidth: 0.1, dash: [2, 3]))
111+
.foregroundStyle(Color.primary)
112+
}
113+
}
114+
115+
if let preset = self.preset, preset.endDate > Date.now {
116+
Text(preset.title)
117+
.font(.footnote)
118+
.padding(.trailing, 5)
119+
.padding(.top, 2)
120+
}
121+
}
122+
}
123+
}
124+
125+
struct ChartValues: Identifiable {
126+
public let id: UUID
127+
public let x: Date
128+
public let y: Double
129+
public let color: String
130+
131+
init(x: Date, y: Double, color: String) {
132+
self.id = UUID()
133+
self.x = x
134+
self.y = y
135+
self.color = color
136+
}
137+
138+
static func convert(data: [Double], startDate: Date, interval: TimeInterval, useLimits: Bool, lowerLimit: Double, upperLimit: Double) -> [ChartValues] {
139+
let twoHours = Date.now.addingTimeInterval(.hours(4))
140+
141+
return data.enumerated().filter { (index, item) in
142+
return startDate.addingTimeInterval(interval * Double(index)) < twoHours
143+
}.map { (index, item) in
144+
return ChartValues(
145+
x: startDate.addingTimeInterval(interval * Double(index)),
146+
y: item,
147+
color: !useLimits ? "Default" : item < lowerLimit ? "Low" : item > upperLimit ? "High" : "Good"
148+
)
149+
}
150+
}
151+
152+
static func convert(data: [GlucoseSampleAttributes], useLimits: Bool, lowerLimit: Double, upperLimit: Double) -> [ChartValues] {
153+
return data.map { item in
154+
return ChartValues(
155+
x: item.x,
156+
y: item.y,
157+
color: !useLimits ? "Default" : item.y < lowerLimit ? "Low" : item.y > upperLimit ? "High" : "Good"
158+
)
159+
}
160+
}
161+
}

0 commit comments

Comments
 (0)