LiveActivity.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. import ActivityKit
  2. import Charts
  3. import SwiftUI
  4. import WidgetKit
  5. struct LiveActivity: Widget {
  6. let dateFormatter: DateFormatter = {
  7. var f = DateFormatter()
  8. f.dateStyle = .none
  9. f.timeStyle = .short
  10. return f
  11. }()
  12. private var bolusFormatter: NumberFormatter {
  13. let formatter = NumberFormatter()
  14. formatter.numberStyle = .decimal
  15. formatter.maximumFractionDigits = 2
  16. formatter.decimalSeparator = "."
  17. return formatter
  18. }
  19. private var carbsFormatter: NumberFormatter {
  20. let formatter = NumberFormatter()
  21. formatter.numberStyle = .decimal
  22. formatter.maximumFractionDigits = 0
  23. return formatter
  24. }
  25. func changeLabel(context: ActivityViewContext<LiveActivityAttributes>) -> Text {
  26. if !context.isStale && !context.state.change.isEmpty {
  27. Text(context.state.change)
  28. } else {
  29. Text("--")
  30. }
  31. }
  32. func updatedLabel(context: ActivityViewContext<LiveActivityAttributes>) -> Text {
  33. Text("Updated: \(dateFormatter.string(from: context.state.date))").italic()
  34. }
  35. func bgLabel(context: ActivityViewContext<LiveActivityAttributes>) -> Text {
  36. if context.isStale {
  37. Text("--")
  38. } else {
  39. Text(context.state.bg).fontWeight(.bold)
  40. }
  41. }
  42. @ViewBuilder func mealLabel(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
  43. VStack(alignment: .leading, spacing: 1, content: {
  44. HStack {
  45. Text("COB: ").font(.caption)
  46. Text(
  47. (carbsFormatter.string(from: context.state.cob as NSNumber) ?? "--") +
  48. NSLocalizedString(" g", comment: "grams of carbs")
  49. ).font(.caption).fontWeight(.bold)
  50. }
  51. HStack {
  52. Text("IOB: ").font(.caption)
  53. Text(
  54. (bolusFormatter.string(from: context.state.iob as NSNumber) ?? "--") +
  55. NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)")
  56. ).font(.caption).fontWeight(.bold)
  57. }
  58. })
  59. }
  60. @ViewBuilder func trend(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
  61. if context.isStale {
  62. Text("--")
  63. } else {
  64. if let trendSystemImage = context.state.trendSystemImage {
  65. Image(systemName: trendSystemImage)
  66. }
  67. }
  68. }
  69. @ViewBuilder func bgAndTrend(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
  70. if context.isStale {
  71. Text("--")
  72. } else {
  73. HStack {
  74. Text(context.state.bg).fontWeight(.bold)
  75. if let trendSystemImage = context.state.trendSystemImage {
  76. Image(systemName: trendSystemImage)
  77. }
  78. }
  79. }
  80. }
  81. @ViewBuilder func bobble(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
  82. @State var angularGradient = AngularGradient(colors: [
  83. Color(red: 0.7215686275, green: 0.3411764706, blue: 1),
  84. Color(red: 0.6235294118, green: 0.4235294118, blue: 0.9803921569),
  85. Color(red: 0.4862745098, green: 0.5450980392, blue: 0.9529411765),
  86. Color(red: 0.3411764706, green: 0.6666666667, blue: 0.9254901961),
  87. Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902),
  88. Color(red: 0.7215686275, green: 0.3411764706, blue: 1)
  89. ], center: .center, startAngle: .degrees(270), endAngle: .degrees(-90))
  90. let triangleColor = Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902)
  91. WidgetBobble(gradient: angularGradient, color: triangleColor)
  92. .rotationEffect(.degrees(context.state.rotationDegrees))
  93. }
  94. @ViewBuilder func chart(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
  95. if context.isStale {
  96. Text("No data available")
  97. } else {
  98. Chart {
  99. ForEach(context.state.chart.indices, id: \.self) { index in
  100. let currentValue = context.state.chart[index]
  101. if currentValue > context.state.highGlucose {
  102. PointMark(
  103. x: .value("Time", context.state.chartDate[index] ?? Date()),
  104. y: .value("Value", currentValue)
  105. ).foregroundStyle(Color.orange.gradient).symbolSize(12)
  106. } else if currentValue < context.state.lowGlucose {
  107. PointMark(
  108. x: .value("Time", context.state.chartDate[index] ?? Date()),
  109. y: .value("Value", currentValue)
  110. ).foregroundStyle(Color.red.gradient).symbolSize(12)
  111. } else {
  112. PointMark(
  113. x: .value("Time", context.state.chartDate[index] ?? Date()),
  114. y: .value("Value", currentValue)
  115. ).foregroundStyle(Color.green.gradient).symbolSize(12)
  116. }
  117. }
  118. }.chartPlotStyle { plotContent in
  119. plotContent.background(.cyan.opacity(0.1))
  120. }
  121. .chartYAxis {
  122. AxisMarks(position: .leading) { _ in
  123. AxisValueLabel().foregroundStyle(Color.white)
  124. AxisGridLine(stroke: .init(lineWidth: 0.1, dash: [2, 3])).foregroundStyle(Color.white)
  125. }
  126. }
  127. .chartXAxis {
  128. AxisMarks(position: .automatic) { _ in
  129. AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
  130. .foregroundStyle(Color.white)
  131. AxisGridLine(stroke: .init(lineWidth: 0.1, dash: [2, 3])).foregroundStyle(Color.white)
  132. }
  133. }
  134. }
  135. }
  136. var body: some WidgetConfiguration {
  137. ActivityConfiguration(for: LiveActivityAttributes.self) { context in
  138. // Lock screen/banner UI goes here
  139. if context.state.lockScreenView == "Simple" {
  140. HStack(spacing: 3) {
  141. bgAndTrend(context: context).font(.title)
  142. Spacer()
  143. VStack(alignment: .trailing, spacing: 5) {
  144. changeLabel(context: context).font(.title3)
  145. updatedLabel(context: context).font(.caption).foregroundStyle(.black.opacity(0.7))
  146. }
  147. }
  148. .privacySensitive()
  149. .imageScale(.small)
  150. .padding(.all, 15)
  151. .background(Color.white.opacity(0.2))
  152. .foregroundColor(Color.black)
  153. .activityBackgroundTint(Color.cyan.opacity(0.2))
  154. .activitySystemActionForegroundColor(Color.black)
  155. } else {
  156. HStack(spacing: 2) {
  157. VStack {
  158. chart(context: context).frame(width: UIScreen.main.bounds.width / 1.8)
  159. }.padding(.all, 15)
  160. Divider().foregroundStyle(Color.white)
  161. VStack(alignment: .center) {
  162. Spacer()
  163. ZStack {
  164. bobble(context: context)
  165. .scaleEffect(0.6)
  166. .clipped()
  167. VStack {
  168. bgLabel(context: context).font(.title2).imageScale(.small)
  169. changeLabel(context: context).font(.callout)
  170. }
  171. }.scaleEffect(0.85).offset(y: 15)
  172. mealLabel(context: context).padding(.bottom, 8)
  173. updatedLabel(context: context).font(.caption).padding(.bottom, 50)
  174. }
  175. }
  176. .privacySensitive()
  177. .imageScale(.small)
  178. .background(Color.white.opacity(0.2))
  179. .foregroundColor(Color.white)
  180. .activityBackgroundTint(Color.black.opacity(0.7))
  181. .activitySystemActionForegroundColor(Color.white)
  182. }
  183. } dynamicIsland: { context in
  184. DynamicIsland {
  185. // Expanded UI goes here. Compose the expanded UI through
  186. // various regions, like leading/trailing/center/bottom
  187. DynamicIslandExpandedRegion(.leading) {
  188. HStack(spacing: 3) {
  189. bgAndTrend(context: context)
  190. }.imageScale(.small).font(.title).padding(.leading, 5)
  191. }
  192. DynamicIslandExpandedRegion(.trailing) {
  193. changeLabel(context: context).font(.title).padding(.trailing, 5)
  194. }
  195. DynamicIslandExpandedRegion(.bottom) {
  196. chart(context: context)
  197. }
  198. DynamicIslandExpandedRegion(.center) {
  199. updatedLabel(context: context).font(.caption).foregroundStyle(Color.secondary)
  200. }
  201. } compactLeading: {
  202. HStack(spacing: 1) {
  203. bgAndTrend(context: context)
  204. }.bold().imageScale(.small).padding(.leading, 5)
  205. } compactTrailing: {
  206. changeLabel(context: context).padding(.trailing, 5)
  207. } minimal: {
  208. bgLabel(context: context).bold()
  209. }
  210. .widgetURL(URL(string: "freeaps-x://"))
  211. .keylineTint(Color.cyan.opacity(0.5))
  212. }
  213. }
  214. }
  215. // private extension LiveActivityAttributes {
  216. // static var preview: LiveActivityAttributes {
  217. // LiveActivityAttributes(startDate: Date())
  218. // }
  219. // }
  220. //
  221. // private extension LiveActivityAttributes.ContentState {
  222. // static var test: LiveActivityAttributes.ContentState {
  223. // LiveActivityAttributes.ContentState(bg: "100", trendSystemImage: "arrow.right", change: "+2", date: Date())
  224. // }
  225. // }
  226. //
  227. // #Preview("Notification", as: .content, using: LiveActivityAttributes.preview) {
  228. // LiveActivity()
  229. // } contentStates: {
  230. // LiveActivityAttributes.ContentState.test
  231. // }