Kaynağa Gözat

Merge pull request #842 from bastiaanv/feat/dev-medtrum

chore: update Medtrum & sync dev
Sjoerd Bozon 6 ay önce
ebeveyn
işleme
c9d640598b
23 değiştirilmiş dosya ile 473 ekleme ve 206 silme
  1. 1 1
      Config.xcconfig
  2. 54 0
      LiveActivity/LiveActivity+Helper.swift
  3. 101 35
      LiveActivity/LiveActivity.swift
  4. 25 18
      LiveActivity/Views/LiveActivityChartView.swift
  5. 4 1
      LiveActivity/Views/LiveActivityGlucoseDeltaLabelView.swift
  6. 62 44
      LiveActivity/Views/LiveActivityView.swift
  7. 10 14
      LiveActivity/Views/WidgetItems/LiveActivityBGLabelLargeView.swift
  8. 0 6
      LiveActivity/Views/WidgetItems/LiveActivityBGLabelView.swift
  9. 44 0
      LiveActivity/Views/WidgetItems/LiveActivityBGLabelWatchView.swift
  10. 5 10
      LiveActivity/Views/WidgetItems/LiveActivityCOBLabelView.swift
  11. 8 14
      LiveActivity/Views/WidgetItems/LiveActivityIOBLabelView.swift
  12. 8 8
      LiveActivity/Views/WidgetItems/LiveActivityTotalDailyDoseView.swift
  13. 16 9
      LiveActivity/Views/WidgetItems/LiveActivityUpdatedLabelView.swift
  14. 1 1
      MedtrumKit
  15. 9 1
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  16. 5 0
      Trio/Sources/Models/TrioSettings.swift
  17. 4 1
      Trio/Sources/Modules/Home/View/Header/LoopStatusView.swift
  18. 2 0
      Trio/Sources/Modules/LiveActivitySettings/LiveActivitySettingsStateModel.swift
  19. 67 5
      Trio/Sources/Modules/LiveActivitySettings/View/LiveActivitySettingsRootView.swift
  20. 9 3
      Trio/Sources/Services/LiveActivity/LiveActitiyAttributes.swift
  21. 16 27
      Trio/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift
  22. 1 1
      Trio/Sources/Services/LiveActivity/LiveActivityAttributes.swift
  23. 21 7
      Trio/Sources/Services/LiveActivity/LiveActivityManager.swift

+ 1 - 1
Config.xcconfig

@@ -19,7 +19,7 @@ TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group
 
 // The developers set the version numbers, please leave them alone
 APP_VERSION = 0.6.0
-APP_DEV_VERSION = 0.6.0.7
+APP_DEV_VERSION = 0.6.0.9
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 

+ 54 - 0
LiveActivity/LiveActivity+Helper.swift

@@ -179,6 +179,7 @@ func bgAndTrend(
             case .minimal:
                 let scaledText = text.scaleEffect(x: 0.7, y: 0.7, anchor: .leading)
                 scaledText.foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
+
             case .compact:
                 text.scaleEffect(x: 0.8, y: 0.8, anchor: .leading).padding(.trailing, -3)
 
@@ -191,3 +192,56 @@ func bgAndTrend(
 
     return (stack, characters)
 }
+
+private struct LiveActivityWatchOS: EnvironmentKey {
+    // Value to add support for older iOS version (17 and lower) in order to keep using the ActivityFamily class
+    static let defaultValue = false
+}
+
+public extension EnvironmentValues {
+    var isWatchOS: Bool {
+        get { self[LiveActivityWatchOS.self] }
+        set { self[LiveActivityWatchOS.self] = newValue }
+    }
+}
+
+@available(iOS 18, *) struct LiveActivityWatchOSModifier: ViewModifier {
+    @Environment(\.activityFamily) var activityFamily
+
+    func body(content: Content) -> some View {
+        content.environment(\.isWatchOS, activityFamily == .small)
+    }
+}
+
+extension View {
+    @ViewBuilder func addIsWatchOS() -> some View {
+        if #available(iOS 18, *) {
+            modifier(LiveActivityWatchOSModifier())
+        } else {
+            self
+        }
+    }
+
+    @ViewBuilder func addLiveActivityModifiers(isWatchOS: Bool) -> some View {
+        modifier(LiveActivityModifiers(isWatchOS: isWatchOS))
+    }
+}
+
+struct LiveActivityModifiers: ViewModifier {
+    let isWatchOS: Bool
+
+    func body(content: Content) -> some View {
+        content
+            .padding(.all, isWatchOS ? 10 : 14)
+            .frame(minHeight: 0, maxHeight: .infinity)
+            .privacySensitive()
+            // 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 variable does work here, but BackgroundStyle gives us this functionality for free
+            .foregroundStyle(Color.primary)
+            .background(BackgroundStyle.background.opacity(isWatchOS ? 1 : 0.4))
+            .activityBackgroundTint(isWatchOS ? .black : Color.clear)
+    }
+}

+ 101 - 35
LiveActivity/LiveActivity.swift

