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 36 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 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
Jul 25, 2024
c68d743
feat: Add glucose targets & preset targets to Live Activity
Jul 25, 2024
3a2ab67
fix: Fix preMeal override & remove glucoseTarget while override is ac…
bastiaanv Jul 27, 2024
95f6f13
wip
Jul 31, 2024
c69db56
merge
Jul 31, 2024
985a1e4
fix: Restruct preset code
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
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
11 changes: 11 additions & 0 deletions Loop Widget Extension/Bootstrap/Bootstrap.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// Bootstrap.swift
// Loop Widget Extension
//
// Created by Bastiaan Verhaar on 25/06/2024.
// Copyright © 2024 LoopKit Authors. All rights reserved.
//

import Foundation

class Bootstrap{}
21 changes: 21 additions & 0 deletions Loop Widget Extension/Helpers/LocalizedString.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// LocalizedString.swift
// Loop Widget Extension
//
// Created by Bastiaan Verhaar on 25/06/2024.
// Copyright © 2024 LoopKit Authors. All rights reserved.
//

import Foundation

private class FrameworkBundle {
static let main = Bundle(for: Bootstrap.self)
}

func LocalizedString(_ key: String, tableName: String? = nil, value: String? = nil, comment: String) -> String {
if let value = value {
return NSLocalizedString(key, tableName: tableName, bundle: FrameworkBundle.main, value: value, comment: comment)
} else {
return NSLocalizedString(key, tableName: tableName, bundle: FrameworkBundle.main, comment: comment)
}
}
46 changes: 46 additions & 0 deletions Loop Widget Extension/Live Activity/BasalViewActivity.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// BasalView.swift
// Loop
//
// Created by Noah Brauner on 8/15/22.
// Copyright © 2022 LoopKit Authors. All rights reserved.
//

import SwiftUI

struct BasalViewActivity: View {
let percent: Double
let rate: Double

var body: some View {
VStack(spacing: 1) {
BasalRateView(percent: percent)
.overlay(
BasalRateView(percent: percent)
.stroke(Color("insulin"), lineWidth: 2)
)
.foregroundColor(Color("insulin").opacity(0.5))
.frame(width: 44, height: 22)

if let rateString = decimalFormatter.string(from: NSNumber(value: rate)) {
Text("\(rateString)U")
.font(.subheadline)
}
else {
Text("-U")
.font(.subheadline)
}
}
}

private let decimalFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 1
formatter.minimumIntegerDigits = 1
formatter.positiveFormat = "+0.0##"
formatter.negativeFormat = "-0.0##"

return formatter
}()
}
159 changes: 159 additions & 0 deletions Loop Widget Extension/Live Activity/ChartView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
//
// ChartValues.swift
// Loop Widget Extension
//
// Created by Bastiaan Verhaar on 25/06/2024.
// Copyright © 2024 LoopKit Authors. All rights reserved.
//

import Foundation
import SwiftUI
import Charts

struct ChartView: View {
private let glucoseSampleData: [ChartValues]
private let predicatedData: [ChartValues]
private let glucoseRanges: [GlucoseRangeValue]
private let preset: Preset?
private let yAxisMarks: [Double]

init(glucoseSamples: [GlucoseSampleAttributes], predicatedGlucose: [Double], predicatedStartDate: Date?, predicatedInterval: TimeInterval?, useLimits: Bool, lowerLimit: Double, upperLimit: Double, glucoseRanges: [GlucoseRangeValue], preset: Preset?, yAxisMarks: [Double]) {
self.glucoseSampleData = ChartValues.convert(data: glucoseSamples, useLimits: useLimits, lowerLimit: lowerLimit, upperLimit: upperLimit)
self.predicatedData = ChartValues.convert(
data: predicatedGlucose,
startDate: predicatedStartDate ?? Date.now,
interval: predicatedInterval ?? .minutes(5),
useLimits: useLimits,
lowerLimit: lowerLimit,
upperLimit: upperLimit
)
self.preset = preset
self.glucoseRanges = glucoseRanges
self.yAxisMarks = yAxisMarks
}

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

var body: some View {
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)){
Chart {
if let preset = self.preset, predicatedData.count > 0, preset.endDate > Date.now.addingTimeInterval(.hours(-6)) {
RectangleMark(
xStart: .value("Start", preset.startDate),
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,
"Low": .red,
"Default": .blue
])
.chartPlotStyle { plotContent in
plotContent.background(.cyan.opacity(0.15))
}
.chartLegend(.hidden)
.chartYScale(domain: [yAxisMarks.first ?? 0, yAxisMarks.last ?? 0])
.chartYAxis {
AxisMarks(values: yAxisMarks)
}
.chartYAxis {
AxisMarks(position: .leading) { _ in
AxisValueLabel().foregroundStyle(Color.primary)
AxisGridLine(stroke: .init(lineWidth: 0.1, dash: [2, 3]))
.foregroundStyle(Color.primary)
}
}
.chartXAxis {
AxisMarks(position: .automatic, values: .stride(by: .hour)) { _ in
AxisValueLabel(format: .dateTime.hour(.twoDigits(amPM: .narrow)), anchor: .top)
.foregroundStyle(Color.primary)
AxisGridLine(stroke: .init(lineWidth: 0.1, dash: [2, 3]))
.foregroundStyle(Color.primary)
}
}

if let preset = self.preset, preset.endDate > Date.now {
Text(preset.title)
.font(.footnote)
.padding(.trailing, 5)
.padding(.top, 2)
}
}
}
}

struct ChartValues: Identifiable {
public let id: UUID
public let x: Date
public let y: Double
public let color: String

init(x: Date, y: Double, color: String) {
self.id = UUID()
self.x = x
self.y = y
self.color = color
}

static func convert(data: [Double], startDate: Date, interval: TimeInterval, useLimits: Bool, lowerLimit: Double, upperLimit: Double) -> [ChartValues] {
let twoHours = Date.now.addingTimeInterval(.hours(4))

return data.enumerated().filter { (index, item) in
return startDate.addingTimeInterval(interval * Double(index)) < twoHours
}.map { (index, item) in
return ChartValues(
x: startDate.addingTimeInterval(interval * Double(index)),
y: item,
color: !useLimits ? "Default" : item < lowerLimit ? "Low" : item > upperLimit ? "High" : "Good"
)
}
}

static func convert(data: [GlucoseSampleAttributes], useLimits: Bool, lowerLimit: Double, upperLimit: Double) -> [ChartValues] {
return data.map { item in
return ChartValues(
x: item.x,
y: item.y,
color: !useLimits ? "Default" : item.y < lowerLimit ? "Low" : item.y > upperLimit ? "High" : "Good"
)
}
}
}
Loading