소스 검색

Add temp target display to live activity lock screen widget

When a temp target is active, an orange badge showing the temp target
name appears in the top-trailing corner of the chart overlay — mirroring
the existing purple override badge. Both badges stack vertically if an
override and a temp target are simultaneously active.

Pipeline mirrors the override pattern end-to-end:
TempTargetData struct → fetchAndMapTempTarget() in DataManager →
@Published tempTarget in LiveActivityData → TempTargetStored Core Data
observer in LiveActivityManager → five new fields in ContentAdditionalState
→ orange badge in LiveActivityView.
t1dude 3 주 전
부모
커밋
f22bb763fa

+ 20 - 0
LiveActivity/Assets.xcassets/LoopGreen.colorset/Contents.json

@@ -0,0 +1,20 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0.592",
+          "green" : "0.812",
+          "red" : "0.435"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 5 - 0
LiveActivity/LiveActivity.swift

@@ -121,6 +121,11 @@ private extension LiveActivityAttributes.ContentState {
             overrideDate: Date().addingTimeInterval(-3600),
             overrideDuration: 120,
             overrideTarget: 150,
+            isTempTargetActive: false,
+            tempTargetName: "Temp Target",
+            tempTargetDate: Date().addingTimeInterval(-1800),
+            tempTargetDuration: 60,
+            tempTargetTarget: 120,
             widgetItems: LiveActivityAttributes.LiveActivityItem.defaultItems
         )
 

+ 24 - 1
LiveActivity/Views/LiveActivityChartView.swift

@@ -33,12 +33,13 @@ struct LiveActivityChartView: View {
         let target = isMgdL ? state.target : state.target.asMmolL
 
         let isOverrideActive = additionalState.isOverrideActive == true
+        let isTempTargetActive = additionalState.isTempTargetActive == true
 
         let calendar = Calendar.current
         let now = Date()
 
         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) :
+        let endDate = (isOverrideActive || isTempTargetActive) ? (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
@@ -79,6 +80,10 @@ struct LiveActivityChartView: View {
                 drawActiveOverrides()
             }
 
+            if isTempTargetActive {
+                drawActiveTempTarget()
+            }
+
             drawChart(yAxisRuleMarkMin: yAxisRuleMarkMin, yAxisRuleMarkMax: yAxisRuleMarkMax)
         }
         .chartYAxis {
@@ -125,6 +130,24 @@ struct LiveActivityChartView: View {
         .lineStyle(.init(lineWidth: 8))
     }
 
+    private func drawActiveTempTarget() -> some ChartContent {
+        let start: Date = context.state.detailedViewState.tempTargetDate
+
+        let duration = context.state.detailedViewState.tempTargetDuration
+        let durationAsTimeInterval = TimeInterval((duration as NSDecimalNumber).doubleValue * 60) // return seconds
+
+        let end: Date = start.addingTimeInterval(durationAsTimeInterval)
+        let target = context.state.detailedViewState.tempTargetTarget
+
+        return RuleMark(
+            xStart: .value("Start", start, unit: .second),
+            xEnd: .value("End", end, unit: .second),
+            y: .value("Value", target)
+        )
+        .foregroundStyle(Color("LoopGreen").opacity(0.6))
+        .lineStyle(.init(lineWidth: 8))
+    }
+
     private func drawChart(yAxisRuleMarkMin _: Decimal, yAxisRuleMarkMax _: Decimal) -> some ChartContent {
         // 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)

+ 18 - 7
LiveActivity/Views/LiveActivityView.swift

@@ -63,17 +63,28 @@ struct LiveActivityView: View {
                     .frame(maxWidth: UIScreen.main.bounds.width * 0.9)
                     .frame(height: 80)
                     .overlay(alignment: .topTrailing) {
-                        if context.state.detailedViewState.isOverrideActive {
-                            HStack {
-                                Text("\(context.state.detailedViewState.overrideName)")
+                        HStack(spacing: 4) {
+                            if context.state.detailedViewState.isOverrideActive {
+                                Text(context.state.detailedViewState.overrideName)
                                     .font(.footnote)
                                     .fontWeight(.bold)
                                     .foregroundStyle(.white)
+                                    .padding(6)
+                                    .background {
+                                        RoundedRectangle(cornerRadius: 10)
+                                            .fill(Color.purple.opacity(colorScheme == .dark ? 0.6 : 0.8))
+                                    }
                             }
-                            .padding(6)
-                            .background {
-                                RoundedRectangle(cornerRadius: 10)
-                                    .fill(Color.purple.opacity(colorScheme == .dark ? 0.6 : 0.8))
+                            if context.state.detailedViewState.isTempTargetActive {
+                                Text(context.state.detailedViewState.tempTargetName)
+                                    .font(.footnote)
+                                    .fontWeight(.bold)
+                                    .foregroundStyle(.white)
+                                    .padding(6)
+                                    .background {
+                                        RoundedRectangle(cornerRadius: 10)
+                                            .fill(Color("LoopGreen").opacity(colorScheme == .dark ? 0.6 : 0.8))
+                                    }
                             }
                         }
                     }

+ 4 - 0
Trio.xcodeproj/project.pbxproj

@@ -435,6 +435,7 @@
 		BDB3C1192C03DD1000CEEAA1 /* UserDefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */; };
 		BDB899882C564509006F3298 /* ForecastChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899872C564509006F3298 /* ForecastChart.swift */; };
 		BDB8998A2C565D0C006F3298 /* CarbsGlucose+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */; };
+		BB5227A51D9D4377A1A70BA6 /* TempTargetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8F3B0E159844FD8746DFC0 /* TempTargetData.swift */; };
 		BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBAACF92C2D439700370AAE /* OverrideData.swift */; };
 		BDC2EA452C3043B000E5BBD0 /* OverrideStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */; };
 		BDC2EA472C3045AD00E5BBD0 /* Override.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA462C3045AD00E5BBD0 /* Override.swift */; };
