Browse Source

Major live activity refactoring to make use of colorScheme environment

Deniz Cengiz 1 year ago
parent
commit
dcb265f2a7
2 changed files with 324 additions and 415 deletions
  1. 268 413
      LiveActivity/LiveActivity.swift
  2. 56 2
      Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

+ 268 - 413
LiveActivity/LiveActivity.swift

@@ -1,6 +1,5 @@
 import ActivityKit
 import Charts
-import Foundation
 import SwiftUI
 import WidgetKit
 
@@ -59,227 +58,247 @@ extension NumberFormatter {
     }()
 }
 
-struct LiveActivity: Widget {
-    private let dateFormatter: DateFormatter = {
-        var f = DateFormatter()
-        f.dateStyle = .none
-        f.timeStyle = .short
-        return f
-    }()
+extension Color {
+    static let systemBackground = Color(UIColor.systemBackground)
+}
 
-    private var bolusFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = 1
-        formatter.decimalSeparator = "."
-        return formatter
+struct LiveActivity: Widget {
+    var body: some WidgetConfiguration {
+        ActivityConfiguration(for: LiveActivityAttributes.self) { context in
+            LiveActivityView(context: context)
+        } dynamicIsland: { context in
+            DynamicIsland {
+                DynamicIslandExpandedRegion(.leading) {
+                    LiveActivityExpandedLeadingView(context: context)
+                }
+                DynamicIslandExpandedRegion(.trailing) {
+                    LiveActivityExpandedTrailingView(context: context)
+                }
+                DynamicIslandExpandedRegion(.bottom) {
+                    LiveActivityExpandedBottomView(context: context)
+                }
+                DynamicIslandExpandedRegion(.center) {
+                    LiveActivityExpandedCenterView(context: context)
+                }
+            } compactLeading: {
+                LiveActivityCompactLeadingView(context: context)
+            } compactTrailing: {
+                LiveActivityCompactTrailingView(context: context)
+            } minimal: {
+                LiveActivityMinimalView(context: context)
+            }
+        }
     }
+}
 
-    private var carbsFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = 0
-        return formatter
-    }
+struct LiveActivityView: View {
+    @Environment(\.colorScheme) var colorScheme
+    var context: ActivityViewContext<LiveActivityAttributes>
 
-    @ViewBuilder private func changeLabel(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
-        HStack(spacing: -5) {
-            if !context.state.change.isEmpty {
-                Text(context.state.change).foregroundStyle(.primary).font(.subheadline)
-                    .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
-            } else {
-                Text("--")
+    var body: some View {
+        if let detailedViewState = context.state.detailedViewState {
+            VStack {
+                LiveActivityChartView(context: context, additionalState: detailedViewState)
+                    .frame(maxWidth: UIScreen.main.bounds.width * 0.9)
+                    .frame(height: 80)
+
+                HStack {
+                    ForEach(context.state.itemOrder, id: \.self) { item in
+                        switch item {
+                        case "currentGlucose":
+                            if context.state.showCurrentGlucose {
+                                VStack {
+                                    LiveActivityBGLabelView(context: context, additionalState: detailedViewState)
+                                    HStack {
+                                        LiveActivityGlucoseDeltaLabelView(context: context)
+                                        if !context.isStale, let direction = context.state.direction {
+                                            Text(direction).font(.headline)
+                                        }
+                                    }
+                                }
+                            }
+                        case "iob":
+                            if context.state.showIOB {
+                                LiveActivityIOBLabelView(context: context, additionalState: detailedViewState)
+                            }
+                        case "cob":
+                            if context.state.showCOB {
+                                LiveActivityCOBLabelView(context: context, additionalState: detailedViewState)
+                            }
+                        case "updatedLabel":
+                            if context.state.showUpdatedLabel {
+                                LiveActivityUpdatedLabelView(context: context, isDetailedLayout: true)
+                            }
+                        default:
+                            EmptyView()
+                        }
+                        Divider().foregroundStyle(.primary).fontWeight(.bold).frame(width: 10)
+                    }
+                }
+            }
+            .privacySensitive()
+            .padding(.all, 14)
+            .foregroundStyle(Color.primary)
+            .activityBackgroundTint(colorScheme == .light ? Color.white.opacity(0.43) : Color.black.opacity(0.43))
+        } else {
+            HStack(spacing: 3) {
+                LiveActivityBGAndTrendView(context: context, size: .expanded).font(.title)
+                Spacer()
+                VStack(alignment: .trailing, spacing: 5) {
+                    LiveActivityGlucoseDeltaLabelView(context: context).font(.title3)
+                    LiveActivityUpdatedLabelView(context: context, isDetailedLayout: false).font(.caption)
+                        .foregroundStyle(.primary.opacity(0.7))
+                }
             }
+            .privacySensitive()
+            .padding(.all, 15)
+            .foregroundStyle(Color.primary)
+            .activityBackgroundTint(colorScheme == .light ? Color.white.opacity(0.43) : Color.black.opacity(0.43))
         }
     }
+}
 
-    @ViewBuilder func cobLabel(
-        context: ActivityViewContext<LiveActivityAttributes>,
-        additionalState: LiveActivityAttributes.ContentAdditionalState
-    ) -> some View {
-        VStack(spacing: 2) {
-            HStack {
-                Text(
-                    carbsFormatter.string(from: additionalState.cob as NSNumber) ?? "--"
-                ).fontWeight(.bold).font(.title3).strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
-                Text(NSLocalizedString("g", comment: "grams of carbs")).foregroundStyle(.primary).font(.headline)
-                    .fontWeight(.bold)
-            }
+// Separate the smaller sections into reusable views
+struct LiveActivityBGAndTrendView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+    fileprivate var size: Size
 
-            Text("COB").font(.subheadline).foregroundStyle(.primary)
-        }
+    var body: some View {
+        let (view, _) = bgAndTrend(context: context, size: size)
+        return view
     }
+}
 
-    @ViewBuilder func iobLabel(
-        context: ActivityViewContext<LiveActivityAttributes>,
-        additionalState: LiveActivityAttributes.ContentAdditionalState
-    ) -> some View {
-        VStack(spacing: 2) {
-            HStack {
-                Text(
-                    bolusFormatter.string(from: additionalState.iob as NSNumber) ?? "--"
-                ).font(.title3).fontWeight(.bold).strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
-                Text(NSLocalizedString("U", comment: "Unit in number of units delivered (keep the space character!)"))
-                    .foregroundStyle(.primary).font(.headline).fontWeight(.bold)
-            }
+struct LiveActivityBGLabelView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+    var additionalState: LiveActivityAttributes.ContentAdditionalState
 
-            Text("IOB").font(.subheadline).foregroundStyle(.primary)
-        }
+    var body: some View {
+        Text(context.state.bg)
+            .fontWeight(.bold)
+            .font(.title3)
+            .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
     }
+}
 
-    @ViewBuilder func mealLabel(
-        context: ActivityViewContext<LiveActivityAttributes>,
-        additionalState: LiveActivityAttributes.ContentAdditionalState
-    ) -> some View {
-        HStack {
-            VStack(alignment: .leading, spacing: 0, content: {
-                HStack {
-                    Image(systemName: "fork.knife")
-                        .font(.title3)
-                        .foregroundColor(.yellow)
-                }
-                HStack {
-                    Image(systemName: "syringe.fill")
-                        .font(.title3)
-                        .foregroundColor(.blue)
-                }
-            })
-            VStack(alignment: .trailing, spacing: 0, content: {
-                HStack {
-                    Text(
-                        carbsFormatter.string(from: additionalState.cob as NSNumber) ?? "--"
-                    ).fontWeight(.bold).font(.title3).strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
-                    Text(NSLocalizedString(" g", comment: "grams of carbs")).foregroundStyle(.primary).font(.headline)
-                }
-                HStack {
-                    Text(
-                        bolusFormatter.string(from: additionalState.iob as NSNumber) ?? "--"
-                    ).font(.title3).fontWeight(.bold).strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
-                    Text(NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)"))
-                        .foregroundStyle(.primary).font(.headline)
-                }
-            })
-            VStack(alignment: .trailing, spacing: 1, content: {
-                if additionalState.isOverrideActive {
-                    Image(systemName: "person.crop.circle.fill.badge.checkmark")
-                        .font(.title3)
-                }
-            })
-        }
-    }
+struct LiveActivityGlucoseDeltaLabelView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
 
-    @ViewBuilder func trend(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
-        if context.isStale {
-            Text("--")
+    var body: some View {
+        if !context.state.change.isEmpty {
+            Text(context.state.change).foregroundStyle(.primary).font(.subheadline)
+                .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
         } else {
-            if let trendSystemImage = context.state.direction {
-                Image(systemName: trendSystemImage)
-            }
+            Text("--")
         }
     }
+}
 
-    private func expiredLabel() -> some View {
-        Text("Live Activity Expired. Open Trio to Refresh")
-            .minimumScaleFactor(0.01)
-    }
+struct LiveActivityIOBLabelView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+    var additionalState: LiveActivityAttributes.ContentAdditionalState
 
-    @ViewBuilder private func updatedLabel(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
-        VStack {
-            let dateText = Text("\(dateFormatter.string(from: context.state.date))").font(.title3).foregroundStyle(.primary)
+    private var bolusFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 1
+        formatter.decimalSeparator = "."
+        return formatter
+    }
 
-            if context.isStale {
-                if #available(iOSApplicationExtension 17.0, *) {
-                    dateText.bold().foregroundStyle(.red)
-                } else {
-                    dateText.bold().foregroundColor(.red)
-                }
-            } else {
-                if #available(iOSApplicationExtension 17.0, *) {
-                    dateText.bold().foregroundStyle(.primary)
-                } else {
-                    dateText.bold().foregroundColor(.primary)
-                }
+    var body: some View {
+        VStack(spacing: 2) {
+            HStack {
+                Text(
+                    bolusFormatter.string(from: additionalState.iob as NSNumber) ?? "--"
+                ).fontWeight(.bold).font(.title3).strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+                Text("U").foregroundStyle(.primary).font(.headline).fontWeight(.bold)
             }
-
-            Text("Updated").font(.subheadline).foregroundStyle(.primary)
+            Text("IOB").font(.subheadline).foregroundStyle(.primary)
         }
     }
+}
 
-    @ViewBuilder private func bgLabel(
-        context: ActivityViewContext<LiveActivityAttributes>,
-        additionalState _: LiveActivityAttributes.ContentAdditionalState
-    ) -> some View {
-        HStack(alignment: .center) {
-            Text(context.state.bg)
-                .fontWeight(.bold)
-                .font(.title3)
-                .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+struct LiveActivityCOBLabelView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+    var additionalState: LiveActivityAttributes.ContentAdditionalState
+
+    var body: some View {
+        VStack(spacing: 2) {
+            HStack {
+                Text(
+                    "\(additionalState.cob)"
+                ).fontWeight(.bold).font(.title3).strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+                Text("g").foregroundStyle(.primary).font(.headline).fontWeight(.bold)
+            }
+            Text("COB").font(.subheadline).foregroundStyle(.primary)
         }
     }
+}
 
-    private func bgAndTrend(context: ActivityViewContext<LiveActivityAttributes>, size: Size) -> (some View, Int) {
-        var characters = 0
+struct LiveActivityUpdatedLabelView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+    var isDetailedLayout: Bool
 
-        let bgText = context.state.bg
-        characters += bgText.count
+    private var dateFormatter: DateFormatter {
+        let formatter = DateFormatter()
+        formatter.dateStyle = .none
+        formatter.timeStyle = .short
+        return formatter
+    }
 
-        // narrow mode is for the minimal dynamic island view
-        // there is not enough space to show all three arrow there
-        // and everything has to be squeezed together to some degree
-        // only display the first arrow character and make it red in case there were more characters
-        var directionText: String?
-        var warnColor: Color?
-        if let direction = context.state.direction {
-            if size == .compact {
-                directionText = String(direction[direction.startIndex ... direction.startIndex])
+    var body: some View {
+        if isDetailedLayout {
+            let dateText = Text("\(dateFormatter.string(from: context.state.date))").font(.title3)
+                .foregroundStyle(.primary)
 
-                if direction.count > 1 {
-                    warnColor = Color.red
+            VStack {
+                if context.isStale {
+                    if #available(iOSApplicationExtension 17.0, *) {
+                        dateText.bold().foregroundStyle(.red)
+                    } else {
+                        dateText.bold().foregroundColor(.red)
+                    }
+                } else {
+                    if #available(iOSApplicationExtension 17.0, *) {
+                        dateText.bold().foregroundStyle(.primary)
+                    } else {
+                        dateText.bold().foregroundColor(.primary)
+                    }
                 }
-            } else {
-                directionText = direction
-            }
 
-            characters += directionText!.count
-        }
+                Text("Updated").font(.subheadline).foregroundStyle(.primary)
+            }
+        } else {
+            let dateText = Text("\(dateFormatter.string(from: context.state.date))").font(.subheadline)
+                .foregroundStyle(.secondary)
 
-        let spacing: CGFloat
-        switch size {
-        case .minimal: spacing = -1
-        case .compact: spacing = 0
-        case .expanded: spacing = 3
-        }
+            HStack {
+                Text("Updated:").font(.subheadline).foregroundStyle(.secondary)
 
-        let stack = HStack(spacing: spacing) {
-            Text(bgText)
-                .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
-            if let direction = directionText {
-                let text = Text(direction)
-                switch size {
-                case .minimal:
-                    let scaledText = text.scaleEffect(x: 0.7, y: 0.7, anchor: .leading)
-                    if let warnColor {
-                        scaledText.foregroundStyle(warnColor)
+                if context.isStale {
+                    if #available(iOSApplicationExtension 17.0, *) {
+                        dateText.bold().foregroundStyle(.red)
                     } else {
-                        scaledText
+                        dateText.bold().foregroundColor(.red)
+                    }
+                } else {
+                    if #available(iOSApplicationExtension 17.0, *) {
+                        dateText.bold().foregroundStyle(.primary)
+                    } else {
+                        dateText.bold().foregroundColor(.primary)
                     }
-                case .compact:
-                    text.scaleEffect(x: 0.8, y: 0.8, anchor: .leading).padding(.trailing, -3)
-
-                case .expanded:
-                    text.scaleEffect(x: 0.7, y: 0.7, anchor: .leading).padding(.trailing, -5)
                 }
             }
         }
-        .foregroundStyle(context.isStale ? Color.primary.opacity(0.5) : Color.primary)
-
-        return (stack, characters)
     }
+}
 
-    @ViewBuilder func chart(
-        context: ActivityViewContext<LiveActivityAttributes>,
-        additionalState: LiveActivityAttributes.ContentAdditionalState
-    ) -> some View {
+struct LiveActivityChartView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+    var additionalState: LiveActivityAttributes.ContentAdditionalState
+
+    var body: some View {
         if context.isStale {
             Text("No data available")
         } else {
@@ -341,260 +360,96 @@ struct LiveActivity: Widget {
             }
         }
     }
+}
 
-    @ViewBuilder func content(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
-        if let detailedViewState = context.state.detailedViewState {
-            VStack(content: {
-                chart(context: context, additionalState: detailedViewState)
-                    .frame(maxWidth: UIScreen.main.bounds.width * 0.9)
-                    .frame(height: 80)
+// Expanded, minimal, compact view components
+struct LiveActivityExpandedLeadingView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
 
-                HStack {
-                    ForEach(context.state.itemOrder, id: \.self) { item in
-                        switch item {
-                        case "currentGlucose":
-                            if context.state.showCurrentGlucose {
-                                VStack {
-                                    bgLabel(context: context, additionalState: detailedViewState)
-                                    HStack {
-                                        changeLabel(context: context)
-                                        if !context.isStale, let direction = context.state.direction {
-                                            Text(direction).font(.headline)
-                                        }
-                                    }
-                                }
-                            }
-                        case "iob":
-                            if context.state.showIOB {
-                                iobLabel(context: context, additionalState: detailedViewState)
-                            }
-                        case "cob":
-                            if context.state.showCOB {
-                                cobLabel(context: context, additionalState: detailedViewState)
-                            }
-                        case "updatedLabel":
-                            if context.state.showUpdatedLabel {
-                                updatedLabel(context: context)
-                            }
-                        default:
-                            EmptyView()
-                        }
-                        Divider().foregroundStyle(.primary).fontWeight(.bold).frame(width: 10)
-                    }
-                }
-            })
-                .privacySensitive()
-                .padding(.all, 14)
-                .imageScale(.small)
-                .foregroundStyle(Color.primary)
-                .activityBackgroundTint(Color.clear)
-        } else {
-            Group {
-                if context.state.isInitialState {
-                    // add vertical and horizontal spacers around the label to ensure that the live activity view gets filled completely
-                    HStack {
-                        Spacer()
-                        VStack {
-                            Spacer()
-                            expiredLabel()
-                            Spacer()
-                        }
-                        Spacer()
-                    }
-                } else {
-                    HStack(spacing: 3) {
-                        bgAndTrend(context: context, size: .expanded).0.font(.title)
-                        Spacer()
-                        VStack(alignment: .trailing, spacing: 5) {
-                            changeLabel(context: context).font(.title3)
-                            updatedLabel(context: context).font(.caption).foregroundStyle(.primary.opacity(0.7))
-                        }
-                    }
-                }
-            }
-            .privacySensitive()
-            .padding(.all, 15)
-            // Semantic BackgroundStyle and Color values work here. They adapt to the given interface style (light mode, dark mode)
-            // Semantic UIColors do NOT (as of iOS 17.1.1). Like UIColor.systemBackgroundColor (it does not adapt to changes of the interface style)
-            // The colorScheme environment varaible that is usually used to detect dark mode does NOT work here (it reports false values)
-            .foregroundStyle(Color.primary)
-            .background(BackgroundStyle.background.opacity(0.4))
-            .activityBackgroundTint(Color.black.opacity(0.4))
-        }
+    var body: some View {
+        LiveActivityBGAndTrendView(context: context, size: .expanded).font(.title2).padding(.leading, 5)
     }
+}
 
-    func dynamicIsland(context: ActivityViewContext<LiveActivityAttributes>) -> DynamicIsland {
-        DynamicIsland {
-            DynamicIslandExpandedRegion(.leading) {
-                bgAndTrend(context: context, size: .expanded).0.font(.title2).padding(.leading, 5)
-            }
-            DynamicIslandExpandedRegion(.trailing) {
-                changeLabel(context: context).font(.title2).padding(.trailing, 5)
-            }
-            DynamicIslandExpandedRegion(.bottom) {
-                if context.state.isInitialState {
-                    expiredLabel()
-                } else if let detailedViewState = context.state.detailedViewState {
-                    chart(context: context, additionalState: detailedViewState)
-                } else {
-                    Group {
-                        updatedLabel(context: context).font(.caption).foregroundStyle(Color.secondary)
-                    }
-                    .frame(
-                        maxHeight: .infinity,
-                        alignment: .bottom
-                    )
-                }
-            }
-            DynamicIslandExpandedRegion(.center) {
-                if context.state.detailedViewState != nil {
-                    updatedLabel(context: context).font(.caption).foregroundStyle(Color.secondary)
-                }
-            }
-        } compactLeading: {
-            bgAndTrend(context: context, size: .compact).0.padding(.leading, 4)
-        } compactTrailing: {
-            changeLabel(context: context).padding(.trailing, 4)
-        } minimal: {
-            let (_label, characterCount) = bgAndTrend(context: context, size: .minimal)
-            let label = _label.padding(.leading, 7).padding(.trailing, 3)
-
-            if characterCount < 4 {
-                label
-            } else if characterCount < 5 {
-                label.fontWidth(.condensed)
-            } else {
-                label.fontWidth(.compressed)
-            }
-        }
-        .widgetURL(URL(string: "Trio://"))
-        .keylineTint(Color.purple)
-        .contentMargins(.horizontal, 0, for: .minimal)
-        .contentMargins(.trailing, 0, for: .compactLeading)
-        .contentMargins(.leading, 0, for: .compactTrailing)
-    }
+struct LiveActivityExpandedTrailingView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
 
-    var body: some WidgetConfiguration {
-        ActivityConfiguration(for: LiveActivityAttributes.self, content: self.content, dynamicIsland: self.dynamicIsland)
+    var body: some View {
+        LiveActivityGlucoseDeltaLabelView(context: context).font(.title2).padding(.trailing, 5)
     }
 }
 
-private extension LiveActivityAttributes {
-    static var preview: LiveActivityAttributes {
-        LiveActivityAttributes(startDate: Date())
+struct LiveActivityExpandedBottomView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+
+    var body: some View {
+        if context.state.isInitialState {
+            Text("Live Activity Expired. Open Trio to Refresh")
+        } else if let detailedViewState = context.state.detailedViewState {
+            LiveActivityChartView(context: context, additionalState: detailedViewState)
+        }
     }
 }
 
-private extension LiveActivityAttributes.ContentState {
-    // 0 is the widest digit. Use this to get an upper bound on text width.
-
-    // Use mmol/l notation with decimal point as well for the same reason, it uses up to 4 characters, while mg/dl uses up to 3
-    static var testWide: LiveActivityAttributes.ContentState {
-        LiveActivityAttributes.ContentState(
-            bg: "00.0",
-            direction: "→",
-            change: "+0.0",
-            date: Date(),
-            detailedViewState: nil,
-            showCOB: true,
-            showIOB: true,
-            showCurrentGlucose: true,
-            showUpdatedLabel: true,
-            itemOrder: ["currentGlucose", "iob", "cob", "updatedLabel"],
-            isInitialState: false
-        )
+struct LiveActivityExpandedCenterView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+
+    var body: some View {
+        LiveActivityUpdatedLabelView(context: context, isDetailedLayout: false).font(.caption).foregroundStyle(Color.secondary)
     }
+}
+
+struct LiveActivityCompactLeadingView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
 
-    static var testVeryWide: LiveActivityAttributes.ContentState {
-        LiveActivityAttributes.ContentState(
-            bg: "00.0",
-            direction: "↑↑",
-            change: "+0.0",
-            date: Date(),
-            detailedViewState: nil,
-            showCOB: true,
-            showIOB: true,
-            showCurrentGlucose: true,
-            showUpdatedLabel: true,
-            itemOrder: ["currentGlucose", "iob", "cob", "updatedLabel"],
-            isInitialState: false
-        )
+    var body: some View {
+        LiveActivityBGAndTrendView(context: context, size: .compact).padding(.leading, 4)
     }
+}
+
+struct LiveActivityCompactTrailingView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
 
-    static var testSuperWide: LiveActivityAttributes.ContentState {
-        LiveActivityAttributes.ContentState(
-            bg: "00.0",
-            direction: "↑↑↑",
-            change: "+0.0",
-            date: Date(),
-            detailedViewState: nil,
-            showCOB: true,
-            showIOB: true,
-            showCurrentGlucose: true,
-            showUpdatedLabel: true,
-            itemOrder: ["currentGlucose", "iob", "cob", "updatedLabel"],
-            isInitialState: false
-        )
+    var body: some View {
+        LiveActivityGlucoseDeltaLabelView(context: context).padding(.trailing, 4)
     }
+}
+
+struct LiveActivityMinimalView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+
+    var body: some View {
+        let (label, characterCount) = bgAndTrend(context: context, size: .minimal)
+        let adjustedLabel = label.padding(.leading, 7).padding(.trailing, 3)
 
-    // 2 characters for BG, 1 character for change is the minimum that will be shown
-    static var testNarrow: LiveActivityAttributes.ContentState {
-        LiveActivityAttributes.ContentState(
-            bg: "00",
-            direction: "↑",
-            change: "+0",
-            date: Date(),
-            detailedViewState: nil,
-            showCOB: true,
-            showIOB: true,
-            showCurrentGlucose: true,
-            showUpdatedLabel: true,
-            itemOrder: ["currentGlucose", "iob", "cob", "updatedLabel"],
-            isInitialState: false
-        )
+        if characterCount < 4 {
+            adjustedLabel
+        } else if characterCount < 5 {
+            adjustedLabel.fontWidth(.condensed)
+        } else {
+            adjustedLabel.fontWidth(.compressed)
+        }
     }
+}
 
-    static var testMedium: LiveActivityAttributes.ContentState {
-        LiveActivityAttributes.ContentState(
-            bg: "000",
-            direction: "↗︎",
-            change: "+00",
-            date: Date(),
-            detailedViewState: nil,
-            showCOB: true,
-            showIOB: true,
-            showCurrentGlucose: true,
-            showUpdatedLabel: true,
-            itemOrder: ["currentGlucose", "iob", "cob", "updatedLabel"],
-            isInitialState: false
-        )
+// Helper function for bgAndTrend logic
+private func bgAndTrend(context: ActivityViewContext<LiveActivityAttributes>, size _: Size) -> (some View, Int) {
+    var characters = 0
+    let bgText = context.state.bg
+    characters += bgText.count
+
+    var directionText: String?
+    if let direction = context.state.direction {
+        directionText = direction
+        characters += directionText!.count
     }
 
-    static var testExpired: LiveActivityAttributes.ContentState {
-        LiveActivityAttributes.ContentState(
-            bg: "--",
-            direction: nil,
-            change: "--",
-            date: Date().addingTimeInterval(-60 * 60),
-            detailedViewState: nil,
-            showCOB: true,
-            showIOB: true,
-            showCurrentGlucose: true,
-            showUpdatedLabel: true,
-            itemOrder: ["currentGlucose", "iob", "cob", "updatedLabel"],
-            isInitialState: true
-        )
+    let stack = HStack {
+        Text(bgText)
+        if let direction = directionText {
+            Text(direction)
+        }
     }
-}
 
-@available(iOS 17.0, iOSApplicationExtension 17.0, *)
-#Preview("Notification", as: .content, using: LiveActivityAttributes.preview) {
-    LiveActivity()
-} contentStates: {
-    LiveActivityAttributes.ContentState.testSuperWide
-    LiveActivityAttributes.ContentState.testVeryWide
-    LiveActivityAttributes.ContentState.testWide
-    LiveActivityAttributes.ContentState.testMedium
-    LiveActivityAttributes.ContentState.testNarrow
-    LiveActivityAttributes.ContentState.testExpired
+    return (stack, characters)
 }

+ 56 - 2
Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -1,5 +1,5 @@
 {
-  "originHash" : "cef813f4bbb01679d4ac9bf4a9f82c1a0a61e44dc839643e81aa92e4d00642bc",
+  "originHash" : "59ac7eba66375d6eb406e758cb0b9964f4b3b0ae45c5665596f00384c32262b9",
   "pins" : [
     {
       "identity" : "cryptoswift",
@@ -11,6 +11,15 @@
       }
     },
     {
+      "identity" : "mkringprogressview",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/maxkonovalov/MKRingProgressView.git",
+      "state" : {
+        "branch" : "master",
+        "revision" : "660888aab1d2ab0ed7eb9eb53caec12af4955fa7"
+      }
+    },
+    {
       "identity" : "slidebutton",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/no-comment/SlideButton",
@@ -20,15 +29,60 @@
       }
     },
     {
+      "identity" : "swift-algorithms",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/apple/swift-algorithms",
+      "state" : {
+        "revision" : "2327673b0e9c7e90e6b1826376526ec3627210e4",
+        "version" : "0.2.1"
+      }
+    },
+    {
+      "identity" : "swift-numerics",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/apple/swift-numerics",
+      "state" : {
+        "revision" : "6583ac70c326c3ee080c1d42d9ca3361dca816cd",
+        "version" : "0.1.0"
+      }
+    },
+    {
       "identity" : "swiftcharts",
       "kind" : "remoteSourceControl",
-      "location" : "https://github.com/ivanschuetz/SwiftCharts",
+      "location" : "https://github.com/ivanschuetz/SwiftCharts.git",
       "state" : {
         "branch" : "master",
         "revision" : "c354c1945bb35a1f01b665b22474f6db28cba4a2"
       }
     },
     {
+      "identity" : "swiftdate",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/malcommac/SwiftDate",
+      "state" : {
+        "revision" : "6190d0cefff3013e77ed567e6b074f324e5c5bf5",
+        "version" : "6.3.1"
+      }
+    },
+    {
+      "identity" : "swiftmessages",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/SwiftKickMobile/SwiftMessages",
+      "state" : {
+        "revision" : "62e12e138fc3eedf88c7553dd5d98712aa119f40",
+        "version" : "9.0.9"
+      }
+    },
+    {
+      "identity" : "swinject",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/Swinject/Swinject",
+      "state" : {
+        "revision" : "be9dbcc7b86811bc131539a20c6f9c2d3e56919f",
+        "version" : "2.9.1"
+      }
+    },
+    {
       "identity" : "tidepoolkit",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/tidepool-org/TidepoolKit",