|
@@ -1,578 +1,69 @@
|
|
|
import ActivityKit
|
|
import ActivityKit
|
|
|
-import Charts
|
|
|
|
|
-import Foundation
|
|
|
|
|
import SwiftUI
|
|
import SwiftUI
|
|
|
import WidgetKit
|
|
import WidgetKit
|
|
|
|
|
|
|
|
-private enum Size {
|
|
|
|
|
- case minimal
|
|
|
|
|
- case compact
|
|
|
|
|
- case expanded
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-enum GlucoseUnits: String, Equatable {
|
|
|
|
|
- case mgdL = "mg/dL"
|
|
|
|
|
- case mmolL = "mmol/L"
|
|
|
|
|
-
|
|
|
|
|
- static let exchangeRate: Decimal = 0.0555
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-enum GlucoseColorScheme: String, Equatable {
|
|
|
|
|
- case staticColor
|
|
|
|
|
- case dynamicColor
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-func rounded(_ value: Decimal, scale: Int, roundingMode: NSDecimalNumber.RoundingMode) -> Decimal {
|
|
|
|
|
- var result = Decimal()
|
|
|
|
|
- var toRound = value
|
|
|
|
|
- NSDecimalRound(&result, &toRound, scale, roundingMode)
|
|
|
|
|
- return result
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-extension Int {
|
|
|
|
|
- var asMmolL: Decimal {
|
|
|
|
|
- rounded(Decimal(self) * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- var formattedAsMmolL: String {
|
|
|
|
|
- NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-extension Decimal {
|
|
|
|
|
- var asMmolL: Decimal {
|
|
|
|
|
- rounded(self * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- var asMgdL: Decimal {
|
|
|
|
|
- rounded(self / GlucoseUnits.exchangeRate, scale: 0, roundingMode: .plain)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- var formattedAsMmolL: String {
|
|
|
|
|
- NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-extension NumberFormatter {
|
|
|
|
|
- static let glucoseFormatter: NumberFormatter = {
|
|
|
|
|
- let formatter = NumberFormatter()
|
|
|
|
|
- formatter.locale = Locale.current
|
|
|
|
|
- formatter.numberStyle = .decimal
|
|
|
|
|
- formatter.minimumFractionDigits = 1
|
|
|
|
|
- formatter.maximumFractionDigits = 1
|
|
|
|
|
- return formatter
|
|
|
|
|
- }()
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
struct LiveActivity: Widget {
|
|
struct LiveActivity: Widget {
|
|
|
- // Helper function to decide how to pick the glucose color
|
|
|
|
|
- func getDynamicGlucoseColor(
|
|
|
|
|
- glucoseValue: Decimal,
|
|
|
|
|
- highGlucoseColorValue: Decimal,
|
|
|
|
|
- lowGlucoseColorValue: Decimal,
|
|
|
|
|
- targetGlucose: Decimal,
|
|
|
|
|
- glucoseColorScheme: String,
|
|
|
|
|
- offset: Decimal
|
|
|
|
|
- ) -> Color {
|
|
|
|
|
- // Convert Decimal to Int for high and low glucose values
|
|
|
|
|
- let lowGlucose = lowGlucoseColorValue - offset
|
|
|
|
|
- let highGlucose = highGlucoseColorValue + (offset * 1.75)
|
|
|
|
|
- let targetGlucose = targetGlucose
|
|
|
|
|
-
|
|
|
|
|
- // Only use calculateHueBasedGlucoseColor if the setting is enabled in preferences
|
|
|
|
|
- if glucoseColorScheme == "dynamicColor" {
|
|
|
|
|
- return calculateHueBasedGlucoseColor(
|
|
|
|
|
- glucoseValue: glucoseValue,
|
|
|
|
|
- highGlucose: highGlucose,
|
|
|
|
|
- lowGlucose: lowGlucose,
|
|
|
|
|
- targetGlucose: targetGlucose
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
- // Otheriwse, use static (orange = high, red = low, green = range)
|
|
|
|
|
- else {
|
|
|
|
|
- if glucoseValue > highGlucose {
|
|
|
|
|
- return Color.orange
|
|
|
|
|
- } else if glucoseValue < lowGlucose {
|
|
|
|
|
- return Color.red
|
|
|
|
|
- } else {
|
|
|
|
|
- return Color.green
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Dynamic color - Define the hue values for the key points
|
|
|
|
|
- // We'll shift color gradually one glucose point at a time
|
|
|
|
|
- // We'll shift through the rainbow colors of ROY-G-BIV from low to high
|
|
|
|
|
- // Start at red for lowGlucose, green for targetGlucose, and violet for highGlucose
|
|
|
|
|
- func calculateHueBasedGlucoseColor(
|
|
|
|
|
- glucoseValue: Decimal,
|
|
|
|
|
- highGlucose: Decimal,
|
|
|
|
|
- lowGlucose: Decimal,
|
|
|
|
|
- targetGlucose: Decimal
|
|
|
|
|
- ) -> Color {
|
|
|
|
|
- let redHue: CGFloat = 0.0 / 360.0 // 0 degrees
|
|
|
|
|
- let greenHue: CGFloat = 120.0 / 360.0 // 120 degrees
|
|
|
|
|
- let purpleHue: CGFloat = 270.0 / 360.0 // 270 degrees
|
|
|
|
|
-
|
|
|
|
|
- // Calculate the hue based on the bgLevel
|
|
|
|
|
- var hue: CGFloat
|
|
|
|
|
- if glucoseValue <= lowGlucose {
|
|
|
|
|
- hue = redHue
|
|
|
|
|
- } else if glucoseValue >= highGlucose {
|
|
|
|
|
- hue = purpleHue
|
|
|
|
|
- } else if glucoseValue <= targetGlucose {
|
|
|
|
|
- // Interpolate between red and green
|
|
|
|
|
- let ratio = CGFloat(truncating: (glucoseValue - lowGlucose) / (targetGlucose - lowGlucose) as NSNumber)
|
|
|
|
|
-
|
|
|
|
|
- hue = redHue + ratio * (greenHue - redHue)
|
|
|
|
|
- } else {
|
|
|
|
|
- // Interpolate between green and purple
|
|
|
|
|
- let ratio = CGFloat(truncating: (glucoseValue - targetGlucose) / (highGlucose - targetGlucose) as NSNumber)
|
|
|
|
|
- hue = greenHue + ratio * (purpleHue - greenHue)
|
|
|
|
|
- }
|
|
|
|
|
- // Return the color with full saturation and brightness
|
|
|
|
|
- let color = Color(hue: hue, saturation: 0.6, brightness: 0.9)
|
|
|
|
|
- return color
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private let dateFormatter: DateFormatter = {
|
|
|
|
|
- var f = DateFormatter()
|
|
|
|
|
- f.dateStyle = .none
|
|
|
|
|
- f.timeStyle = .short
|
|
|
|
|
- return f
|
|
|
|
|
- }()
|
|
|
|
|
-
|
|
|
|
|
- private var bolusFormatter: NumberFormatter {
|
|
|
|
|
- let formatter = NumberFormatter()
|
|
|
|
|
- formatter.numberStyle = .decimal
|
|
|
|
|
- formatter.maximumFractionDigits = 2
|
|
|
|
|
- formatter.decimalSeparator = "."
|
|
|
|
|
- return formatter
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private var carbsFormatter: NumberFormatter {
|
|
|
|
|
- let formatter = NumberFormatter()
|
|
|
|
|
- formatter.numberStyle = .decimal
|
|
|
|
|
- formatter.maximumFractionDigits = 0
|
|
|
|
|
- return formatter
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- @ViewBuilder private func changeLabel(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
|
|
|
|
|
- if !context.state.change.isEmpty {
|
|
|
|
|
- Text(context.state.change).foregroundStyle(.primary).font(.headline)
|
|
|
|
|
- .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
|
|
|
|
|
- } else {
|
|
|
|
|
- Text("--")
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- @ViewBuilder func mealLabel(
|
|
|
|
|
- context: ActivityViewContext<LiveActivityAttributes>,
|
|
|
|
|
- additionalState: LiveActivityAttributes.ContentAdditionalState
|
|
|
|
|
- ) -> some View {
|
|
|
|
|
- HStack {
|
|
|
|
|
- VStack(alignment: .leading, spacing: 1, content: {
|
|
|
|
|
- HStack {
|
|
|
|
|
- Image(systemName: "fork.knife")
|
|
|
|
|
- .font(.title3)
|
|
|
|
|
- .foregroundColor(.yellow)
|
|
|
|
|
- }
|
|
|
|
|
- HStack {
|
|
|
|
|
- Image(systemName: "syringe.fill")
|
|
|
|
|
- .font(.title3)
|
|
|
|
|
- .foregroundColor(.blue)
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
- VStack(alignment: .trailing, spacing: 1, content: {
|
|
|
|
|
- HStack {
|
|
|
|
|
- Text(
|
|
|
|
|
- carbsFormatter.string(from: additionalState.cob as NSNumber) ?? "--"
|
|
|
|
|
- ).fontWeight(.bold).font(.headline).strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
|
|
|
|
|
- Text(NSLocalizedString(" g", comment: "grams of carbs")).foregroundStyle(.secondary).font(.footnote)
|
|
|
|
|
- }
|
|
|
|
|
- HStack {
|
|
|
|
|
- Text(
|
|
|
|
|
- bolusFormatter.string(from: additionalState.iob as NSNumber) ?? "--"
|
|
|
|
|
- ).font(.headline).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(.secondary).font(.footnote)
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
- VStack(alignment: .trailing, spacing: 1, content: {
|
|
|
|
|
- if additionalState.isOverrideActive {
|
|
|
|
|
- Image(systemName: "person.crop.circle.fill.badge.checkmark")
|
|
|
|
|
- .font(.title3)
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- @ViewBuilder func trend(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
|
|
|
|
|
- if context.isStale {
|
|
|
|
|
- Text("--")
|
|
|
|
|
- } else {
|
|
|
|
|
- if let trendSystemImage = context.state.direction {
|
|
|
|
|
- Image(systemName: trendSystemImage)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private func expiredLabel() -> some View {
|
|
|
|
|
- Text("Live Activity Expired. Open Trio to Refresh")
|
|
|
|
|
- .minimumScaleFactor(0.01)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private func updatedLabel(context: ActivityViewContext<LiveActivityAttributes>) -> Text {
|
|
|
|
|
- let text = Text("Updated: \(dateFormatter.string(from: context.state.date))")
|
|
|
|
|
- .font(.caption2)
|
|
|
|
|
- if context.isStale {
|
|
|
|
|
- // foregroundStyle is not available in <iOS 17 hence the check here
|
|
|
|
|
- if #available(iOSApplicationExtension 17.0, *) {
|
|
|
|
|
- return text.bold().foregroundStyle(.red)
|
|
|
|
|
- } else {
|
|
|
|
|
- return text.bold().foregroundColor(.red)
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- if #available(iOSApplicationExtension 17.0, *) {
|
|
|
|
|
- return text.bold().foregroundStyle(.secondary)
|
|
|
|
|
- } else {
|
|
|
|
|
- return text.bold().foregroundColor(.red)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- @ViewBuilder private func bgLabel(
|
|
|
|
|
- context: ActivityViewContext<LiveActivityAttributes>,
|
|
|
|
|
- additionalState: LiveActivityAttributes.ContentAdditionalState
|
|
|
|
|
- ) -> some View {
|
|
|
|
|
- HStack(alignment: .center) {
|
|
|
|
|
- Text(context.state.bg)
|
|
|
|
|
- .fontWeight(.bold)
|
|
|
|
|
- .font(.largeTitle)
|
|
|
|
|
- .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
|
|
|
|
|
- Text(additionalState.unit).foregroundStyle(.secondary).font(.subheadline).offset(x: -5, y: 5)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private func bgAndTrend(
|
|
|
|
|
- context: ActivityViewContext<LiveActivityAttributes>,
|
|
|
|
|
- size: Size,
|
|
|
|
|
- hasStaticColorScheme: Bool,
|
|
|
|
|
- glucoseColor: Color
|
|
|
|
|
- ) -> (some View, Int) {
|
|
|
|
|
- var characters = 0
|
|
|
|
|
-
|
|
|
|
|
- let bgText = context.state.bg
|
|
|
|
|
- characters += bgText.count
|
|
|
|
|
-
|
|
|
|
|
- // 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
|
|
|
|
|
- var directionText: String?
|
|
|
|
|
- if let direction = context.state.direction {
|
|
|
|
|
- if size == .compact {
|
|
|
|
|
- directionText = String(direction[direction.startIndex ... direction.startIndex])
|
|
|
|
|
- } else {
|
|
|
|
|
- directionText = direction
|
|
|
|
|
|
|
+ var body: some WidgetConfiguration {
|
|
|
|
|
+ ActivityConfiguration(for: LiveActivityAttributes.self) { context in
|
|
|
|
|
+ LiveActivityView(context: context)
|
|
|
|
|
+ } dynamicIsland: { context in
|
|
|
|
|
+ let hasStaticColorScheme = context.state.glucoseColorScheme == "staticColor"
|
|
|
|
|
+
|
|
|
|
|
+ var glucoseColor: Color {
|
|
|
|
|
+ let state = context.state
|
|
|
|
|
+ let detailedState = state.detailedViewState
|
|
|
|
|
+ let isMgdL = detailedState?.unit == "mg/dL"
|
|
|
|
|
+
|
|
|
|
|
+ // 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
|
|
|
|
|
+ let hardCodedHigh = isMgdL ? Decimal(220) : 220.asMmolL
|
|
|
|
|
+
|
|
|
|
|
+ return Color.getDynamicGlucoseColor(
|
|
|
|
|
+ glucoseValue: Decimal(string: state.bg) ?? 100,
|
|
|
|
|
+ highGlucoseColorValue: !hasStaticColorScheme ? hardCodedHigh : state.highGlucose,
|
|
|
|
|
+ lowGlucoseColorValue: !hasStaticColorScheme ? hardCodedLow : state.lowGlucose,
|
|
|
|
|
+ targetGlucose: isMgdL ? state.target : state.target.asMmolL,
|
|
|
|
|
+ glucoseColorScheme: state.glucoseColorScheme
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- characters += directionText!.count
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- let spacing: CGFloat
|
|
|
|
|
- switch size {
|
|
|
|
|
- case .minimal: spacing = -1
|
|
|
|
|
- case .compact: spacing = 0
|
|
|
|
|
- case .expanded: spacing = 3
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- let stack = HStack(spacing: spacing) {
|
|
|
|
|
- Text(bgText)
|
|
|
|
|
- .foregroundColor(hasStaticColorScheme ? .primary : glucoseColor)
|
|
|
|
|
- .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)
|
|
|
|
|
- scaledText.foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
|
|
|
|
|
- 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)
|
|
|
|
|
|
|
+ return DynamicIsland {
|
|
|
|
|
+ DynamicIslandExpandedRegion(.leading) {
|
|
|
|
|
+ LiveActivityExpandedLeadingView(context: context, glucoseColor: glucoseColor)
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- .foregroundColor(context.isStale ? Color.primary.opacity(0.5) : (hasStaticColorScheme ? .primary : glucoseColor))
|
|
|
|
|
-
|
|
|
|
|
- return (stack, characters)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- @ViewBuilder func trendArrow(
|
|
|
|
|
- context: ActivityViewContext<LiveActivityAttributes>,
|
|
|
|
|
- additionalState: LiveActivityAttributes.ContentAdditionalState
|
|
|
|
|
- ) -> some View {
|
|
|
|
|
- let gradient = LinearGradient(colors: [
|
|
|
|
|
- Color(red: 0.6235294118, green: 0.4235294118, blue: 0.9803921569),
|
|
|
|
|
- Color(red: 0.4862745098, green: 0.5450980392, blue: 0.9529411765),
|
|
|
|
|
- Color(red: 0.3411764706, green: 0.6666666667, blue: 0.9254901961),
|
|
|
|
|
- Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902)
|
|
|
|
|
- ], startPoint: .leading, endPoint: .trailing)
|
|
|
|
|
-
|
|
|
|
|
- if !context.isStale {
|
|
|
|
|
- Image(systemName: "arrow.right")
|
|
|
|
|
- .font(.title)
|
|
|
|
|
- .rotationEffect(.degrees(additionalState.rotationDegrees))
|
|
|
|
|
- .foregroundStyle(gradient)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- @ViewBuilder func chart(
|
|
|
|
|
- context: ActivityViewContext<LiveActivityAttributes>,
|
|
|
|
|
- additionalState: LiveActivityAttributes.ContentAdditionalState
|
|
|
|
|
- ) -> some View {
|
|
|
|
|
- if context.isStale {
|
|
|
|
|
- Text("No data available")
|
|
|
|
|
- } else {
|
|
|
|
|
- // Determine scale
|
|
|
|
|
- let min = min(additionalState.chart.min() ?? 45, 40) - 20
|
|
|
|
|
- let max = max(additionalState.chart.max() ?? 270, 300) + 50
|
|
|
|
|
-
|
|
|
|
|
- let yAxisRuleMarkMin = additionalState.unit == "mg/dL" ? context.state.lowGlucose : context.state.lowGlucose
|
|
|
|
|
- .asMmolL
|
|
|
|
|
- let yAxisRuleMarkMax = additionalState.unit == "mg/dL" ? context.state.highGlucose : context.state.highGlucose
|
|
|
|
|
- .asMmolL
|
|
|
|
|
-
|
|
|
|
|
- // TODO: grab target from proper targets, do not hard code.
|
|
|
|
|
- let highColor = getDynamicGlucoseColor(
|
|
|
|
|
- glucoseValue: yAxisRuleMarkMax,
|
|
|
|
|
- highGlucoseColorValue: yAxisRuleMarkMax,
|
|
|
|
|
- lowGlucoseColorValue: yAxisRuleMarkMin,
|
|
|
|
|
- targetGlucose: additionalState.unit == "mg/dL" ? Decimal(90) : Decimal(90).asMmolL,
|
|
|
|
|
- glucoseColorScheme: context.state.glucoseColorScheme,
|
|
|
|
|
- offset: additionalState.unit == "mg/dL" ? Decimal(20) : Decimal(20).asMmolL
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- // TODO: grab target from proper targets, do not hard code.
|
|
|
|
|
- let lowColor = getDynamicGlucoseColor(
|
|
|
|
|
- glucoseValue: yAxisRuleMarkMin,
|
|
|
|
|
- highGlucoseColorValue: yAxisRuleMarkMax,
|
|
|
|
|
- lowGlucoseColorValue: yAxisRuleMarkMin,
|
|
|
|
|
- targetGlucose: additionalState.unit == "mg/dL" ? Decimal(90) : Decimal(90).asMmolL,
|
|
|
|
|
- glucoseColorScheme: context.state.glucoseColorScheme,
|
|
|
|
|
- offset: additionalState.unit == "mg/dL" ? Decimal(20) : Decimal(20).asMmolL
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- Chart {
|
|
|
|
|
- RuleMark(y: .value("High", yAxisRuleMarkMax))
|
|
|
|
|
- .foregroundStyle(highColor)
|
|
|
|
|
- .lineStyle(.init(lineWidth: 0.5, dash: [5]))
|
|
|
|
|
- RuleMark(y: .value("Low", yAxisRuleMarkMin))
|
|
|
|
|
- .foregroundStyle(lowColor)
|
|
|
|
|
- .lineStyle(.init(lineWidth: 0.5, dash: [5]))
|
|
|
|
|
-
|
|
|
|
|
- ForEach(additionalState.chart.indices, id: \.self) { index in
|
|
|
|
|
- let currentValue = additionalState.chart[index]
|
|
|
|
|
- let displayValue = additionalState.unit == "mg/dL" ? currentValue : currentValue.asMmolL
|
|
|
|
|
-
|
|
|
|
|
- // TODO: grab target from proper targets, do not hard code.
|
|
|
|
|
- let pointMarkColor = self.getDynamicGlucoseColor(
|
|
|
|
|
- glucoseValue: currentValue,
|
|
|
|
|
- highGlucoseColorValue: context.state.highGlucose,
|
|
|
|
|
- lowGlucoseColorValue: context.state.lowGlucose,
|
|
|
|
|
- targetGlucose: 90,
|
|
|
|
|
- glucoseColorScheme: context.state.glucoseColorScheme,
|
|
|
|
|
- offset: 20
|
|
|
|
|
|
|
+ DynamicIslandExpandedRegion(.trailing) {
|
|
|
|
|
+ LiveActivityExpandedTrailingView(
|
|
|
|
|
+ context: context,
|
|
|
|
|
+ glucoseColor: hasStaticColorScheme ? .primary : glucoseColor
|
|
|
)
|
|
)
|
|
|
-
|
|
|
|
|
- let chartDate = additionalState.chartDate[index] ?? Date()
|
|
|
|
|
-
|
|
|
|
|
- let pointMark = PointMark(
|
|
|
|
|
- x: .value("Time", chartDate),
|
|
|
|
|
- y: .value("Value", displayValue)
|
|
|
|
|
- ).symbolSize(15)
|
|
|
|
|
-
|
|
|
|
|
- pointMark.foregroundStyle(pointMarkColor)
|
|
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
- .chartYAxis {
|
|
|
|
|
- AxisMarks(position: .trailing) { _ in
|
|
|
|
|
- AxisGridLine(stroke: .init(lineWidth: 0.2, dash: [2, 3])).foregroundStyle(Color.white)
|
|
|
|
|
- AxisValueLabel().foregroundStyle(.secondary).font(.footnote)
|
|
|
|
|
|
|
+ DynamicIslandExpandedRegion(.bottom) {
|
|
|
|
|
+ LiveActivityExpandedBottomView(context: context)
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
- .chartYScale(domain: additionalState.unit == "mg/dL" ? min ... max : min.asMmolL ... max.asMmolL)
|
|
|
|
|
- .chartXAxis {
|
|
|
|
|
- AxisMarks(position: .automatic) { _ in
|
|
|
|
|
- AxisGridLine(stroke: .init(lineWidth: 0.2, dash: [2, 3])).foregroundStyle(Color.white)
|
|
|
|
|
|
|
+ DynamicIslandExpandedRegion(.center) {
|
|
|
|
|
+ LiveActivityExpandedCenterView(context: context)
|
|
|
}
|
|
}
|
|
|
|
|
+ } compactLeading: {
|
|
|
|
|
+ LiveActivityCompactLeadingView(context: context, glucoseColor: glucoseColor)
|
|
|
|
|
+ } compactTrailing: {
|
|
|
|
|
+ LiveActivityCompactTrailingView(context: context, glucoseColor: hasStaticColorScheme ? .primary : glucoseColor)
|
|
|
|
|
+ } minimal: {
|
|
|
|
|
+ LiveActivityMinimalView(context: context, glucoseColor: glucoseColor)
|
|
|
}
|
|
}
|
|
|
|
|
+ .widgetURL(URL(string: "Trio://"))
|
|
|
|
|
+ .keylineTint(glucoseColor)
|
|
|
|
|
+ .contentMargins(.horizontal, 0, for: .minimal)
|
|
|
|
|
+ .contentMargins(.trailing, 0, for: .compactLeading)
|
|
|
|
|
+ .contentMargins(.leading, 0, for: .compactTrailing)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- @ViewBuilder func content(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
|
|
|
|
|
- let hasStaticColorScheme = context.state.glucoseColorScheme == "staticColor"
|
|
|
|
|
- // TODO: grab target from proper targets, do not hard code.
|
|
|
|
|
- let glucoseColor = getDynamicGlucoseColor(
|
|
|
|
|
- glucoseValue: Decimal(string: context.state.bg) ?? 100,
|
|
|
|
|
- highGlucoseColorValue: context.state.detailedViewState?.unit == "mg/dL" ? context.state.highGlucose : context.state
|
|
|
|
|
- .highGlucose.asMmolL,
|
|
|
|
|
- lowGlucoseColorValue: context.state.detailedViewState?.unit == "mg/dL" ? context.state.lowGlucose : context.state
|
|
|
|
|
- .lowGlucose.asMmolL,
|
|
|
|
|
- targetGlucose: context.state.detailedViewState?.unit == "mg/dL" ? 90 : 90.asMmolL,
|
|
|
|
|
- glucoseColorScheme: context.state.glucoseColorScheme,
|
|
|
|
|
- offset: context.state.detailedViewState?.unit == "mg/dL" ? 20 : 20.asMmolL
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- if let detailedViewState = context.state.detailedViewState {
|
|
|
|
|
- HStack(spacing: 12) {
|
|
|
|
|
- chart(context: context, additionalState: detailedViewState)
|
|
|
|
|
- .frame(maxWidth: UIScreen.main.bounds.width / 1.8)
|
|
|
|
|
- VStack(alignment: .leading) {
|
|
|
|
|
- Spacer()
|
|
|
|
|
- bgLabel(context: context, additionalState: detailedViewState)
|
|
|
|
|
- HStack {
|
|
|
|
|
- changeLabel(context: context)
|
|
|
|
|
- trendArrow(context: context, additionalState: detailedViewState)
|
|
|
|
|
- }
|
|
|
|
|
- mealLabel(context: context, additionalState: detailedViewState).padding(.bottom, 8)
|
|
|
|
|
- updatedLabel(context: context).padding(.bottom, 10)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- .privacySensitive()
|
|
|
|
|
- .padding(.all, 14)
|
|
|
|
|
- .imageScale(.small)
|
|
|
|
|
- .foregroundColor(Color.white)
|
|
|
|
|
- .activityBackgroundTint(Color.black.opacity(0.8))
|
|
|
|
|
- } 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,
|
|
|
|
|
- hasStaticColorScheme: hasStaticColorScheme,
|
|
|
|
|
- glucoseColor: glucoseColor
|
|
|
|
|
- ).0.font(.title)
|
|
|
|
|
- Spacer()
|
|
|
|
|
- VStack(alignment: .trailing, spacing: 5) {
|
|
|
|
|
- changeLabel(context: context).font(.title3)
|
|
|
|
|
- .foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
|
|
|
|
|
- updatedLabel(context: context).font(.caption)
|
|
|
|
|
- .foregroundStyle(
|
|
|
|
|
- hasStaticColorScheme ? .primary
|
|
|
|
|
- .opacity(0.7) : glucoseColor
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- .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.clear)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- func dynamicIsland(context: ActivityViewContext<LiveActivityAttributes>) -> DynamicIsland {
|
|
|
|
|
- let glucoseValueForColor = context.state.bg
|
|
|
|
|
- let highGlucose = context.state.highGlucose
|
|
|
|
|
- let lowGlucose = context.state.lowGlucose
|
|
|
|
|
-
|
|
|
|
|
- let hasStaticColorScheme = context.state.glucoseColorScheme == "staticColor"
|
|
|
|
|
- // TODO: grab target from proper targets, do not hard code.
|
|
|
|
|
- let glucoseColor = getDynamicGlucoseColor(
|
|
|
|
|
- glucoseValue: Decimal(string: glucoseValueForColor) ?? 100,
|
|
|
|
|
- highGlucoseColorValue: context.state.detailedViewState?.unit == "mg/dL" ? highGlucose : highGlucose.asMmolL,
|
|
|
|
|
- lowGlucoseColorValue: context.state.detailedViewState?.unit == "mg/dL" ? lowGlucose : lowGlucose.asMmolL,
|
|
|
|
|
- targetGlucose: context.state.detailedViewState?.unit == "mg/dL" ? 90 : 90.asMmolL,
|
|
|
|
|
- glucoseColorScheme: context.state.glucoseColorScheme,
|
|
|
|
|
- offset: context.state.detailedViewState?.unit == "mg/dL" ? 20 : 20.asMmolL
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- return DynamicIsland {
|
|
|
|
|
- DynamicIslandExpandedRegion(.leading) {
|
|
|
|
|
- bgAndTrend(
|
|
|
|
|
- context: context,
|
|
|
|
|
- size: .expanded,
|
|
|
|
|
- hasStaticColorScheme: hasStaticColorScheme,
|
|
|
|
|
- glucoseColor: glucoseColor
|
|
|
|
|
- ).0.font(.title2).padding(.leading, 5)
|
|
|
|
|
- }
|
|
|
|
|
- DynamicIslandExpandedRegion(.trailing) {
|
|
|
|
|
- changeLabel(context: context).font(.title2).padding(.trailing, 5)
|
|
|
|
|
- .foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
|
|
|
|
|
- }
|
|
|
|
|
- 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, hasStaticColorScheme: hasStaticColorScheme, glucoseColor: glucoseColor).0
|
|
|
|
|
- .padding(.leading, 4)
|
|
|
|
|
- } compactTrailing: {
|
|
|
|
|
- changeLabel(context: context).padding(.trailing, 4).foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
|
|
|
|
|
- } minimal: {
|
|
|
|
|
- let (_label, characterCount) = bgAndTrend(
|
|
|
|
|
- context: context,
|
|
|
|
|
- size: .minimal,
|
|
|
|
|
- hasStaticColorScheme: hasStaticColorScheme,
|
|
|
|
|
- glucoseColor: glucoseColor
|
|
|
|
|
- )
|
|
|
|
|
- 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(hasStaticColorScheme ? Color.purple : glucoseColor)
|
|
|
|
|
- .contentMargins(.horizontal, 0, for: .minimal)
|
|
|
|
|
- .contentMargins(.trailing, 0, for: .compactLeading)
|
|
|
|
|
- .contentMargins(.leading, 0, for: .compactTrailing)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- var body: some WidgetConfiguration {
|
|
|
|
|
- ActivityConfiguration(for: LiveActivityAttributes.self, content: self.content, dynamicIsland: self.dynamicIsland)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+// Mock structure to replace GlucoseData
|
|
|
|
|
+struct MockGlucoseData {
|
|
|
|
|
+ var glucose: Int
|
|
|
|
|
+ var date: Date
|
|
|
|
|
+ var direction: String? // You can refine this based on your expected data
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private extension LiveActivityAttributes {
|
|
private extension LiveActivityAttributes {
|
|
@@ -582,17 +73,39 @@ private extension LiveActivityAttributes {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private extension LiveActivityAttributes.ContentState {
|
|
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: 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,
|
|
|
|
|
+ unit: GlucoseUnits.mgdL.rawValue,
|
|
|
|
|
+ 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.
|
|
// 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
|
|
// 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 {
|
|
static var testWide: LiveActivityAttributes.ContentState {
|
|
|
LiveActivityAttributes.ContentState(
|
|
LiveActivityAttributes.ContentState(
|
|
|
- bg: 00.0.description,
|
|
|
|
|
|
|
+ bg: "00.0",
|
|
|
direction: "→",
|
|
direction: "→",
|
|
|
change: "+0.0",
|
|
change: "+0.0",
|
|
|
date: Date(),
|
|
date: Date(),
|
|
|
highGlucose: 180,
|
|
highGlucose: 180,
|
|
|
lowGlucose: 70,
|
|
lowGlucose: 70,
|
|
|
|
|
+ target: 100,
|
|
|
glucoseColorScheme: "staticColor",
|
|
glucoseColorScheme: "staticColor",
|
|
|
detailedViewState: nil,
|
|
detailedViewState: nil,
|
|
|
isInitialState: false
|
|
isInitialState: false
|
|
@@ -607,6 +120,7 @@ private extension LiveActivityAttributes.ContentState {
|
|
|
date: Date(),
|
|
date: Date(),
|
|
|
highGlucose: 180,
|
|
highGlucose: 180,
|
|
|
lowGlucose: 70,
|
|
lowGlucose: 70,
|
|
|
|
|
+ target: 100,
|
|
|
glucoseColorScheme: "staticColor",
|
|
glucoseColorScheme: "staticColor",
|
|
|
detailedViewState: nil,
|
|
detailedViewState: nil,
|
|
|
isInitialState: false
|
|
isInitialState: false
|
|
@@ -621,6 +135,7 @@ private extension LiveActivityAttributes.ContentState {
|
|
|
date: Date(),
|
|
date: Date(),
|
|
|
highGlucose: 180,
|
|
highGlucose: 180,
|
|
|
lowGlucose: 70,
|
|
lowGlucose: 70,
|
|
|
|
|
+ target: 100,
|
|
|
glucoseColorScheme: "staticColor",
|
|
glucoseColorScheme: "staticColor",
|
|
|
detailedViewState: nil,
|
|
detailedViewState: nil,
|
|
|
isInitialState: false
|
|
isInitialState: false
|
|
@@ -636,6 +151,7 @@ private extension LiveActivityAttributes.ContentState {
|
|
|
date: Date(),
|
|
date: Date(),
|
|
|
highGlucose: 180,
|
|
highGlucose: 180,
|
|
|
lowGlucose: 70,
|
|
lowGlucose: 70,
|
|
|
|
|
+ target: 100,
|
|
|
glucoseColorScheme: "staticColor",
|
|
glucoseColorScheme: "staticColor",
|
|
|
detailedViewState: nil,
|
|
detailedViewState: nil,
|
|
|
isInitialState: false
|
|
isInitialState: false
|
|
@@ -650,6 +166,7 @@ private extension LiveActivityAttributes.ContentState {
|
|
|
date: Date(),
|
|
date: Date(),
|
|
|
highGlucose: 180,
|
|
highGlucose: 180,
|
|
|
lowGlucose: 70,
|
|
lowGlucose: 70,
|
|
|
|
|
+ target: 100,
|
|
|
glucoseColorScheme: "staticColor",
|
|
glucoseColorScheme: "staticColor",
|
|
|
detailedViewState: nil,
|
|
detailedViewState: nil,
|
|
|
isInitialState: false
|
|
isInitialState: false
|
|
@@ -664,15 +181,107 @@ private extension LiveActivityAttributes.ContentState {
|
|
|
date: Date().addingTimeInterval(-60 * 60),
|
|
date: Date().addingTimeInterval(-60 * 60),
|
|
|
highGlucose: 180,
|
|
highGlucose: 180,
|
|
|
lowGlucose: 70,
|
|
lowGlucose: 70,
|
|
|
|
|
+ target: 100,
|
|
|
glucoseColorScheme: "staticColor",
|
|
glucoseColorScheme: "staticColor",
|
|
|
detailedViewState: nil,
|
|
detailedViewState: nil,
|
|
|
- isInitialState: true
|
|
|
|
|
|
|
+ isInitialState: false
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static var testWideDetailed: LiveActivityAttributes.ContentState {
|
|
|
|
|
+ LiveActivityAttributes.ContentState(
|
|
|
|
|
+ bg: "00.0",
|
|
|
|
|
+ direction: "→",
|
|
|
|
|
+ change: "+0.0",
|
|
|
|
|
+ date: Date(),
|
|
|
|
|
+ highGlucose: 180,
|
|
|
|
|
+ lowGlucose: 70,
|
|
|
|
|
+ target: 100,
|
|
|
|
|
+ glucoseColorScheme: "staticColor",
|
|
|
|
|
+ detailedViewState: detailedViewState,
|
|
|
|
|
+ isInitialState: false
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static var testVeryWideDetailed: LiveActivityAttributes.ContentState {
|
|
|
|
|
+ LiveActivityAttributes.ContentState(
|
|
|
|
|
+ bg: "00.0",
|
|
|
|
|
+ direction: "↑↑",
|
|
|
|
|
+ change: "+0.0",
|
|
|
|
|
+ date: Date(),
|
|
|
|
|
+ highGlucose: 180,
|
|
|
|
|
+ lowGlucose: 70,
|
|
|
|
|
+ target: 100,
|
|
|
|
|
+ glucoseColorScheme: "staticColor",
|
|
|
|
|
+ detailedViewState: detailedViewState,
|
|
|
|
|
+ isInitialState: false
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static var testSuperWideDetailed: LiveActivityAttributes.ContentState {
|
|
|
|
|
+ LiveActivityAttributes.ContentState(
|
|
|
|
|
+ bg: "00.0",
|
|
|
|
|
+ direction: "↑↑↑",
|
|
|
|
|
+ change: "+0.0",
|
|
|
|
|
+ date: Date(),
|
|
|
|
|
+ highGlucose: 180,
|
|
|
|
|
+ lowGlucose: 70,
|
|
|
|
|
+ target: 100,
|
|
|
|
|
+ glucoseColorScheme: "staticColor",
|
|
|
|
|
+ detailedViewState: detailedViewState,
|
|
|
|
|
+ isInitialState: false
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2 characters for BG, 1 character for change is the minimum that will be shown
|
|
|
|
|
+ static var testNarrowDetailed: LiveActivityAttributes.ContentState {
|
|
|
|
|
+ LiveActivityAttributes.ContentState(
|
|
|
|
|
+ bg: "00",
|
|
|
|
|
+ direction: "↑",
|
|
|
|
|
+ change: "+0",
|
|
|
|
|
+ date: Date(),
|
|
|
|
|
+ highGlucose: 180,
|
|
|
|
|
+ lowGlucose: 70,
|
|
|
|
|
+ target: 100,
|
|
|
|
|
+ glucoseColorScheme: "staticColor",
|
|
|
|
|
+ detailedViewState: detailedViewState,
|
|
|
|
|
+ isInitialState: false
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static var testMediumDetailed: LiveActivityAttributes.ContentState {
|
|
|
|
|
+ LiveActivityAttributes.ContentState(
|
|
|
|
|
+ bg: "000",
|
|
|
|
|
+ direction: "↗︎",
|
|
|
|
|
+ change: "+00",
|
|
|
|
|
+ date: Date(),
|
|
|
|
|
+ highGlucose: 180,
|
|
|
|
|
+ lowGlucose: 70,
|
|
|
|
|
+ target: 100,
|
|
|
|
|
+ glucoseColorScheme: "staticColor",
|
|
|
|
|
+ detailedViewState: detailedViewState,
|
|
|
|
|
+ isInitialState: false
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static var testExpiredDetailed: LiveActivityAttributes.ContentState {
|
|
|
|
|
+ LiveActivityAttributes.ContentState(
|
|
|
|
|
+ bg: "--",
|
|
|
|
|
+ direction: nil,
|
|
|
|
|
+ change: "--",
|
|
|
|
|
+ date: Date().addingTimeInterval(-60 * 60),
|
|
|
|
|
+ highGlucose: 180,
|
|
|
|
|
+ lowGlucose: 70,
|
|
|
|
|
+ target: 100,
|
|
|
|
|
+ glucoseColorScheme: "staticColor",
|
|
|
|
|
+ detailedViewState: detailedViewState,
|
|
|
|
|
+ isInitialState: false
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@available(iOS 17.0, iOSApplicationExtension 17.0, *)
|
|
@available(iOS 17.0, iOSApplicationExtension 17.0, *)
|
|
|
-#Preview("Notification", as: .content, using: LiveActivityAttributes.preview) {
|
|
|
|
|
|
|
+#Preview("Simple", as: .content, using: LiveActivityAttributes.preview) {
|
|
|
LiveActivity()
|
|
LiveActivity()
|
|
|
} contentStates: {
|
|
} contentStates: {
|
|
|
LiveActivityAttributes.ContentState.testSuperWide
|
|
LiveActivityAttributes.ContentState.testSuperWide
|
|
@@ -682,3 +291,15 @@ private extension LiveActivityAttributes.ContentState {
|
|
|
LiveActivityAttributes.ContentState.testNarrow
|
|
LiveActivityAttributes.ContentState.testNarrow
|
|
|
LiveActivityAttributes.ContentState.testExpired
|
|
LiveActivityAttributes.ContentState.testExpired
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+@available(iOS 17.0, iOSApplicationExtension 17.0, *)
|
|
|
|
|
+#Preview("Detailed", as: .content, using: LiveActivityAttributes.preview) {
|
|
|
|
|
+ LiveActivity()
|
|
|
|
|
+} contentStates: {
|
|
|
|
|
+ LiveActivityAttributes.ContentState.testSuperWideDetailed
|
|
|
|
|
+ LiveActivityAttributes.ContentState.testVeryWideDetailed
|
|
|
|
|
+ LiveActivityAttributes.ContentState.testWideDetailed
|
|
|
|
|
+ LiveActivityAttributes.ContentState.testMediumDetailed
|
|
|
|
|
+ LiveActivityAttributes.ContentState.testNarrowDetailed
|
|
|
|
|
+ LiveActivityAttributes.ContentState.testExpiredDetailed
|
|
|
|
|
+}
|