@@ -1297,6 +1298,7 @@
 		BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtension.swift; sourceTree = "<group>"; };
 		BDB899872C564509006F3298 /* ForecastChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastChart.swift; sourceTree = "<group>"; };
 		BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbsGlucose+helper.swift"; sourceTree = "<group>"; };
+		4B8F3B0E159844FD8746DFC0 /* TempTargetData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetData.swift; sourceTree = "<group>"; };
 		BDBAACF92C2D439700370AAE /* OverrideData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideData.swift; sourceTree = "<group>"; };
 		BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStorage.swift; sourceTree = "<group>"; };
 		BDC2EA462C3045AD00E5BBD0 /* Override.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Override.swift; sourceTree = "<group>"; };
@@ -3199,6 +3201,7 @@
 				BDF34F842C10C62E00D51995 /* GlucoseData.swift */,
 				BDF34F942C10D27300D51995 /* DeterminationData.swift */,
 				BDBAACF92C2D439700370AAE /* OverrideData.swift */,
+				4B8F3B0E159844FD8746DFC0 /* TempTargetData.swift */,
 			);
 			path = Data;
 			sourceTree = "<group>";
@@ -4815,6 +4818,7 @@
 				23888883D4EA091C88480FF2 /* TreatmentsProvider.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
 				715120D22D3C2BB4005D9FB6 /* GlucoseNotificationsOption.swift in Sources */,
+				BB5227A51D9D4377A1A70BA6 /* TempTargetData.swift in Sources */,
 				BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */,
 				DD9ECB682CA99F4500AA7C45 /* TrioRemoteControl.swift in Sources */,
 				38569353270B5E350002C50D /* CGMRootView.swift in Sources */,

+ 28 - 0
Trio/Sources/Services/LiveActivity/Data/DataManager.swift

@@ -67,6 +67,34 @@ extension LiveActivityManager {
         }
     }
 
