import ActivityKit import Charts import SwiftUI import WidgetKit struct LiveActivity: Widget { 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 } func changeLabel(context: ActivityViewContext) -> Text { if !context.isStale && !context.state.change.isEmpty { Text(context.state.change) } else { Text("--") } } func updatedLabel(context: ActivityViewContext) -> Text { Text("Updated: \(dateFormatter.string(from: context.state.date))").italic() } func bgLabel(context: ActivityViewContext) -> Text { if context.isStale { Text("--") } else { Text(context.state.bg).fontWeight(.bold) } } @ViewBuilder func mealLabel(context: ActivityViewContext) -> some View { VStack(alignment: .leading, spacing: 1, content: { HStack { Text("COB: ").font(.caption) Text( (carbsFormatter.string(from: context.state.cob as NSNumber) ?? "--") + NSLocalizedString(" g", comment: "grams of carbs") ).font(.caption).fontWeight(.bold) } HStack { Text("IOB: ").font(.caption) Text( (bolusFormatter.string(from: context.state.iob as NSNumber) ?? "--") + NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)") ).font(.caption).fontWeight(.bold) } }) } @ViewBuilder func trend(context: ActivityViewContext) -> some View { if context.isStale { Text("--") } else { if let trendSystemImage = context.state.trendSystemImage { Image(systemName: trendSystemImage) } } } @ViewBuilder func bgAndTrend(context: ActivityViewContext) -> some View { if context.isStale { Text("--") } else { HStack { Text(context.state.bg).fontWeight(.bold) if let trendSystemImage = context.state.trendSystemImage { Image(systemName: trendSystemImage) } } } } @ViewBuilder func bobble(context: ActivityViewContext) -> some View { @State var angularGradient = AngularGradient(colors: [ Color(red: 0.7215686275, green: 0.3411764706, blue: 1), 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), Color(red: 0.7215686275, green: 0.3411764706, blue: 1) ], center: .center, startAngle: .degrees(270), endAngle: .degrees(-90)) let triangleColor = Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902) WidgetBobble(gradient: angularGradient, color: triangleColor) .rotationEffect(.degrees(context.state.rotationDegrees)) } @ViewBuilder func chart(context: ActivityViewContext) -> some View { if context.isStale { Text("No data available") } else { Chart { 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(12) } else if currentValue < context.state.lowGlucose { PointMark( x: .value("Time", context.state.chartDate[index] ?? Date()), y: .value("Value", currentValue) ).foregroundStyle(Color.red.gradient).symbolSize(12) } else { PointMark( x: .value("Time", context.state.chartDate[index] ?? Date()), y: .value("Value", currentValue) ).foregroundStyle(Color.green.gradient).symbolSize(12) } } }.chartPlotStyle { plotContent in plotContent.background(.cyan.opacity(0.1)) } .chartYAxis { AxisMarks(position: .leading) { _ in AxisValueLabel().foregroundStyle(Color.white) AxisGridLine(stroke: .init(lineWidth: 0.1, dash: [2, 3])).foregroundStyle(Color.white) } } .chartXAxis { AxisMarks(position: .automatic) { _ in AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top) .foregroundStyle(Color.white) AxisGridLine(stroke: .init(lineWidth: 0.1, dash: [2, 3])).foregroundStyle(Color.white) } } } } 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).font(.title) Spacer() VStack(alignment: .trailing, spacing: 5) { changeLabel(context: context).font(.title3) updatedLabel(context: context).font(.caption).foregroundStyle(.black.opacity(0.7)) } } .privacySensitive() .imageScale(.small) .padding(.all, 15) .background(Color.white.opacity(0.2)) .foregroundColor(Color.black) .activityBackgroundTint(Color.cyan.opacity(0.2)) .activitySystemActionForegroundColor(Color.black) } else { HStack(spacing: 2) { VStack { chart(context: context).frame(width: UIScreen.main.bounds.width / 1.8) }.padding(.all, 15) Divider().foregroundStyle(Color.white) VStack(alignment: .center) { Spacer() ZStack { bobble(context: context) .scaleEffect(0.6) .clipped() VStack { bgLabel(context: context).font(.title2).imageScale(.small) changeLabel(context: context).font(.callout) } }.scaleEffect(0.85).offset(y: 15) mealLabel(context: context).padding(.bottom, 8) updatedLabel(context: context).font(.caption).padding(.bottom, 50) } } .privacySensitive() .imageScale(.small) .background(Color.white.opacity(0.2)) .foregroundColor(Color.white) .activityBackgroundTint(Color.black.opacity(0.7)) .activitySystemActionForegroundColor(Color.white) } } dynamicIsland: { context in DynamicIsland { // Expanded UI goes here. Compose the expanded UI through // various regions, like leading/trailing/center/bottom DynamicIslandExpandedRegion(.leading) { HStack(spacing: 3) { bgAndTrend(context: context) }.imageScale(.small).font(.title).padding(.leading, 5) } DynamicIslandExpandedRegion(.trailing) { changeLabel(context: context).font(.title).padding(.trailing, 5) } DynamicIslandExpandedRegion(.bottom) { chart(context: context) } DynamicIslandExpandedRegion(.center) { updatedLabel(context: context).font(.caption).foregroundStyle(Color.secondary) } } compactLeading: { HStack(spacing: 1) { bgAndTrend(context: context) }.bold().imageScale(.small).padding(.leading, 5) } compactTrailing: { changeLabel(context: context).padding(.trailing, 5) } minimal: { bgLabel(context: context).bold() } .widgetURL(URL(string: "freeaps-x://")) .keylineTint(Color.cyan.opacity(0.5)) } } } // private extension LiveActivityAttributes { // static var preview: LiveActivityAttributes { // LiveActivityAttributes(startDate: Date()) // } // } // // private extension LiveActivityAttributes.ContentState { // static var test: LiveActivityAttributes.ContentState { // LiveActivityAttributes.ContentState(bg: "100", trendSystemImage: "arrow.right", change: "+2", date: Date()) // } // } // // #Preview("Notification", as: .content, using: LiveActivityAttributes.preview) { // LiveActivity() // } contentStates: { // LiveActivityAttributes.ContentState.test // }