OverrideSelectionHistory.swift 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. //
  2. // OverrideSelectionHistory.swift
  3. // LoopUI
  4. //
  5. // Created by Anna Quinlan on 8/1/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import SwiftUI
  9. import LoopKit
  10. import HealthKit
  11. public class OverrideHistoryViewModel: ObservableObject {
  12. var overrides: [TemporaryScheduleOverride]
  13. var glucoseUnit: HKUnit
  14. var didEditOverride: ((TemporaryScheduleOverride) -> Void)?
  15. var didDeleteOverride: ((TemporaryScheduleOverride) -> Void)?
  16. public init(
  17. overrides: [TemporaryScheduleOverride],
  18. glucoseUnit: HKUnit
  19. ) {
  20. self.overrides = overrides
  21. self.glucoseUnit = glucoseUnit
  22. }
  23. }
  24. public struct OverrideSelectionHistory: View {
  25. @ObservedObject var model: OverrideHistoryViewModel
  26. private var quantityFormatter: QuantityFormatter
  27. private var glucoseNumberFormatter: NumberFormatter
  28. private var durationFormatter: DateComponentsFormatter
  29. public init(model: OverrideHistoryViewModel) {
  30. self.model = model
  31. self.quantityFormatter = {
  32. let quantityFormatter = QuantityFormatter()
  33. quantityFormatter.setPreferredNumberFormatter(for: model.glucoseUnit)
  34. return quantityFormatter
  35. }()
  36. self.glucoseNumberFormatter = quantityFormatter.numberFormatter
  37. self.durationFormatter = {
  38. let formatter = DateComponentsFormatter()
  39. formatter.allowedUnits = [.hour, .minute]
  40. formatter.unitsStyle = .short
  41. return formatter
  42. }()
  43. }
  44. // Style conditionally based on iOS so we get a grouped list style
  45. public var body: some View {
  46. #if swift(>=5.2)
  47. if #available(iOS 14.0, *) {
  48. bodyContents
  49. .listStyle(InsetGroupedListStyle())
  50. } else {
  51. bodyContents
  52. .listStyle(GroupedListStyle())
  53. .environment(\.horizontalSizeClass, .regular)
  54. }
  55. #else
  56. bodyContents
  57. .listStyle(GroupedListStyle())
  58. .environment(\.horizontalSizeClass, .regular)
  59. #endif
  60. }
  61. private var bodyContents: some View {
  62. List {
  63. ForEach(model.overrides, id: \.self) { override in
  64. Group {
  65. // Don't show overrides in history that were never active
  66. if override.actualEnd != .deleted {
  67. Section {
  68. NavigationLink(destination: self.detailView(for: override)) {
  69. self.createCell(for: override)
  70. .padding(EdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 0))
  71. }
  72. }
  73. }
  74. }
  75. }
  76. }
  77. .navigationBarTitle(Text(LocalizedString("Override History", comment: "Title for override history view")), displayMode: .large)
  78. }
  79. private func makeTargetRangeText(from targetRange: ClosedRange<HKQuantity>) -> String {
  80. guard
  81. let minTarget = glucoseNumberFormatter.string(from: targetRange.lowerBound.doubleValue(for: model.glucoseUnit)),
  82. let maxTarget = glucoseNumberFormatter.string(from: targetRange.upperBound.doubleValue(for: model.glucoseUnit))
  83. else {
  84. return ""
  85. }
  86. return String(format: LocalizedString("%1$@ – %2$@ %3$@", comment: "The format for a glucose target range. (1: min target)(2: max target)(3: glucose unit)"), minTarget, maxTarget, quantityFormatter.string(from: model.glucoseUnit))
  87. }
  88. private func createCell(for override: TemporaryScheduleOverride) -> OverrideViewCell {
  89. let startTime = DateFormatter.localizedString(from: override.startDate, dateStyle: .none, timeStyle: .short)
  90. var targetRange: String = ""
  91. if let range = override.settings.targetRange {
  92. targetRange = makeTargetRangeText(from: range)
  93. }
  94. var duration: String {
  95. // Don't use the durationFormatter if the interval is infinite
  96. if !override.duration.isFinite && override.scheduledEndDate == override.actualEndDate {
  97. return "∞"
  98. }
  99. return durationFormatter.string(from: override.startDate, to: override.actualEndDate)!
  100. }
  101. let insulinNeeds = override.settings.insulinNeedsScaleFactor
  102. switch override.context {
  103. case .legacyWorkout:
  104. return OverrideViewCell(
  105. symbol: Text("🏃‍♂️"),
  106. name: Text("Workout", comment: "Title for workout override history cell"),
  107. targetRange: Text(targetRange),
  108. duration: Text(duration),
  109. subtitle: Text(startTime),
  110. insulinNeedsScaleFactor: insulinNeeds)
  111. case .preMeal:
  112. return OverrideViewCell(
  113. symbol: Text("🍽"),
  114. name: Text("Pre-Meal", comment: "Title for pre-meal override history cell"),
  115. targetRange: Text(targetRange),
  116. duration: Text(duration),
  117. subtitle: Text(startTime),
  118. insulinNeedsScaleFactor: insulinNeeds)
  119. case .preset(let preset):
  120. return OverrideViewCell(
  121. symbol: Text(preset.symbol),
  122. name: Text(preset.name),
  123. targetRange: Text(targetRange),
  124. duration: Text(duration),
  125. subtitle: Text(startTime),
  126. insulinNeedsScaleFactor: insulinNeeds)
  127. case .custom:
  128. return OverrideViewCell(
  129. symbol: Text("···"),
  130. name: Text("Custom", comment: "Title for custom override history cell"),
  131. targetRange: Text(targetRange),
  132. duration: Text(duration),
  133. subtitle: Text(startTime),
  134. insulinNeedsScaleFactor: insulinNeeds)
  135. }
  136. }
  137. private func title(for override: TemporaryScheduleOverride) -> String {
  138. switch override.context {
  139. case .legacyWorkout:
  140. return LocalizedString("🏃‍♂️ Workout", comment: "Workout override preset title")
  141. case .preMeal:
  142. return LocalizedString("🍽 Pre-Meal", comment: "Premeal override preset title")
  143. case .preset(let preset):
  144. let symbol = preset.symbol
  145. let name = preset.name
  146. let format = LocalizedString("%1$@ %2$@", comment: "The format for an override symbol and name (1: symbol)(2: name)")
  147. return String(format: format, symbol, name)
  148. case .custom:
  149. return LocalizedString("Custom Override", comment: "Custom override preset title")
  150. }
  151. }
  152. private func detailView(for override: TemporaryScheduleOverride) -> some View {
  153. let editorTitle = title(for: override)
  154. return HistoricalOverrideDetailView(
  155. override: override,
  156. glucoseUnit: model.glucoseUnit,
  157. delegate: nil
  158. ).navigationBarTitle(editorTitle)
  159. }
  160. }