+    func fetchAndMapTempTarget() async throws -> TempTargetData? {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TempTargetStored.self,
+            onContext: context,
+            predicate: NSPredicate.predicateForOneDayAgo,
+            key: "date",
+            ascending: false,
+            fetchLimit: 1,
+            propertiesToFetch: ["enabled", "name", "target", "date", "duration"]
+        )
+
+        return try await context.perform {
+            guard let tempTargetResults = results as? [[String: Any]] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
+
+            return tempTargetResults.first.map {
+                TempTargetData(
+                    isActive: $0["enabled"] as? Bool ?? false,
+                    tempTargetName: $0["name"] as? String ?? "Temp Target",
+                    date: $0["date"] as? Date ?? Date(),
+                    duration: $0["duration"] as? Decimal ?? 0,
+                    target: $0["target"] as? Decimal ?? 0
+                )
+            }
+        }
+    }
+
     func fetchAndMapOverride() async throws -> OverrideData? {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,

+ 9 - 0
Trio/Sources/Services/LiveActivity/Data/TempTargetData.swift

@@ -0,0 +1,9 @@
+import Foundation
+
+struct TempTargetData {
+    let isActive: Bool
+    let tempTargetName: String
+    let date: Date
+    let duration: Decimal
+    let target: Decimal
+}

+ 5 - 0
Trio/Sources/Services/LiveActivity/LiveActitiyAttributes.swift

@@ -43,6 +43,11 @@ struct LiveActivityAttributes: ActivityAttributes {
         let overrideDate: Date
         let overrideDuration: Decimal
         let overrideTarget: Decimal
+        let isTempTargetActive: Bool
+        let tempTargetName: String
+        let tempTargetDate: Date
+        let tempTargetDuration: Decimal
+        let tempTargetTarget: Decimal
         let widgetItems: [LiveActivityItem]
     }
 

+ 6 - 0
Trio/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift

@@ -69,6 +69,7 @@ extension LiveActivityAttributes.ContentState {
         determination: DeterminationData?,
         iob: Decimal?,
         override: OverrideData?,
+        tempTarget: TempTargetData?,
         widgetItems: [LiveActivityAttributes.LiveActivityItem]?
     ) {
         let glucose = bg.glucose
@@ -112,6 +113,11 @@ extension LiveActivityAttributes.ContentState {
             overrideDate: override?.date ?? Date(),
             overrideDuration: override?.duration ?? 0,
             overrideTarget: override?.target ?? 0,
+            isTempTargetActive: tempTarget?.isActive ?? false,
+            tempTargetName: tempTarget?.tempTargetName ?? "Temp Target",
+            tempTargetDate: tempTarget?.date ?? Date(),
+            tempTargetDuration: tempTarget?.duration ?? 0,
+            tempTargetTarget: tempTarget?.target ?? 0,
             widgetItems: widgetItems ?? [] // set empty array here to silence compiler; this can never be nil
         )
 

+ 22 - 0
Trio/Sources/Services/LiveActivity/LiveActivityManager.swift

@@ -36,6 +36,8 @@ final class LiveActivityData: ObservableObject {
     @Published var glucoseFromPersistence: [GlucoseData]?
     /// The current override data (if any).
     @Published var override: OverrideData?
+    /// The current temp target data (if any).
+    @Published var tempTarget: TempTargetData?
     /// The widget items displayed within the live activity.
     @Published var widgetItems: [LiveActivityAttributes.LiveActivityItem]?
 }
@@ -141,6 +143,10 @@ final class LiveActivityData: ObservableObject {
             Task { await self?.loadOverrides() }
         }.store(in: &subscriptions)
 
+        coreDataPublisher?.filteredByEntityName("TempTargetStored").sink { [weak self] _ in
+            Task { await self?.loadTempTarget() }
+        }.store(in: &subscriptions)
+
         coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
             Task { await self?.loadGlucose() }
         }.store(in: &subscriptions)
@@ -179,6 +185,15 @@ final class LiveActivityData: ObservableObject {
         }
     }
 
+    /// Fetches and maps temp target data and updates the live activity content state.
+    private func loadTempTarget() async {
+        do {
+            data.tempTarget = try await fetchAndMapTempTarget()
+        } catch {
+            debug(.default, "[LiveActivityManager] \(DebuggingIdentifiers.failed) failed to fetch and map temp target: \(error)")
+        }
+    }
+
     /// Handles changes to the live activity order.
     ///
     /// Loads widget items from user defaults and triggers an update to the live activity order.
@@ -203,6 +218,7 @@ final class LiveActivityData: ObservableObject {
         Task {
             await self.loadGlucose()
             await self.loadOverrides()
+            await self.loadTempTarget()
             await self.loadDetermination()
             self.loadWidgetItems()
         }
@@ -301,6 +317,11 @@ final class LiveActivityData: ObservableObject {
                                 overrideDate: Date.now,
                                 overrideDuration: 0,
                                 overrideTarget: 0,
+                                isTempTargetActive: false,
+                                tempTargetName: "",
+                                tempTargetDate: Date.now,
+                                tempTargetDuration: 0,
+                                tempTargetTarget: 0,
                                 widgetItems: []
                             ),
                             isInitialState: true
@@ -399,6 +420,7 @@ final class LiveActivityData: ObservableObject {
             determination: determination,
             iob: data.iob,
             override: data.override,
+            tempTarget: data.tempTarget,
             widgetItems: data.widgetItems
         )