|
@@ -47,7 +47,10 @@ struct LiveActivity: Widget {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- @ViewBuilder func mealLabel(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
|
|
|
|
|
|
|
+ @ViewBuilder func mealLabel(
|
|
|
|
|
+ context _: ActivityViewContext<LiveActivityAttributes>,
|
|
|
|
|
+ additionalState: LiveActivityAttributes.ContentAdditionalState
|
|
|
|
|
+ ) -> some View {
|
|
|
HStack {
|
|
HStack {
|
|
|
VStack(alignment: .leading, spacing: 1, content: {
|
|
VStack(alignment: .leading, spacing: 1, content: {
|
|
|
HStack {
|
|
HStack {
|
|
@@ -63,46 +66,23 @@ struct LiveActivity: Widget {
|
|
|
})
|
|
})
|
|
|
VStack(alignment: .trailing, spacing: 1, content: {
|
|
VStack(alignment: .trailing, spacing: 1, content: {
|
|
|
HStack {
|
|
HStack {
|
|
|
- if context.isStale {
|
|
|
|
|
- Text(
|
|
|
|
|
- carbsFormatter.string(from: context.state.cob as NSNumber) ?? "--"
|
|
|
|
|
- ).fontWeight(.bold).font(.headline).strikethrough(pattern: .solid, color: .red.opacity(0.6))
|
|
|
|
|
- .font(.callout)
|
|
|
|
|
- Text(NSLocalizedString(" g", comment: "grams of carbs")).foregroundStyle(.secondary).font(.footnote)
|
|
|
|
|
- } else {
|
|
|
|
|
- Text(
|
|
|
|
|
- carbsFormatter.string(from: context.state.cob as NSNumber) ?? "--"
|
|
|
|
|
- ).fontWeight(.bold).font(.headline)
|
|
|
|
|
- Text(NSLocalizedString(" g", comment: "grams of carbs")).foregroundStyle(.secondary).font(.footnote)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ Text(
|
|
|
|
|
+ carbsFormatter.string(from: additionalState.cob as NSNumber) ?? "--"
|
|
|
|
|
+ ).fontWeight(.bold).font(.headline)
|
|
|
|
|
+ Text(NSLocalizedString(" g", comment: "grams of carbs")).foregroundStyle(.secondary).font(.footnote)
|
|
|
}
|
|
}
|
|
|
HStack {
|
|
HStack {
|
|
|
- if context.isStale {
|
|
|
|
|
- Text(
|
|
|
|
|
- bolusFormatter.string(from: context.state.iob as NSNumber) ?? "--"
|
|
|
|
|
- ).font(.headline).fontWeight(.bold).strikethrough(pattern: .solid, color: .red.opacity(0.6))
|
|
|
|
|
- .font(.callout)
|
|
|
|
|
- Text(NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)"))
|
|
|
|
|
- .foregroundStyle(.secondary).font(.footnote)
|
|
|
|
|
- } else {
|
|
|
|
|
- Text(
|
|
|
|
|
- bolusFormatter.string(from: context.state.iob as NSNumber) ?? "--"
|
|
|
|
|
- ).font(.headline).fontWeight(.bold)
|
|
|
|
|
- Text(NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)"))
|
|
|
|
|
- .foregroundStyle(.secondary).font(.footnote)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ Text(
|
|
|
|
|
+ bolusFormatter.string(from: additionalState.iob as NSNumber) ?? "--"
|
|
|
|
|
+ ).font(.headline).fontWeight(.bold)
|
|
|
|
|
+ Text(NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)"))
|
|
|
|
|
+ .foregroundStyle(.secondary).font(.footnote)
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
VStack(alignment: .trailing, spacing: 1, content: {
|
|
VStack(alignment: .trailing, spacing: 1, content: {
|
|
|
- if context.state.isOverrideActive {
|
|
|
|
|
- if !context.isStale {
|
|
|
|
|
- Image(systemName: "person.crop.circle.fill.badge.checkmark")
|
|
|
|
|
- .font(.title3)
|
|
|
|
|
- } else {
|
|
|
|
|
- Image(systemName: "person.crop.circle.fill.badge.checkmark")
|
|
|
|
|
- .font(.title3)
|
|
|
|
|
- .strikethrough(pattern: .solid, color: .red.opacity(0.6))
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if additionalState.isOverrideActive {
|
|
|
|
|
+ Image(systemName: "person.crop.circle.fill.badge.checkmark")
|
|
|
|
|
+ .font(.title3)
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
@@ -118,6 +98,11 @@ struct LiveActivity: Widget {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private func expiredLabel() -> some View {
|
|
|
|
|
+ Text("Live Activity Expired. Open Trio to Refresh")
|
|
|
|
|
+ .minimumScaleFactor(0.01)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private func updatedLabel(context: ActivityViewContext<LiveActivityAttributes>) -> Text {
|
|
private func updatedLabel(context: ActivityViewContext<LiveActivityAttributes>) -> Text {
|
|
|
let text = Text("Updated: \(dateFormatter.string(from: context.state.date))")
|
|
let text = Text("Updated: \(dateFormatter.string(from: context.state.date))")
|
|
|
.font(.caption2)
|
|
.font(.caption2)
|
|
@@ -137,13 +122,16 @@ struct LiveActivity: Widget {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- @ViewBuilder private func bgLabel(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
|
|
|
|
|
|
|
+ @ViewBuilder private func bgLabel(
|
|
|
|
|
+ context: ActivityViewContext<LiveActivityAttributes>,
|
|
|
|
|
+ additionalState: LiveActivityAttributes.ContentAdditionalState
|
|
|
|
|
+ ) -> some View {
|
|
|
HStack(alignment: .center) {
|
|
HStack(alignment: .center) {
|
|
|
Text(context.state.bg)
|
|
Text(context.state.bg)
|
|
|
.fontWeight(.bold)
|
|
.fontWeight(.bold)
|
|
|
.font(.largeTitle)
|
|
.font(.largeTitle)
|
|
|
.strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
|
|
.strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
|
|
|
- Text(context.state.unit).foregroundStyle(.secondary).font(.subheadline).offset(x: -5, y: 5)
|
|
|
|
|
|
|
+ Text(additionalState.unit).foregroundStyle(.secondary).font(.subheadline).offset(x: -5, y: 5)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -206,7 +194,10 @@ struct LiveActivity: Widget {
|
|
|
return (stack, characters)
|
|
return (stack, characters)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- @ViewBuilder func bobble(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
|
|
|
|
|
|
|
+ @ViewBuilder func trendArrow(
|
|
|
|
|
+ context: ActivityViewContext<LiveActivityAttributes>,
|
|
|
|
|
+ additionalState: LiveActivityAttributes.ContentAdditionalState
|
|
|
|
|
+ ) -> some View {
|
|
|
let gradient = LinearGradient(colors: [
|
|
let gradient = LinearGradient(colors: [
|
|
|
Color(red: 0.6235294118, green: 0.4235294118, blue: 0.9803921569),
|
|
Color(red: 0.6235294118, green: 0.4235294118, blue: 0.9803921569),
|
|
|
Color(red: 0.4862745098, green: 0.5450980392, blue: 0.9529411765),
|
|
Color(red: 0.4862745098, green: 0.5450980392, blue: 0.9529411765),
|
|
@@ -217,43 +208,43 @@ struct LiveActivity: Widget {
|
|
|
if !context.isStale {
|
|
if !context.isStale {
|
|
|
Image(systemName: "arrow.right")
|
|
Image(systemName: "arrow.right")
|
|
|
.font(.title)
|
|
.font(.title)
|
|
|
- .rotationEffect(.degrees(context.state.rotationDegrees))
|
|
|
|
|
|
|
+ .rotationEffect(.degrees(additionalState.rotationDegrees))
|
|
|
.foregroundStyle(gradient)
|
|
.foregroundStyle(gradient)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- @ViewBuilder func chart(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
|
|
|
|
|
|
|
+ @ViewBuilder func chart(
|
|
|
|
|
+ context: ActivityViewContext<LiveActivityAttributes>,
|
|
|
|
|
+ additionalState: LiveActivityAttributes.ContentAdditionalState
|
|
|
|
|
+ ) -> some View {
|
|
|
if context.isStale {
|
|
if context.isStale {
|
|
|
Text("No data available")
|
|
Text("No data available")
|
|
|
} else {
|
|
} else {
|
|
|
- // determine scale
|
|
|
|
|
- let min = (context.state.chart.min() ?? 40 * (context.state.unit == " mmol/L" ? 0.0555 : 1)) - 20 *
|
|
|
|
|
- (context.state.unit == " mmol/L" ? 0.0555 : 1)
|
|
|
|
|
- let max = (context.state.chart.max() ?? 270 * (context.state.unit == " mmol/L" ? 0.0555 : 1)) + 50 *
|
|
|
|
|
- (context.state.unit == " mmol/L" ? 0.0555 : 1)
|
|
|
|
|
|
|
+ // Determine scale
|
|
|
|
|
+ let conversionFactor = additionalState.unit == "mmol/L" ? 0.0555 : 1
|
|
|
|
|
+ let min = (additionalState.chart.min() ?? 40 * conversionFactor) - 20 * conversionFactor
|
|
|
|
|
+ let max = (additionalState.chart.max() ?? 270 * conversionFactor) + 50 * conversionFactor
|
|
|
|
|
|
|
|
Chart {
|
|
Chart {
|
|
|
- RuleMark(y: .value("high", context.state.highGlucose))
|
|
|
|
|
|
|
+ RuleMark(y: .value("High", additionalState.highGlucose))
|
|
|
.lineStyle(.init(lineWidth: 0.5, dash: [5]))
|
|
.lineStyle(.init(lineWidth: 0.5, dash: [5]))
|
|
|
- RuleMark(y: .value("low", context.state.lowGlucose))
|
|
|
|
|
|
|
+ RuleMark(y: .value("Low", additionalState.lowGlucose))
|
|
|
.lineStyle(.init(lineWidth: 0.5, dash: [5]))
|
|
.lineStyle(.init(lineWidth: 0.5, dash: [5]))
|
|
|
- ForEach(context.state.chart.indices, id: \.self) { index in
|
|
|
|
|
- let currentValue = context.state.chart[index]
|
|
|
|
|
- if currentValue > context.state.highGlucose {
|
|
|
|
|
- PointMark(
|
|
|
|
|
- x: .value("Time", context.state.chartDate[index] ?? Date()),
|
|
|
|
|
- y: .value("Value", currentValue)
|
|
|
|
|
- ).foregroundStyle(Color.orange.gradient).symbolSize(15)
|
|
|
|
|
- } else if currentValue < context.state.lowGlucose {
|
|
|
|
|
- PointMark(
|
|
|
|
|
- x: .value("Time", context.state.chartDate[index] ?? Date()),
|
|
|
|
|
- y: .value("Value", currentValue)
|
|
|
|
|
- ).foregroundStyle(Color.red.gradient).symbolSize(15)
|
|
|
|
|
|
|
+
|
|
|
|
|
+ ForEach(additionalState.chart.indices, id: \.self) { index in
|
|
|
|
|
+ let currentValue = additionalState.chart[index]
|
|
|
|
|
+ let chartDate = additionalState.chartDate[index] ?? Date()
|
|
|
|
|
+ let pointMark = PointMark(
|
|
|
|
|
+ x: .value("Time", chartDate),
|
|
|
|
|
+ y: .value("Value", currentValue)
|
|
|
|
|
+ ).symbolSize(15)
|
|
|
|
|
+
|
|
|
|
|
+ if currentValue > additionalState.highGlucose {
|
|
|
|
|
+ pointMark.foregroundStyle(Color.orange.gradient)
|
|
|
|
|
+ } else if currentValue < additionalState.lowGlucose {
|
|
|
|
|
+ pointMark.foregroundStyle(Color.red.gradient)
|
|
|
} else {
|
|
} else {
|
|
|
- PointMark(
|
|
|
|
|
- x: .value("Time", context.state.chartDate[index] ?? Date()),
|
|
|
|
|
- y: .value("Value", currentValue)
|
|
|
|
|
- ).foregroundStyle(Color.green.gradient).symbolSize(15)
|
|
|
|
|
|
|
+ pointMark.foregroundStyle(Color.green.gradient)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -272,96 +263,203 @@ struct LiveActivity: Widget {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- var body: some WidgetConfiguration {
|
|
|
|
|
- ActivityConfiguration(for: LiveActivityAttributes.self) { context in
|
|
|
|
|
- // Lock screen/banner UI goes here
|
|
|
|
|
- if context.state.lockScreenView == "Simple" {
|
|
|
|
|
- HStack(spacing: 3) {
|
|
|
|
|
- bgAndTrend(context: context, size: .expanded).0.font(.title)
|
|
|
|
|
|
|
+ @ViewBuilder func content(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
|
|
|
|
|
+ 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()
|
|
Spacer()
|
|
|
- VStack(alignment: .trailing, spacing: 5) {
|
|
|
|
|
- changeLabel(context: context).font(.title3)
|
|
|
|
|
- updatedLabel(context: context).font(.caption).foregroundStyle(.primary.opacity(0.7))
|
|
|
|
|
|
|
+ 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, 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)
|
|
|
|
|
- } else {
|
|
|
|
|
- HStack(spacing: 12) {
|
|
|
|
|
- chart(context: context).frame(maxWidth: UIScreen.main.bounds.width / 1.8)
|
|
|
|
|
- VStack(alignment: .leading) {
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ .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()
|
|
Spacer()
|
|
|
- bgLabel(context: context)
|
|
|
|
|
- HStack {
|
|
|
|
|
- changeLabel(context: context)
|
|
|
|
|
- bobble(context: context)
|
|
|
|
|
|
|
+ VStack {
|
|
|
|
|
+ Spacer()
|
|
|
|
|
+ expiredLabel()
|
|
|
|
|
+ Spacer()
|
|
|
}
|
|
}
|
|
|
- mealLabel(context: context).padding(.bottom, 8)
|
|
|
|
|
- updatedLabel(context: context).padding(.bottom, 10)
|
|
|
|
|
|
|
+ Spacer()
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
- .privacySensitive()
|
|
|
|
|
- .padding(.all, 14)
|
|
|
|
|
- .imageScale(.small)
|
|
|
|
|
- .foregroundColor(Color.white)
|
|
|
|
|
- .activityBackgroundTint(Color.black.opacity(0.8))
|
|
|
|
|
- }
|
|
|
|
|
- } dynamicIsland: { context in
|
|
|
|
|
- DynamicIsland {
|
|
|
|
|
- // Expanded UI goes here. Compose the expanded UI through
|
|
|
|
|
- // various regions, like leading/trailing/center/bottom
|
|
|
|
|
- DynamicIslandExpandedRegion(.leading) {
|
|
|
|
|
- bgAndTrend(context: context, size: .expanded).0.font(.title2).padding(.leading, 5)
|
|
|
|
|
- }
|
|
|
|
|
- DynamicIslandExpandedRegion(.trailing) {
|
|
|
|
|
- changeLabel(context: context).font(.title2).padding(.trailing, 5)
|
|
|
|
|
- }
|
|
|
|
|
- DynamicIslandExpandedRegion(.bottom) {
|
|
|
|
|
- if context.state.lockScreenView == "Simple" {
|
|
|
|
|
- Group {
|
|
|
|
|
- updatedLabel(context: context).font(.caption).foregroundStyle(Color.secondary)
|
|
|
|
|
|
|
+ } else {
|
|
|
|
|
+ HStack(spacing: 3) {
|
|
|
|
|
+ bgAndTrend(context: context, size: .expanded).0.font(.title)
|
|
|
|
|
+ Spacer()
|
|
|
|
|
+ VStack(alignment: .trailing, spacing: 5) {
|
|
|
|
|
+ changeLabel(context: context).font(.title3)
|
|
|
|
|
+ updatedLabel(context: context).font(.caption).foregroundStyle(.primary.opacity(0.7))
|
|
|
}
|
|
}
|
|
|
- .frame(
|
|
|
|
|
- maxHeight: .infinity,
|
|
|
|
|
- alignment: .bottom
|
|
|
|
|
- )
|
|
|
|
|
- } else {
|
|
|
|
|
- chart(context: context)
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- DynamicIslandExpandedRegion(.center) {
|
|
|
|
|
- if context.state.lockScreenView == "Detailed" {
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ .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 {
|
|
|
|
|
+ DynamicIsland {
|
|
|
|
|
+ DynamicIslandExpandedRegion(.leading) {
|
|
|
|
|
+ bgAndTrend(context: context, size: .expanded).0.font(.title2).padding(.leading, 5)
|
|
|
|
|
+ }
|
|
|
|
|
+ DynamicIslandExpandedRegion(.trailing) {
|
|
|
|
|
+ changeLabel(context: context).font(.title2).padding(.trailing, 5)
|
|
|
|
|
+ }
|
|
|
|
|
+ DynamicIslandExpandedRegion(.bottom) {
|
|
|
|
|
+ if context.state.isInitialState {
|
|
|
|
|
+ expiredLabel()
|
|
|
|
|
+ } else if let detailedViewState = context.state.detailedViewState {
|
|
|
|
|
+ chart(context: context, additionalState: detailedViewState)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ Group {
|
|
|
updatedLabel(context: context).font(.caption).foregroundStyle(Color.secondary)
|
|
updatedLabel(context: context).font(.caption).foregroundStyle(Color.secondary)
|
|
|
}
|
|
}
|
|
|
|
|
+ .frame(
|
|
|
|
|
+ maxHeight: .infinity,
|
|
|
|
|
+ alignment: .bottom
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
- } compactLeading: {
|
|
|
|
|
- bgAndTrend(context: context, size: .compact).0.padding(.leading, 4)
|
|
|
|
|
- } compactTrailing: {
|
|
|
|
|
- changeLabel(context: context).padding(.trailing, 4)
|
|
|
|
|
- } minimal: {
|
|
|
|
|
- let (_label, characterCount) = bgAndTrend(context: context, size: .minimal)
|
|
|
|
|
-
|
|
|
|
|
- let label = _label.padding(.leading, 7).padding(.trailing, 3)
|
|
|
|
|
-
|
|
|
|
|
- if characterCount < 4 {
|
|
|
|
|
- label
|
|
|
|
|
- } else if characterCount < 5 {
|
|
|
|
|
- label.fontWidth(.condensed)
|
|
|
|
|
- } else {
|
|
|
|
|
- label.fontWidth(.compressed)
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ DynamicIslandExpandedRegion(.center) {
|
|
|
|
|
+ if context.state.detailedViewState != nil {
|
|
|
|
|
+ updatedLabel(context: context).font(.caption).foregroundStyle(Color.secondary)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- .widgetURL(URL(string: "freeaps-x://"))
|
|
|
|
|
- .keylineTint(Color.purple)
|
|
|
|
|
- .contentMargins(.horizontal, 0, for: .minimal)
|
|
|
|
|
- .contentMargins(.trailing, 0, for: .compactLeading)
|
|
|
|
|
- .contentMargins(.leading, 0, for: .compactTrailing)
|
|
|
|
|
|
|
+ } compactLeading: {
|
|
|
|
|
+ bgAndTrend(context: context, size: .compact).0.padding(.leading, 4)
|
|
|
|
|
+ } compactTrailing: {
|
|
|
|
|
+ changeLabel(context: context).padding(.trailing, 4)
|
|
|
|
|
+ } minimal: {
|
|
|
|
|
+ let (_label, characterCount) = bgAndTrend(context: context, size: .minimal)
|
|
|
|
|
+ let label = _label.padding(.leading, 7).padding(.trailing, 3)
|
|
|
|
|
+
|
|
|
|
|
+ if characterCount < 4 {
|
|
|
|
|
+ label
|
|
|
|
|
+ } else if characterCount < 5 {
|
|
|
|
|
+ label.fontWidth(.condensed)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ label.fontWidth(.compressed)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+ .widgetURL(URL(string: "Trio://"))
|
|
|
|
|
+ .keylineTint(Color.purple)
|
|
|
|
|
+ .contentMargins(.horizontal, 0, for: .minimal)
|
|
|
|
|
+ .contentMargins(.trailing, 0, for: .compactLeading)
|
|
|
|
|
+ .contentMargins(.leading, 0, for: .compactTrailing)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var body: some WidgetConfiguration {
|
|
|
|
|
+ ActivityConfiguration(for: LiveActivityAttributes.self, content: self.content, dynamicIsland: self.dynamicIsland)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+private extension LiveActivityAttributes {
|
|
|
|
|
+ static var preview: LiveActivityAttributes {
|
|
|
|
|
+ LiveActivityAttributes(startDate: Date())
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+private extension LiveActivityAttributes.ContentState {
|
|
|
|
|
+ // 0 is the widest digit. Use this to get an upper bound on text width.
|
|
|
|
|
+
|
|
|
|
|
+ // Use mmol/l notation with decimal point as well for the same reason, it uses up to 4 characters, while mg/dl uses up to 3
|
|
|
|
|
+ static var testWide: LiveActivityAttributes.ContentState {
|
|
|
|
|
+ LiveActivityAttributes.ContentState(
|
|
|
|
|
+ bg: "00.0",
|
|
|
|
|
+ direction: "→",
|
|
|
|
|
+ change: "+0.0",
|
|
|
|
|
+ date: Date(),
|
|
|
|
|
+ detailedViewState: nil,
|
|
|
|
|
+ isInitialState: false
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static var testVeryWide: LiveActivityAttributes.ContentState {
|
|
|
|
|
+ LiveActivityAttributes.ContentState(
|
|
|
|
|
+ bg: "00.0",
|
|
|
|
|
+ direction: "↑↑",
|
|
|
|
|
+ change: "+0.0",
|
|
|
|
|
+ date: Date(),
|
|
|
|
|
+ detailedViewState: nil,
|
|
|
|
|
+ isInitialState: false
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static var testSuperWide: LiveActivityAttributes.ContentState {
|
|
|
|
|
+ LiveActivityAttributes.ContentState(
|
|
|
|
|
+ bg: "00.0",
|
|
|
|
|
+ direction: "↑↑↑",
|
|
|
|
|
+ change: "+0.0",
|
|
|
|
|
+ date: Date(),
|
|
|
|
|
+ detailedViewState: nil,
|
|
|
|
|
+ isInitialState: false
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2 characters for BG, 1 character for change is the minimum that will be shown
|
|
|
|
|
+ static var testNarrow: LiveActivityAttributes.ContentState {
|
|
|
|
|
+ LiveActivityAttributes.ContentState(
|
|
|
|
|
+ bg: "00",
|
|
|
|
|
+ direction: "↑",
|
|
|
|
|
+ change: "+0",
|
|
|
|
|
+ date: Date(),
|
|
|
|
|
+ detailedViewState: nil,
|
|
|
|
|
+ isInitialState: false
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static var testMedium: LiveActivityAttributes.ContentState {
|
|
|
|
|
+ LiveActivityAttributes.ContentState(
|
|
|
|
|
+ bg: "000",
|
|
|
|
|
+ direction: "↗︎",
|
|
|
|
|
+ change: "+00",
|
|
|
|
|
+ date: Date(),
|
|
|
|
|
+ detailedViewState: nil,
|
|
|
|
|
+ isInitialState: false
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ static var testExpired: LiveActivityAttributes.ContentState {
|
|
|
|
|
+ LiveActivityAttributes.ContentState(
|
|
|
|
|
+ bg: "--",
|
|
|
|
|
+ direction: nil,
|
|
|
|
|
+ change: "--",
|
|
|
|
|
+ date: Date().addingTimeInterval(-60 * 60),
|
|
|
|
|
+ detailedViewState: nil,
|
|
|
|
|
+ isInitialState: true
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@available(iOS 17.0, iOSApplicationExtension 17.0, *)
|
|
|
|
|
+#Preview("Notification", as: .content, using: LiveActivityAttributes.preview) {
|
|
|
|
|
+ LiveActivity()
|
|
|
|
|
+} contentStates: {
|
|
|
|
|
+ LiveActivityAttributes.ContentState.testSuperWide
|
|
|
|
|
+ LiveActivityAttributes.ContentState.testVeryWide
|
|
|
|
|
+ LiveActivityAttributes.ContentState.testWide
|
|
|
|
|
+ LiveActivityAttributes.ContentState.testMedium
|
|
|
|
|
+ LiveActivityAttributes.ContentState.testNarrow
|
|
|
|
|
+ LiveActivityAttributes.ContentState.testExpired
|
|
|
}
|
|
}
|