@@ -4,8 +4,9 @@ import WidgetKit
 
 struct LiveActivity: Widget {
     var body: some WidgetConfiguration {
-        ActivityConfiguration(for: LiveActivityAttributes.self) { context in
+        let configuration = ActivityConfiguration(for: LiveActivityAttributes.self) { context in
             LiveActivityView(context: context)
+                .addIsWatchOS()
         } dynamicIsland: { context in
             let hasStaticColorScheme = context.state.glucoseColorScheme == "staticColor"
 
@@ -19,8 +20,10 @@ struct LiveActivity: Widget {
 
                 return Color.getDynamicGlucoseColor(
                     glucoseValue: Decimal(string: state.bg) ?? 100,
-                    highGlucoseColorValue: !hasStaticColorScheme ? hardCodedHigh : state.highGlucose,
-                    lowGlucoseColorValue: !hasStaticColorScheme ? hardCodedLow : state.lowGlucose,
+                    highGlucoseColorValue: !hasStaticColorScheme
+                        ? hardCodedHigh : state.highGlucose,
+                    lowGlucoseColorValue: !hasStaticColorScheme
+                        ? hardCodedLow : state.lowGlucose,
                     targetGlucose: isMgdL ? state.target : state.target.asMmolL,
                     glucoseColorScheme: state.glucoseColorScheme
                 )
@@ -28,7 +31,10 @@ struct LiveActivity: Widget {
 
             return DynamicIsland {
                 DynamicIslandExpandedRegion(.leading) {
-                    LiveActivityExpandedLeadingView(context: context, glucoseColor: glucoseColor)
+                    LiveActivityExpandedLeadingView(
+                        context: context,
+                        glucoseColor: glucoseColor
+                    )
                 }
                 DynamicIslandExpandedRegion(.trailing) {
                     LiveActivityExpandedTrailingView(
@@ -43,11 +49,20 @@ struct LiveActivity: Widget {
                     LiveActivityExpandedCenterView(context: context)
                 }
             } compactLeading: {
-                LiveActivityCompactLeadingView(context: context, glucoseColor: glucoseColor)
+                LiveActivityCompactLeadingView(
+                    context: context,
+                    glucoseColor: glucoseColor
+                )
             } compactTrailing: {
-                LiveActivityCompactTrailingView(context: context, glucoseColor: hasStaticColorScheme ? .primary : glucoseColor)
+                LiveActivityCompactTrailingView(
+                    context: context,
+                    glucoseColor: hasStaticColorScheme ? .primary : glucoseColor
+                )
             } minimal: {
-                LiveActivityMinimalView(context: context, glucoseColor: glucoseColor)
+                LiveActivityMinimalView(
+                    context: context,
+                    glucoseColor: glucoseColor
+                )
             }
             .widgetURL(URL(string: "Trio://"))
             .keylineTint(glucoseColor)
@@ -55,6 +70,12 @@ struct LiveActivity: Widget {
             .contentMargins(.trailing, 0, for: .compactLeading)
             .contentMargins(.leading, 0, for: .compactTrailing)
         }
+
+        if #available(iOS 18.0, *) {
+            return configuration.supplementalActivityFamilies([.small])
+        } else {
+            return configuration
+        }
     }
 }
 
@@ -73,25 +94,35 @@ private extension LiveActivityAttributes {
 
 private extension LiveActivityAttributes.ContentState {
     static var chartData: [MockGlucoseData] = [
-        MockGlucoseData(glucose: 120, date: Date().addingTimeInterval(-600), direction: "flat"),
-        MockGlucoseData(glucose: 125, date: Date().addingTimeInterval(-300), direction: "flat"),
+        MockGlucoseData(
+            glucose: 120,
+            date: Date().addingTimeInterval(-600),
+            direction: "flat"
+        ),
+        MockGlucoseData(
+            glucose: 125,
+            date: Date().addingTimeInterval(-300),
+            direction: "flat"
+        ),
         MockGlucoseData(glucose: 130, date: Date(), direction: "flat")
     ]
 
-    static var detailedViewState = LiveActivityAttributes.ContentAdditionalState(
-        chart: chartData.map { Decimal($0.glucose) },
-        chartDate: chartData.map(\.date),
-        rotationDegrees: 0,
-        cob: 20,
-        iob: 1.5,
-        tdd: 43.21,
-        isOverrideActive: false,
-        overrideName: "Exercise",
-        overrideDate: Date().addingTimeInterval(-3600),
-        overrideDuration: 120,
-        overrideTarget: 150,
-        widgetItems: LiveActivityAttributes.LiveActivityItem.defaultItems
-    )
+    static var detailedViewState =
+        LiveActivityAttributes.ContentAdditionalState(
+            chart: chartData.map {
+                LiveActivityAttributes.ChartItem(value: Decimal($0.glucose), date: $0.date)
+            },
+            rotationDegrees: 0,
+            cob: 20,
+            iob: 1.5,
+            tdd: 43.21,
+            isOverrideActive: false,
+            overrideName: "Exercise",
+            overrideDate: Date().addingTimeInterval(-3600),
+            overrideDuration: 120,
+            overrideTarget: 150,
+            widgetItems: LiveActivityAttributes.LiveActivityItem.defaultItems
+        )
 
     // 0 is the widest digit. Use this to get an upper bound on text width.
 
@@ -107,7 +138,9 @@ private extension LiveActivityAttributes.ContentState {
             lowGlucose: 70,
             target: 100,
             glucoseColorScheme: "staticColor",
-            detailedViewState: nil,
+            useDetailedViewIOS: false,
+            useDetailedViewWatchOS: false,
+            detailedViewState: detailedViewState,
             isInitialState: false
         )
     }
@@ -123,7 +156,9 @@ private extension LiveActivityAttributes.ContentState {
             lowGlucose: 70,
             target: 100,
             glucoseColorScheme: "staticColor",
-            detailedViewState: nil,
+            useDetailedViewIOS: false,
+            useDetailedViewWatchOS: false,
+            detailedViewState: detailedViewState,
             isInitialState: false
         )
     }
@@ -139,7 +174,9 @@ private extension LiveActivityAttributes.ContentState {
             lowGlucose: 70,
             target: 100,
             glucoseColorScheme: "staticColor",
-            detailedViewState: nil,
+            useDetailedViewIOS: false,
+            useDetailedViewWatchOS: false,
+            detailedViewState: detailedViewState,
             isInitialState: false
         )
     }
@@ -156,7 +193,9 @@ private extension LiveActivityAttributes.ContentState {
             lowGlucose: 70,
             target: 100,
             glucoseColorScheme: "staticColor",
-            detailedViewState: nil,
+            useDetailedViewIOS: false,
+            useDetailedViewWatchOS: false,
+            detailedViewState: detailedViewState,
             isInitialState: false
         )
     }
@@ -172,7 +211,9 @@ private extension LiveActivityAttributes.ContentState {
             lowGlucose: 70,
             target: 100,
             glucoseColorScheme: "staticColor",
-            detailedViewState: nil,
+            useDetailedViewIOS: false,
+            useDetailedViewWatchOS: false,
+            detailedViewState: detailedViewState,
             isInitialState: false
         )
     }
@@ -188,12 +229,15 @@ private extension LiveActivityAttributes.ContentState {
             lowGlucose: 70,
             target: 100,
             glucoseColorScheme: "staticColor",
-            detailedViewState: nil,
+            useDetailedViewIOS: false,
+            useDetailedViewWatchOS: false,
+            detailedViewState: detailedViewState,
             isInitialState: false
         )
     }
 
-    static var testWideDetailed: LiveActivityAttributes.ContentState {
+    static var testWideDetailed: LiveActivityAttributes.ContentState
+    {
         LiveActivityAttributes.ContentState(
             unit: "mg/dL",
             bg: "00.0",
@@ -204,12 +248,16 @@ private extension LiveActivityAttributes.ContentState {
             lowGlucose: 70,
             target: 100,
             glucoseColorScheme: "staticColor",
+            useDetailedViewIOS: true,
+            useDetailedViewWatchOS: true,
             detailedViewState: detailedViewState,
             isInitialState: false
         )
     }
 
-    static var testVeryWideDetailed: LiveActivityAttributes.ContentState {
+    static var testVeryWideDetailed:
+        LiveActivityAttributes.ContentState
+    {
         LiveActivityAttributes.ContentState(
             unit: "mg/dL",
             bg: "00.0",
@@ -220,12 +268,16 @@ private extension LiveActivityAttributes.ContentState {
             lowGlucose: 70,
             target: 100,
             glucoseColorScheme: "staticColor",
+            useDetailedViewIOS: true,
+            useDetailedViewWatchOS: true,
             detailedViewState: detailedViewState,
             isInitialState: false
         )
     }
 
-    static var testSuperWideDetailed: LiveActivityAttributes.ContentState {
+    static var testSuperWideDetailed:
+        LiveActivityAttributes.ContentState
+    {
         LiveActivityAttributes.ContentState(
             unit: "mg/dL",
             bg: "00.0",
@@ -236,13 +288,17 @@ private extension LiveActivityAttributes.ContentState {
             lowGlucose: 70,
             target: 100,
             glucoseColorScheme: "staticColor",
+            useDetailedViewIOS: true,
+            useDetailedViewWatchOS: true,
             detailedViewState: detailedViewState,
             isInitialState: false
         )
     }
 
     // 2 characters for BG, 1 character for change is the minimum that will be shown
-    static var testNarrowDetailed: LiveActivityAttributes.ContentState {
+    static var testNarrowDetailed:
+        LiveActivityAttributes.ContentState
+    {
         LiveActivityAttributes.ContentState(
             unit: "mg/dL",
             bg: "00",
@@ -253,12 +309,16 @@ private extension LiveActivityAttributes.ContentState {
             lowGlucose: 70,
             target: 100,
             glucoseColorScheme: "staticColor",
+            useDetailedViewIOS: true,
+            useDetailedViewWatchOS: true,
             detailedViewState: detailedViewState,
             isInitialState: false
         )
     }
 
-    static var testMediumDetailed: LiveActivityAttributes.ContentState {
+    static var testMediumDetailed:
+        LiveActivityAttributes.ContentState
+    {
         LiveActivityAttributes.ContentState(
             unit: "mg/dL",
             bg: "000",
@@ -269,12 +329,16 @@ private extension LiveActivityAttributes.ContentState {
             lowGlucose: 70,
             target: 100,
             glucoseColorScheme: "staticColor",
+            useDetailedViewIOS: true,
+            useDetailedViewWatchOS: true,
             detailedViewState: detailedViewState,
             isInitialState: false
         )
     }
 
-    static var testExpiredDetailed: LiveActivityAttributes.ContentState {
+    static var testExpiredDetailed:
+        LiveActivityAttributes.ContentState
+    {
         LiveActivityAttributes.ContentState(
             unit: "mg/dL",
             bg: "--",
@@ -285,6 +349,8 @@ private extension LiveActivityAttributes.ContentState {
             lowGlucose: 70,
             target: 100,
             glucoseColorScheme: "staticColor",
+            useDetailedViewIOS: true,
+            useDetailedViewWatchOS: true,
             detailedViewState: detailedViewState,
             isInitialState: false
         )

+ 25 - 18
LiveActivity/Views/LiveActivityChartView.swift

@@ -11,6 +11,7 @@ import WidgetKit
 
 struct LiveActivityChartView: View {
     @Environment(\.colorScheme) var colorScheme
+    @Environment(\.isWatchOS) var isWatchOS
 
     var context: ActivityViewContext<LiveActivityAttributes>
     var additionalState: LiveActivityAttributes.ContentAdditionalState
@@ -19,9 +20,11 @@ struct LiveActivityChartView: View {
         let state = context.state
         let isMgdL: Bool = state.unit == "mg/dL"
 
+        let maxThreshhold: Decimal = isWatchOS ? 220 : 300
+
         // Determine scale
-        let minValue = min(additionalState.chart.min() ?? 39, 39) as Decimal
-        let maxValue = max(additionalState.chart.max() ?? 300, 300) as Decimal
+        let minValue = min(additionalState.chart.min(by: { $0.value < $1.value })?.value ?? 39, 39)
+        let maxValue = max(additionalState.chart.max(by: { $0.value < $1.value })?.value ?? maxThreshhold, maxThreshhold)
 
         let yAxisRuleMarkMin = isMgdL ? state.lowGlucose : state.lowGlucose
             .asMmolL
@@ -34,8 +37,9 @@ struct LiveActivityChartView: View {
         let calendar = Calendar.current
         let now = Date()
 
-        let startDate = calendar.date(byAdding: .hour, value: -6, to: now) ?? now
-        let endDate = isOverrideActive ? (calendar.date(byAdding: .hour, value: 2, to: now) ?? now) : now
+        let startDate = calendar.date(byAdding: .hour, value: isWatchOS ? -3 : -6, to: now) ?? now
+        let endDate = isOverrideActive ? (calendar.date(byAdding: .hour, value: 2, to: now) ?? now) :
+            (calendar.date(byAdding: .minute, value: isWatchOS ? 5 : 0, to: now) ?? now)
 
         // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
         let hardCodedLow = isMgdL ? Decimal(55) : 55.asMmolL
@@ -104,13 +108,13 @@ struct LiveActivityChartView: View {
     }
 
     private func drawActiveOverrides() -> some ChartContent {
-        let start: Date = context.state.detailedViewState?.overrideDate ?? .distantPast
+        let start: Date = context.state.detailedViewState.overrideDate
 
-        let duration = context.state.detailedViewState?.overrideDuration ?? 0
+        let duration = context.state.detailedViewState.overrideDuration
         let durationAsTimeInterval = TimeInterval((duration as NSDecimalNumber).doubleValue * 60) // return seconds
 
         let end: Date = start.addingTimeInterval(durationAsTimeInterval)
-        let target = context.state.detailedViewState?.overrideTarget ?? 0
+        let target = context.state.detailedViewState.overrideTarget
 
         return RuleMark(
             xStart: .value("Start", start, unit: .second),
@@ -122,19 +126,22 @@ struct LiveActivityChartView: View {
     }
 
     private func drawChart(yAxisRuleMarkMin _: Decimal, yAxisRuleMarkMax _: Decimal) -> some ChartContent {
-        ForEach(additionalState.chart.indices, id: \.self) { index in
-            let isMgdL = context.state.unit == "mg/dL"
-            let currentValue = additionalState.chart[index]
-            let displayValue = isMgdL ? currentValue : currentValue.asMmolL
-            let chartDate = additionalState.chartDate[index] ?? Date()
+        // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
+        let hardCodedLow = Decimal(55)
+        let hardCodedHigh = Decimal(220)
+        let hasStaticColorScheme = context.state.glucoseColorScheme == "staticColor"
+        let isMgdL = context.state.unit == "mg/dL"
+
+        let threeHours = TimeInterval(10800)
+        let chartData = isWatchOS ? additionalState.chart
+            .filter { abs($0.date.timeIntervalSinceNow) < threeHours } : additionalState
+            .chart
 
-            // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
-            let hardCodedLow = Decimal(55)
-            let hardCodedHigh = Decimal(220)
-            let hasStaticColorScheme = context.state.glucoseColorScheme == "staticColor"
+        return ForEach(chartData, id: \.self) { item in
+            let displayValue = isMgdL ? item.value : item.value.asMmolL
 
             let pointMarkColor = Color.getDynamicGlucoseColor(
-                glucoseValue: currentValue,
+                glucoseValue: item.value,
                 highGlucoseColorValue: !hasStaticColorScheme ? hardCodedHigh : context.state.highGlucose,
                 lowGlucoseColorValue: !hasStaticColorScheme ? hardCodedLow : context.state.lowGlucose,
                 targetGlucose: context.state.target,
@@ -142,7 +149,7 @@ struct LiveActivityChartView: View {
             )
 
             let pointMark = PointMark(
-                x: .value("Time", chartDate),
+                x: .value("Time", item.date),
                 y: .value("Value", displayValue)
             )
             .symbolSize(16)

+ 4 - 1
LiveActivity/Views/LiveActivityGlucoseDeltaLabelView.swift

@@ -15,7 +15,10 @@ struct LiveActivityGlucoseDeltaLabelView: View {
     var body: some View {
         if !context.state.change.isEmpty {
             Text(context.state.change)
-                .foregroundStyle(context.state.glucoseColorScheme == "staticColor" ? .primary : glucoseColor)
+                .foregroundStyle(
+                    context.isStale ? .secondary : context.state
+                        .glucoseColorScheme == "staticColor" ? .primary : glucoseColor
+                )
                 .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
         } else {
             Text("--")

+ 62 - 44
LiveActivity/Views/LiveActivityView.swift

@@ -1,9 +1,3 @@
-//
-//  LiveActivityView.swift
-//  Trio
-//
-//  Created by Cengiz Deniz on 17.10.24.
-//
 import ActivityKit
 import Foundation
 import SwiftUI
@@ -11,6 +5,8 @@ import WidgetKit
 
 struct LiveActivityView: View {
     @Environment(\.colorScheme) var colorScheme
+    @Environment(\.isWatchOS) var isWatchOS
+
     var context: ActivityViewContext<LiveActivityAttributes>
 
     private var hasStaticColorScheme: Bool {
@@ -27,23 +23,49 @@ struct LiveActivityView: View {
 
         return Color.getDynamicGlucoseColor(
             glucoseValue: Decimal(string: state.bg) ?? 100,
-            highGlucoseColorValue: !hasStaticColorScheme ? hardCodedHigh : state.highGlucose,
-            lowGlucoseColorValue: !hasStaticColorScheme ? hardCodedLow : state.lowGlucose,
+            highGlucoseColorValue: !hasStaticColorScheme ? hardCodedHigh :
+                (isMgdL ? state.highGlucose : state.highGlucose.asMmolL),
+            lowGlucoseColorValue: !hasStaticColorScheme ? hardCodedLow : (isMgdL ? state.lowGlucose : state.lowGlucose.asMmolL),
             targetGlucose: isMgdL ? state.target : state.target.asMmolL,
             glucoseColorScheme: state.glucoseColorScheme
         )
     }
 
     var body: some View {
-        if let detailedViewState = context.state.detailedViewState {
+        if isWatchOS, context.state.useDetailedViewWatchOS {
             VStack {
-                LiveActivityChartView(context: context, additionalState: detailedViewState)
+                LiveActivityBGLabelWatchView(context: context, glucoseColor: glucoseColor)
+                LiveActivityChartView(context: context, additionalState: context.state.detailedViewState)
+                    .frame(maxWidth: UIScreen.main.bounds.width * 0.9)
+            }
+            .addLiveActivityModifiers(isWatchOS: true)
+
+        } else if isWatchOS {
+            HStack {
+                LiveActivityBGLabelLargeView(
+                    context: context,
+                    glucoseColor: glucoseColor
+                )
+                Spacer()
+                VStack {
+                    LiveActivityGlucoseDeltaLabelView(
+                        context: context,
+                        glucoseColor: .primary
+                    )
+                    LiveActivityUpdatedLabelView(context: context, isDetailedLayout: false)
+                }
+            }
+            .addLiveActivityModifiers(isWatchOS: true)
+
+        } else if context.state.useDetailedViewIOS {
+            VStack {
+                LiveActivityChartView(context: context, additionalState: context.state.detailedViewState)
                     .frame(maxWidth: UIScreen.main.bounds.width * 0.9)
                     .frame(height: 80)
                     .overlay(alignment: .topTrailing) {
-                        if detailedViewState.isOverrideActive {
+                        if context.state.detailedViewState.isOverrideActive {
                             HStack {
-                                Text("\(detailedViewState.overrideName)")
+                                Text("\(context.state.detailedViewState.overrideName)")
                                     .font(.footnote)
                                     .fontWeight(.bold)
                                     .foregroundStyle(.white)
@@ -57,12 +79,18 @@ struct LiveActivityView: View {
                     }
 
                 HStack {
-                    if detailedViewState.widgetItems.contains(where: { $0 != .empty }) {
-                        ForEach(Array(detailedViewState.widgetItems.enumerated()), id: \.element) { index, widgetItem in
+                    if context.state.detailedViewState.widgetItems.contains(where: { $0 != .empty }) {
+                        ForEach(
+                            Array(context.state.detailedViewState.widgetItems.enumerated()),
+                            id: \.element
+                        ) { index, widgetItem in
                             switch widgetItem {
                             case .currentGlucose:
                                 VStack {
-                                    LiveActivityBGLabelView(context: context, additionalState: detailedViewState)
+                                    LiveActivityBGLabelView(
+                                        context: context,
+                                        additionalState: context.state.detailedViewState
+                                    )
 
                                     HStack {
                                         LiveActivityGlucoseDeltaLabelView(
@@ -77,25 +105,27 @@ struct LiveActivityView: View {
                             case .currentGlucoseLarge:
                                 LiveActivityBGLabelLargeView(
                                     context: context,
-                                    additionalState: detailedViewState,
                                     glucoseColor: glucoseColor
                                 )
                             case .iob:
-                                LiveActivityIOBLabelView(context: context, additionalState: detailedViewState)
+                                LiveActivityIOBLabelView(context: context, additionalState: context.state.detailedViewState)
                             case .cob:
-                                LiveActivityCOBLabelView(context: context, additionalState: detailedViewState)
+                                LiveActivityCOBLabelView(context: context, additionalState: context.state.detailedViewState)
                             case .updatedLabel:
                                 LiveActivityUpdatedLabelView(context: context, isDetailedLayout: true)
                             case .totalDailyDose:
-                                LiveActivityTotalDailyDoseView(context: context, additionalState: detailedViewState)
+                                LiveActivityTotalDailyDoseView(
+                                    context: context,
+                                    additionalState: context.state.detailedViewState
+                                )
                             case .empty:
                                 Text("").frame(width: 50, height: 50)
                             }
 
                             /// Check if the next item is also non-empty to determine if a divider should be shown
-                            if index < detailedViewState.widgetItems.count - 1 {
-                                let currentItem = detailedViewState.widgetItems[index]
-                                let nextItem = detailedViewState.widgetItems[index + 1]
+                            if index < context.state.detailedViewState.widgetItems.count - 1 {
+                                let currentItem = context.state.detailedViewState.widgetItems[index]
+                                let nextItem = context.state.detailedViewState.widgetItems[index + 1]
 
                                 if currentItem != .empty, nextItem != .empty {
                                     Divider()
@@ -108,15 +138,7 @@ struct LiveActivityView: View {
                     }
                 }
             }
-            .privacySensitive()
-            .padding(.all, 14)
-            .foregroundStyle(Color.primary)
-            // 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 variable does work here, but BackgroundStyle gives us this functionality for free
-            .foregroundStyle(Color.primary)
-            .background(BackgroundStyle.background.opacity(0.4))
-            .activityBackgroundTint(Color.clear)
+            .addLiveActivityModifiers(isWatchOS: false)
         } else {
             Group {
                 if context.state.isInitialState {
@@ -130,21 +152,14 @@ struct LiveActivityView: View {
                                 context: context,
                                 glucoseColor: hasStaticColorScheme ? .primary : glucoseColor
                             ).font(.title3)
-                            LiveActivityUpdatedLabelView(context: context, isDetailedLayout: false).font(.caption)
+                            LiveActivityUpdatedLabelView(context: context, isDetailedLayout: false)
+                                .font(.caption)
                                 .foregroundStyle(.primary.opacity(0.7))
                         }
                     }
                 }
             }
-            .privacySensitive()
-            .padding(.all, 15)
-            .foregroundStyle(Color.primary)
-            /// 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 variable does work here, but BackgroundStyle gives us this functionality for free
-            .foregroundStyle(Color.primary)
-            .background(BackgroundStyle.background.opacity(0.4))
-            .activityBackgroundTint(Color.clear)
+            .addLiveActivityModifiers(isWatchOS: false)
         }
     }
 }
@@ -176,8 +191,9 @@ struct LiveActivityExpandedBottomView: View {
     var body: some View {
         if context.state.isInitialState {
             Text("Live Activity Expired. Open Trio to Refresh").minimumScaleFactor(0.01)
-        } else if let detailedViewState = context.state.detailedViewState {
-            LiveActivityChartView(context: context, additionalState: detailedViewState)
+        } else if context.state.useDetailedViewIOS {
+            LiveActivityChartView(context: context, additionalState: context.state.detailedViewState)
+                .addIsWatchOS()
         }
     }
 }
@@ -186,8 +202,10 @@ struct LiveActivityExpandedCenterView: View {
     var context: ActivityViewContext<LiveActivityAttributes>
 
     var body: some View {
-        LiveActivityUpdatedLabelView(context: context, isDetailedLayout: false).font(Font.caption)
+        LiveActivityUpdatedLabelView(context: context, isDetailedLayout: false)
+            .font(Font.caption)
             .foregroundStyle(Color.secondary)
+            .addIsWatchOS()
     }
 }
 

+ 10 - 14
LiveActivity/Views/WidgetItems/LiveActivityBGLabelLargeView.swift

@@ -3,29 +3,25 @@ import SwiftUI
 import WidgetKit
 
 struct LiveActivityBGLabelLargeView: View {
+    @Environment(\.isWatchOS) var isWatchOS
+
     var context: ActivityViewContext<LiveActivityAttributes>
-    var additionalState: LiveActivityAttributes.ContentAdditionalState
     var glucoseColor: Color
 
     var body: some View {
         HStack(alignment: .center) {
-            if let trendArrow = context.state.direction {
-                Text(context.state.bg)
-                    .fontWeight(.bold)
-                    .font(.title)
-                    .foregroundStyle(context.isStale ? .secondary : glucoseColor)
-                    .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+            Text(context.state.bg)
+                .fontWeight(.bold)
+                .font(!isWatchOS ? .title : .title3)
+                .foregroundStyle(context.isStale ? .secondary : glucoseColor)
+                .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
 
+            if let trendArrow = context.state.direction {
                 Text(trendArrow)
                     .foregroundStyle(context.isStale ? .secondary : glucoseColor)
                     .fontWeight(.bold)
-                    .font(.headline)
-            } else {
-                Text(context.state.bg)
-                    .fontWeight(.bold)
-                    .font(.title)
-                    .foregroundStyle(context.isStale ? .secondary : glucoseColor)
-                    .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+                    .font(!isWatchOS ? .headline : .subheadline)
+                    .padding(.leading, !isWatchOS ? 0 : -5)
             }
         }
     }

+ 0 - 6
LiveActivity/Views/WidgetItems/LiveActivityBGLabelView.swift

@@ -1,9 +1,3 @@
-//
-//  LiveActivityBGLabelView.swift
-//  Trio
-//
-//  Created by Cengiz Deniz on 17.10.24.
-//
 import Foundation
 import SwiftUI
 import WidgetKit

+ 44 - 0
LiveActivity/Views/WidgetItems/LiveActivityBGLabelWatchView.swift

@@ -0,0 +1,44 @@
+import SwiftUI
+import WidgetKit
+
+struct LiveActivityBGLabelWatchView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+    var glucoseColor: Color
+
+    private var dateFormatter: DateFormatter {
+        let formatter = DateFormatter()
+        formatter.dateStyle = .none
+        formatter.timeStyle = .short
+        return formatter
+    }
+
+    var body: some View {
+        HStack {
+            Text(context.state.bg)
+                .font(.callout)
+                .fontWeight(.bold)
+                .foregroundStyle(context.isStale ? .secondary : glucoseColor)
+                .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+
+            if let trendArrow = context.state.direction {
+                Text(trendArrow)
+                    .font(.callout)
+                    .fontWeight(.bold)
+                    .foregroundStyle(context.isStale ? .secondary : glucoseColor)
+                    .padding(.leading, -5)
+            }
+
+            Text(context.state.change)
+                .font(.callout)
+                .foregroundStyle(context.isStale ? .secondary : glucoseColor)
+                .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+
+            Spacer()
+
+            Text("\((context.state.date != nil) ? dateFormatter.string(from: context.state.date!) : "--")")
+                .font(.callout)
+                .foregroundStyle(context.isStale ? .red.opacity(0.6) : .primary)
+                .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+        }
+    }
+}

+ 5 - 10
LiveActivity/Views/WidgetItems/LiveActivityCOBLabelView.swift

@@ -1,9 +1,3 @@
-//
-//  LiveActivityCOBLabelView.swift
-//  Trio
-//
-//  Created by Cengiz Deniz on 17.10.24.
-//
 import Foundation
 import SwiftUI
 import WidgetKit
@@ -15,9 +9,8 @@ struct LiveActivityCOBLabelView: View {
     var body: some View {
         VStack(spacing: 2) {
             HStack {
-                Text(
-                    "\(additionalState.cob)"
-                ).fontWeight(.bold)
+                Text("\(additionalState.cob)" as String)
+                    .fontWeight(.bold)
                     .font(.title3)
                     .foregroundStyle(context.isStale ? .secondary : .primary)
                     .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
@@ -27,7 +20,9 @@ struct LiveActivityCOBLabelView: View {
                     .foregroundStyle(context.isStale ? .secondary : .primary)
                     .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
             }
-            Text("COB").font(.subheadline).foregroundStyle(.primary)
+            Text("COB")
+                .font(.subheadline)
+                .foregroundStyle(.primary)
         }
     }
 }

+ 8 - 14
LiveActivity/Views/WidgetItems/LiveActivityIOBLabelView.swift

@@ -1,9 +1,3 @@
-//
-//  LiveActivityWidgetItems.swift
-//  LiveActivityExtension
-//
-//  Created by Cengiz Deniz on 17.10.24.
-//
 import Foundation
 import SwiftUI
 import WidgetKit
@@ -22,20 +16,20 @@ struct LiveActivityIOBLabelView: View {
     var body: some View {
         VStack(spacing: 2) {
             HStack {
-                Text(
-                    bolusFormatter.string(from: additionalState.iob as NSNumber) ?? "--"
-                )
-                .fontWeight(.bold)
-                .font(.title3)
-                .foregroundStyle(context.isStale ? .secondary : .primary)
-                .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+                Text(bolusFormatter.string(from: additionalState.iob as NSNumber) ?? "--")
+                    .fontWeight(.bold)
+                    .font(.title3)
+                    .foregroundStyle(context.isStale ? .secondary : .primary)
+                    .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
 
                 Text(String(localized: "U", comment: "Insulin unit"))
                     .font(.headline).fontWeight(.bold)
                     .foregroundStyle(context.isStale ? .secondary : .primary)
                     .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
             }
-            Text("IOB").font(.subheadline).foregroundStyle(.primary)
+            Text("IOB")
+                .font(.subheadline)
+                .foregroundStyle(.primary)
         }
     }
 }

+ 8 - 8
LiveActivity/Views/WidgetItems/LiveActivityTotalDailyDoseView.swift

@@ -16,20 +16,20 @@ struct LiveActivityTotalDailyDoseView: View {
     var body: some View {
         VStack(spacing: 2) {
             HStack {
-                Text(
-                    bolusFormatter.string(from: additionalState.tdd as NSNumber) ?? "--"
-                )
-                .fontWeight(.bold)
-                .font(.title3)
-                .foregroundStyle(context.isStale ? .secondary : .primary)
-                .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+                Text(bolusFormatter.string(from: additionalState.tdd as NSNumber) ?? "--")
+                    .fontWeight(.bold)
+                    .font(.title3)
+                    .foregroundStyle(context.isStale ? .secondary : .primary)
+                    .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
 
                 Text(String(localized: "U", comment: "Insulin unit"))
                     .font(.headline).fontWeight(.bold)
                     .foregroundStyle(context.isStale ? .secondary : .primary)
                     .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
             }
-            Text("TDD").font(.subheadline).foregroundStyle(.primary)
+            Text("TDD")
+                .font(.subheadline)
+                .foregroundStyle(.primary)
         }
     }
 }

+ 16 - 9
LiveActivity/Views/WidgetItems/LiveActivityUpdatedLabelView.swift

@@ -1,14 +1,10 @@
-//
-//  LiveActivityUpdatedLabelView.swift
-//  Trio
-//
-//  Created by Cengiz Deniz on 17.10.24.
-//
 import Foundation
 import SwiftUI
 import WidgetKit
 
 struct LiveActivityUpdatedLabelView: View {
+    @Environment(\.isWatchOS) var isWatchOS
+
     var context: ActivityViewContext<LiveActivityAttributes>
     var isDetailedLayout: Bool
 
@@ -22,7 +18,14 @@ struct LiveActivityUpdatedLabelView: View {
     var body: some View {
         let dateText = Text("\((context.state.date != nil) ? dateFormatter.string(from: context.state.date!) : "--")")
 
-        if isDetailedLayout {
+        if isWatchOS {
+            dateText
+                .font(.subheadline)
+                .bold()
+                .foregroundStyle(context.isStale ? .red.opacity(0.6) : .secondary)
+                .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+
+        } else if isDetailedLayout {
             VStack {
                 dateText
                     .font(.title3)
@@ -30,11 +33,15 @@ struct LiveActivityUpdatedLabelView: View {
                     .foregroundStyle(context.isStale ? .red.opacity(0.6) : .primary)
                     .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
 
-                Text("Updated").font(.subheadline).foregroundStyle(.primary)
+                Text("Updated")
+                    .font(.subheadline)
+                    .foregroundStyle(.primary)
             }
         } else {
             HStack {
-                Text("Updated:").font(.subheadline).foregroundStyle(.secondary)
+                Text("Updated:")
+                    .font(.subheadline)
+                    .foregroundStyle(.secondary)
 
                 dateText
                     .font(.subheadline)

+ 1 - 1
MedtrumKit

@@ -1 +1 @@
-Subproject commit a85496e90d0a16ba8fb5dae7781dfbc3e5f66267
+Subproject commit d9bc8b5dd8fb304956a5624c31bc8a937ec8d4d6

+ 9 - 1
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -10034,7 +10034,6 @@
       }
     },
     "%lld h" : {
-      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -227542,6 +227541,9 @@
         }
       }
     },
+    "The Detailed Watch/Carplay Screen Widget offers users a glucose chart as well as the current glucose, delta and the timestamp of current reading." : {
+
+    },
     "The Duration of Insulin Action (DIA) defines how long your insulin continues to lower glucose readings after a dose." : {
       "localizations" : {
         "bg" : {
@@ -251132,6 +251134,9 @@
         }
       }
     },
+    "Trio's Simple Watch/Carplay Widget displays current glucose reading, trend arrow, delta and the timestamp of the current reading." : {
+
+    },
     "Try again in a moment, or configure your Therapy Settings manually instead." : {
       "localizations" : {
         "bg" : {
@@ -261568,6 +261573,9 @@
         }
       }
     },
+    "Watch/Carplay Widget Style" : {
+
+    },
     "We recommend reviewing them carefully — Trio will guide you step-by-step." : {
       "localizations" : {
         "bg" : {

+ 5 - 0
Trio/Sources/Models/TrioSettings.swift

@@ -68,6 +68,7 @@ struct TrioSettings: JSON, Equatable {
     var confirmBolus: Bool = false
     var useLiveActivity: Bool = false
     var lockScreenView: LockScreenView = .simple
+    var smartStackView: LockScreenView = .simple
     var bolusShortcut: BolusShortcutLimit = .notAllowed
     var timeInRangeType: TimeInRangeType = .timeInTightRange
 }
@@ -292,6 +293,10 @@ extension TrioSettings: Decodable {
             settings.lockScreenView = lockScreenView
         }
 
+        if let smartStackView = try? container.decode(LockScreenView.self, forKey: .smartStackView) {
+            settings.smartStackView = smartStackView
+        }
+
         if let bolusShortcut = try? container.decode(BolusShortcutLimit.self, forKey: .bolusShortcut) {
             settings.bolusShortcut = bolusShortcut
         }

+ 4 - 1
Trio/Sources/Modules/Home/View/Header/LoopStatusView.swift

@@ -129,7 +129,10 @@ struct LoopStatusView: View {
         .onAppear {
             lastDetermination = state.determinationsFromPersistence.first
         }
-        .presentationDetents([.height(sheetContentHeight)])
+        .presentationDetents([
+            sheetContentHeight > 0 ? .height(sheetContentHeight) : .fraction(0.9),
+            .large
+        ])
         .presentationDragIndicator(.visible)
         .onPreferenceChange(ContentSizeKey.self) { newSize in
             sheetContentHeight = newSize.height

+ 2 - 0
Trio/Sources/Modules/LiveActivitySettings/LiveActivitySettingsStateModel.swift

@@ -8,10 +8,12 @@ extension LiveActivitySettings {
         @Published var units: GlucoseUnits = .mgdL
         @Published var useLiveActivity = false
         @Published var lockScreenView: LockScreenView = .simple
+        @Published var smartStackView: LockScreenView = .simple
         override func subscribe() {
             units = settingsManager.settings.units
             subscribeSetting(\.useLiveActivity, on: $useLiveActivity) { useLiveActivity = $0 }
             subscribeSetting(\.lockScreenView, on: $lockScreenView) { lockScreenView = $0 }
+            subscribeSetting(\.smartStackView, on: $smartStackView) { smartStackView = $0 }
         }
     }
 }

+ 67 - 5
Trio/Sources/Modules/LiveActivitySettings/View/LiveActivitySettingsRootView.swift

@@ -7,7 +7,8 @@ extension LiveActivitySettings {
         let resolver: Resolver
         @StateObject var state = StateModel()
 
-        @State private var shouldDisplayHint: Bool = false
+        @State private var shouldDisplayHintLockScreen: Bool = false
+        @State private var shouldDisplayHintSmartStack: Bool = false
         @State var hintDetent = PresentationDetent.large
         @State var selectedVerboseHint: AnyView?
         @State var hintLabel: String?
@@ -48,7 +49,7 @@ extension LiveActivitySettings {
                     SettingInputSection(
                         decimalValue: $decimalPlaceholder,
                         booleanValue: $state.useLiveActivity,
-                        shouldDisplayHint: $shouldDisplayHint,
+                        shouldDisplayHint: $shouldDisplayHintLockScreen,
                         selectedVerboseHint: Binding(
                             get: { selectedVerboseHint },
                             set: {
@@ -128,7 +129,7 @@ extension LiveActivitySettings {
                                                         }.font(.footnote)
                                                     }
                                                 )
-                                            shouldDisplayHint.toggle()
+                                            shouldDisplayHintLockScreen.toggle()
                                         },
                                         label: {
                                             HStack {
@@ -151,6 +152,58 @@ extension LiveActivitySettings {
                                 }
                             }
                         }.listRowBackground(Color.chart)
+
+                        Section {
+                            VStack {
+                                Picker(
+                                    selection: $state.smartStackView,
+                                    label: Text("Watch/Carplay Widget Style")
+                                ) {
+                                    ForEach(LockScreenView.allCases) { selection in
+                                        Text(selection.displayName).tag(selection)
+                                    }
+                                }.padding(.top)
+
+                                HStack(alignment: .center) {
+                                    Text(
+                                        "Select simple or detailed style. See hint for more details."
+                                    )
+                                    .font(.footnote)
+                                    .foregroundColor(.secondary)
+                                    .lineLimit(nil)
+                                    Spacer()
+                                    Button(
+                                        action: {
+                                            hintLabel = String(localized: "Watch/Carplay Widget Style")
+                                            selectedVerboseHint =
+                                                AnyView(
+                                                    VStack(alignment: .leading, spacing: 10) {
+                                                        Text("Default: Simple").bold()
+                                                        VStack(alignment: .leading, spacing: 10) {
+                                                            Text("Simple:").bold()
+                                                            Text(
+                                                                "Trio's Simple Watch/Carplay Widget displays current glucose reading, trend arrow, delta and the timestamp of the current reading."
+                                                            )
+                                                        }
+                                                        VStack(alignment: .leading, spacing: 10) {
+                                                            Text("Detailed:").bold()
+                                                            Text(
+                                                                "The Detailed Watch/Carplay Screen Widget offers users a glucose chart as well as the current glucose, delta and the timestamp of current reading."
+                                                            )
+                                                        }
+                                                    }
+                                                )
+                                            shouldDisplayHintSmartStack.toggle()
+                                        },
+                                        label: {
+                                            HStack {
+                                                Image(systemName: "questionmark.circle")
+                                            }
+                                        }
+                                    ).buttonStyle(BorderlessButtonStyle())
+                                }.padding(.top)
+                            }.padding(.bottom)
+                        }.listRowBackground(Color.chart)
                     }
                 }
             }
@@ -158,10 +211,19 @@ extension LiveActivitySettings {
             .onReceive(resolver.resolve(LiveActivityManager.self)!.$systemEnabled, perform: {
                 self.systemLiveActivitySetting = $0
             })
-            .sheet(isPresented: $shouldDisplayHint) {
+            .sheet(isPresented: $shouldDisplayHintLockScreen) {
+                SettingInputHintView(
+                    hintDetent: $hintDetent,
+                    shouldDisplayHint: $shouldDisplayHintLockScreen,
+                    hintLabel: hintLabel ?? "",
+                    hintText: selectedVerboseHint ?? AnyView(EmptyView()),
+                    sheetTitle: String(localized: "Help", comment: "Help sheet title")
+                )
+            }
+            .sheet(isPresented: $shouldDisplayHintSmartStack) {
                 SettingInputHintView(
                     hintDetent: $hintDetent,
-                    shouldDisplayHint: $shouldDisplayHint,
+                    shouldDisplayHint: $shouldDisplayHintSmartStack,
                     hintLabel: hintLabel ?? "",
                     hintText: selectedVerboseHint ?? AnyView(EmptyView()),
                     sheetTitle: String(localized: "Help", comment: "Help sheet title")

+ 9 - 3
Trio/Sources/Services/LiveActivity/LiveActitiyAttributes.swift

@@ -24,15 +24,16 @@ struct LiveActivityAttributes: ActivityAttributes {
         let lowGlucose: Decimal
         let target: Decimal
         let glucoseColorScheme: String
-        let detailedViewState: ContentAdditionalState?
+        let useDetailedViewIOS: Bool
+        let useDetailedViewWatchOS: Bool
+        let detailedViewState: ContentAdditionalState
 
         /// true for the first state that is set on the activity
         let isInitialState: Bool
     }
 
     struct ContentAdditionalState: Codable, Hashable {
-        let chart: [Decimal]
-        let chartDate: [Date?]
+        let chart: [ChartItem]
         let rotationDegrees: Double
         let cob: Decimal
         let iob: Decimal
@@ -45,5 +46,10 @@ struct LiveActivityAttributes: ActivityAttributes {
         let widgetItems: [LiveActivityItem]
     }
 
+    struct ChartItem: Codable, Hashable {
+        let value: Decimal
+        let date: Date
+    }
+
     let startDate: Date
 }

+ 16 - 27
Trio/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift

@@ -68,7 +68,7 @@ extension LiveActivityAttributes.ContentState {
     ) {
         let glucose = bg.glucose
         let formattedBG = Self.formatGlucose(Int(glucose), units: units, forceSign: false)
-        var rotationDegrees: Double = 0.0
+        var rotationDegrees = 0.0
 
         switch bg.direction {
         case .doubleUp,
@@ -96,32 +96,19 @@ extension LiveActivityAttributes.ContentState {
         let trendString = bg.direction?.symbol as? String
         let change = Self.calculateChange(chart: chart, units: units)
 
-        let detailedState: LiveActivityAttributes.ContentAdditionalState?
-
-        switch settings.lockScreenView {
-        case .detailed:
-            let chartBG = chart.map { Decimal($0.glucose) }
-            let chartDate = chart.map(\.date)
-
-            /// glucose limits from UI settings, not from notifications settings
-            detailedState = LiveActivityAttributes.ContentAdditionalState(
-                chart: chartBG,
-                chartDate: chartDate,
-                rotationDegrees: rotationDegrees,
-                cob: Decimal(determination?.cob ?? 0),
-                iob: iob ?? 0 as Decimal,
-                tdd: determination?.tdd ?? 0 as Decimal,
-                isOverrideActive: override?.isActive ?? false,
-                overrideName: override?.overrideName ?? "Override",
-                overrideDate: override?.date ?? Date(),
-                overrideDuration: override?.duration ?? 0,
-                overrideTarget: override?.target ?? 0,
-                widgetItems: widgetItems ?? [] // set empty array here to silence compiler; this can never be nil
-            )
-
-        case .simple:
-            detailedState = nil
-        }
+        let detailedState = LiveActivityAttributes.ContentAdditionalState(
+            chart: chart.map { LiveActivityAttributes.ChartItem(value: Decimal($0.glucose), date: $0.date) },
+            rotationDegrees: rotationDegrees,
+            cob: Decimal(determination?.cob ?? 0),
+            iob: iob ?? 0 as Decimal,
+            tdd: determination?.tdd ?? 0 as Decimal,
+            isOverrideActive: override?.isActive ?? false,
+            overrideName: override?.overrideName ?? "Override",
+            overrideDate: override?.date ?? Date(),
+            overrideDuration: override?.duration ?? 0,
+            overrideTarget: override?.target ?? 0,
+            widgetItems: widgetItems ?? [] // set empty array here to silence compiler; this can never be nil
+        )
 
         self.init(
             unit: settings.units.rawValue,
@@ -133,6 +120,8 @@ extension LiveActivityAttributes.ContentState {
             lowGlucose: settings.low,
             target: determination?.target ?? 100 as Decimal,
             glucoseColorScheme: settings.glucoseColorScheme.rawValue,
+            useDetailedViewIOS: settings.lockScreenView == .detailed,
+            useDetailedViewWatchOS: settings.smartStackView == .detailed,
             detailedViewState: detailedState,
             isInitialState: false
         )

+ 1 - 1
Trio/Sources/Services/LiveActivity/LiveActivityAttributes.swift

@@ -2,7 +2,7 @@ import ActivityKit
 import Foundation
 
 struct LiveActivityAttributes: ActivityAttributes {
-    public struct ContentState: Codable, Hashable {
+    struct ContentState: Codable, Hashable {
         let bg: String
         let direction: String?
         let change: String

+ 21 - 7
Trio/Sources/Services/LiveActivity/LiveActivityManager.swift

@@ -10,7 +10,8 @@ import UIKit
 
     /// Determines if the current activity needs to be recreated.
     ///
-    /// - Returns: `true` if the activity is dismissed, ended, stale, or has been active for more than 60 minutes; otherwise, `false`.
+    /// - Returns: `true` if the activity is dismissed, ended, stale, or has been active for more than 60 minutes; otherwise,
+    /// `false`.
     func needsRecreation() -> Bool {
         switch activity.activityState {
         case .dismissed,
@@ -47,8 +48,7 @@ final class LiveActivityData: ObservableObject {
 ///
 /// Additionally, it supports a restart functionality (via `restartActivityFromLiveActivityIntent()`)
 /// via iOS shortcuts, similar to other iOS apps like xDrip4iOS or Sweet Dreams.
-@available(iOS 16.2, *)
-final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver {
+@available(iOS 16.2, *) final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver {
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var storage: FileStorage!
@@ -288,7 +288,21 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
                             lowGlucose: settings.low,
                             target: data.determination?.target ?? 100 as Decimal,
                             glucoseColorScheme: settings.glucoseColorScheme.rawValue,
-                            detailedViewState: nil,
+                            useDetailedViewIOS: false,
+                            useDetailedViewWatchOS: false,
+                            detailedViewState: LiveActivityAttributes.ContentAdditionalState(
+                                chart: [],
+                                rotationDegrees: 0,
+                                cob: 0,
+                                iob: 0,
+                                tdd: 0,
+                                isOverrideActive: false,
+                                overrideName: "",
+                                overrideDate: Date.now,
+                                overrideDuration: 0,
+                                overrideTarget: 0,
+                                widgetItems: []
+                            ),
                             isInitialState: true
                         ),
                     staleDate: Date.now.addingTimeInterval(60)
@@ -340,7 +354,8 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
 
     /// Restarts the live activity from a Live Activity Intent.
     ///
-    /// This method mimics xdrip's `restartActivityFromLiveActivityIntent()` behavior by verifying that a valid content state exists,
+    /// This method mimics xdrip's `restartActivityFromLiveActivityIntent()` behavior by verifying that a valid content state
+    /// exists,
     /// ending the current live activity, and starting a new one using the current state.
     @MainActor func restartActivityFromLiveActivityIntent() async {
         await endActivity()
@@ -362,8 +377,7 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
     }
 }
 
-@available(iOS 16.2, *)
-extension LiveActivityManager {
+@available(iOS 16.2, *) extension LiveActivityManager {
     @MainActor func pushCurrentContent() async {
         guard let glucose = data.glucoseFromPersistence, let bg = glucose.first else {
             debug(.default, "[LiveActivityManager] pushCurrentContent: no current glucose data available")