MasterViewController.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. //
  2. // MasterViewController.swift
  3. // LoopKit Example
  4. //
  5. // Created by Nathan Racklyeft on 2/24/16.
  6. // Copyright © 2016 Nathan Racklyeft. All rights reserved.
  7. //
  8. import UIKit
  9. import LoopKit
  10. import LoopKitUI
  11. import HealthKit
  12. class MasterViewController: UITableViewController {
  13. private var dataManager: DeviceDataManager?
  14. override func viewDidLoad() {
  15. super.viewDidLoad()
  16. if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil {
  17. dataManager = DeviceDataManager()
  18. }
  19. }
  20. override func viewDidAppear(_ animated: Bool) {
  21. super.viewDidAppear(animated)
  22. guard let dataManager = dataManager else {
  23. return
  24. }
  25. let sampleTypes = Set([
  26. dataManager.glucoseStore.sampleType,
  27. dataManager.carbStore.sampleType,
  28. dataManager.doseStore.sampleType,
  29. ].compactMap { $0 })
  30. if dataManager.glucoseStore.authorizationRequired ||
  31. dataManager.carbStore.authorizationRequired ||
  32. dataManager.doseStore.authorizationRequired
  33. {
  34. dataManager.carbStore.healthStore.requestAuthorization(toShare: sampleTypes, read: sampleTypes) { (success, error) in
  35. if success {
  36. // Call the individual authorization methods to trigger query creation
  37. dataManager.carbStore.authorize({ _ in })
  38. dataManager.doseStore.insulinDeliveryStore.authorize(toShare: true, { _ in })
  39. dataManager.glucoseStore.authorize({ _ in })
  40. }
  41. }
  42. }
  43. }
  44. // MARK: - Data Source
  45. private enum Section: Int, CaseIterable {
  46. case data
  47. case configuration
  48. }
  49. private enum DataRow: Int, CaseIterable {
  50. case carbs = 0
  51. case reservoir
  52. case diagnostic
  53. case generate
  54. case reset
  55. }
  56. private enum ConfigurationRow: Int, CaseIterable {
  57. case basalRate
  58. case carbRatio
  59. case correctionRange
  60. case insulinSensitivity
  61. case pumpID
  62. }
  63. // MARK: UITableViewDataSource
  64. override func numberOfSections(in tableView: UITableView) -> Int {
  65. return Section.allCases.count
  66. }
  67. override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  68. switch Section(rawValue: section)! {
  69. case .configuration:
  70. return ConfigurationRow.allCases.count
  71. case .data:
  72. return DataRow.allCases.count
  73. }
  74. }
  75. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  76. let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
  77. switch Section(rawValue: indexPath.section)! {
  78. case .configuration:
  79. switch ConfigurationRow(rawValue: indexPath.row)! {
  80. case .basalRate:
  81. cell.textLabel?.text = NSLocalizedString("Basal Rates", comment: "The title text for the basal rate schedule")
  82. case .carbRatio:
  83. cell.textLabel?.text = NSLocalizedString("Carb Ratios", comment: "The title of the carb ratios schedule screen")
  84. case .correctionRange:
  85. cell.textLabel?.text = NSLocalizedString("Correction Range", comment: "The title text for the glucose correction range schedule")
  86. case .insulinSensitivity:
  87. cell.textLabel?.text = NSLocalizedString("Insulin Sensitivity", comment: "The title text for the insulin sensitivity schedule")
  88. case .pumpID:
  89. cell.textLabel?.text = NSLocalizedString("Pump ID", comment: "The title text for the pump ID")
  90. }
  91. case .data:
  92. switch DataRow(rawValue: indexPath.row)! {
  93. case .carbs:
  94. cell.textLabel?.text = NSLocalizedString("Carbs", comment: "The title for the cell navigating to the carbs screen")
  95. case .reservoir:
  96. cell.textLabel?.text = NSLocalizedString("Reservoir", comment: "The title for the cell navigating to the reservoir screen")
  97. case .diagnostic:
  98. cell.textLabel?.text = NSLocalizedString("Diagnostic", comment: "The title for the cell displaying diagnostic data")
  99. case .generate:
  100. cell.textLabel?.text = NSLocalizedString("Generate Data", comment: "The title for the cell displaying data generation")
  101. case .reset:
  102. cell.textLabel?.text = NSLocalizedString("Reset", comment: "Title for the cell resetting the data manager")
  103. }
  104. }
  105. return cell
  106. }
  107. // MARK: - UITableViewDelegate
  108. override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  109. let sender = tableView.cellForRow(at: indexPath)
  110. switch Section(rawValue: indexPath.section)! {
  111. case .configuration:
  112. let row = ConfigurationRow(rawValue: indexPath.row)!
  113. switch row {
  114. // case .basalRate:
  115. //
  116. // // x22 with max basal rate of 5U/hr
  117. // let pulsesPerUnit = 20
  118. // let basalRates = (1...100).map { Double($0) / Double(pulsesPerUnit) }
  119. //
  120. // // full x23 rates
  121. // let rateGroup1 = ((1...39).map { Double($0) / Double(40) })
  122. // let rateGroup2 = ((20...199).map { Double($0) / Double(20) })
  123. // let rateGroup3 = ((100...350).map { Double($0) / Double(10) })
  124. // let basalRates = rateGroup1 + rateGroup2 + rateGroup3
  125. // let scheduleVC = BasalScheduleTableViewController(allowedBasalRates: basalRates, maximumScheduleItemCount: 5, minimumTimeInterval: .minutes(30))
  126. //
  127. // if let profile = dataManager?.basalRateSchedule {
  128. // scheduleVC.timeZone = profile.timeZone
  129. //
  130. //
  131. // scheduleVC.scheduleItems = profile.items
  132. // }
  133. // scheduleVC.delegate = self
  134. // scheduleVC.title = sender?.textLabel?.text
  135. // scheduleVC.syncSource = self
  136. //
  137. // show(scheduleVC, sender: sender)
  138. case .carbRatio:
  139. let scheduleVC = DailyQuantityScheduleTableViewController()
  140. scheduleVC.delegate = self
  141. scheduleVC.title = NSLocalizedString("Carb Ratios", comment: "The title of the carb ratios schedule screen")
  142. scheduleVC.unit = .gram()
  143. if let schedule = dataManager?.carbRatioSchedule {
  144. scheduleVC.timeZone = schedule.timeZone
  145. scheduleVC.scheduleItems = schedule.items
  146. scheduleVC.unit = schedule.unit
  147. }
  148. show(scheduleVC, sender: sender)
  149. case .correctionRange:
  150. var therapySettings = TherapySettings()
  151. therapySettings.glucoseTargetRangeSchedule = self.dataManager?.glucoseTargetRangeSchedule
  152. let therapySettingsViewModel = TherapySettingsViewModel(therapySettings: therapySettings)
  153. let view = CorrectionRangeScheduleEditor(mode: .settings, therapySettingsViewModel: therapySettingsViewModel, didSave: {
  154. self.dataManager?.glucoseTargetRangeSchedule = therapySettingsViewModel.therapySettings.glucoseTargetRangeSchedule
  155. self.navigationController?.popToViewController(self, animated: true)
  156. })
  157. .environmentObject(DisplayGlucoseUnitObservable(displayGlucoseUnit: .milligramsPerDeciliter))
  158. let scheduleVC = DismissibleHostingController(rootView: view, dismissalMode: .pop(to: type(of: self)), isModalInPresentation: false)
  159. show(scheduleVC, sender: sender)
  160. case .insulinSensitivity:
  161. let unit = dataManager?.insulinSensitivitySchedule?.unit ?? dataManager?.glucoseStore.preferredUnit ?? HKUnit.milligramsPerDeciliter
  162. let scheduleVC = InsulinSensitivityScheduleViewController(allowedValues: unit.allowedSensitivityValues, unit: unit)
  163. scheduleVC.unit = unit
  164. scheduleVC.delegate = self
  165. scheduleVC.insulinSensitivityScheduleStorageDelegate = self
  166. scheduleVC.schedule = dataManager?.insulinSensitivitySchedule
  167. scheduleVC.title = NSLocalizedString("Insulin Sensitivity", comment: "The title of the insulin sensitivity schedule screen")
  168. show(scheduleVC, sender: sender)
  169. case .pumpID:
  170. let textFieldVC = TextFieldTableViewController()
  171. // textFieldVC.delegate = self
  172. textFieldVC.title = sender?.textLabel?.text
  173. textFieldVC.placeholder = NSLocalizedString("Enter the 6-digit pump ID", comment: "The placeholder text instructing users how to enter a pump ID")
  174. textFieldVC.value = dataManager?.pumpID
  175. textFieldVC.keyboardType = .numberPad
  176. textFieldVC.contextHelp = NSLocalizedString("The pump ID can be found printed on the back, or near the bottom of the STATUS/Esc screen. It is the strictly numerical portion of the serial number (shown as SN or S/N).", comment: "Instructions on where to find the pump ID on a Minimed pump")
  177. show(textFieldVC, sender: sender)
  178. default:
  179. break
  180. }
  181. case .data:
  182. switch DataRow(rawValue: indexPath.row)! {
  183. case .carbs:
  184. //performSegue(withIdentifier: CarbEntryTableViewController.className, sender: sender)
  185. break
  186. case .reservoir:
  187. performSegue(withIdentifier: LegacyInsulinDeliveryTableViewController.className, sender: sender)
  188. case .diagnostic:
  189. let vc = CommandResponseViewController(command: { [weak self] (completionHandler) -> String in
  190. let group = DispatchGroup()
  191. guard let dataManager = self?.dataManager else {
  192. completionHandler("")
  193. return "nil"
  194. }
  195. var doseStoreResponse = ""
  196. group.enter()
  197. dataManager.doseStore.generateDiagnosticReport { (report) in
  198. doseStoreResponse = report
  199. group.leave()
  200. }
  201. var carbStoreResponse = ""
  202. if let carbStore = dataManager.carbStore {
  203. group.enter()
  204. carbStore.generateDiagnosticReport { (report) in
  205. carbStoreResponse = report
  206. group.leave()
  207. }
  208. }
  209. var glucoseStoreResponse = ""
  210. group.enter()
  211. dataManager.glucoseStore.generateDiagnosticReport { (report) in
  212. glucoseStoreResponse = report
  213. group.leave()
  214. }
  215. group.notify(queue: DispatchQueue.main) {
  216. completionHandler([
  217. doseStoreResponse,
  218. carbStoreResponse,
  219. glucoseStoreResponse
  220. ].joined(separator: "\n\n"))
  221. }
  222. return "…"
  223. })
  224. vc.title = "Diagnostic"
  225. show(vc, sender: sender)
  226. case .generate:
  227. let vc = CommandResponseViewController(command: { [weak self] (completionHandler) -> String in
  228. guard let dataManager = self?.dataManager else {
  229. completionHandler("")
  230. return "dataManager is nil"
  231. }
  232. let group = DispatchGroup()
  233. var unitVolume = 150.0
  234. reservoir: for index in sequence(first: TimeInterval(hours: -6), next: { $0 + .minutes(5) }) {
  235. guard index < 0 else {
  236. break reservoir
  237. }
  238. unitVolume -= (drand48() * 2.0)
  239. group.enter()
  240. dataManager.doseStore.addReservoirValue(unitVolume, at: Date(timeIntervalSinceNow: index)) { (_, _, _, error) in
  241. group.leave()
  242. }
  243. }
  244. group.enter()
  245. dataManager.glucoseStore.addGlucoseSamples([NewGlucoseSample(date: Date(), quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 101), condition: nil, trend: nil, trendRate: nil, isDisplayOnly: false, wasUserEntered: false, syncIdentifier: UUID().uuidString)], completion: { (result) in
  246. group.leave()
  247. })
  248. group.notify(queue: .main) {
  249. completionHandler("Completed")
  250. }
  251. return "Generating…"
  252. })
  253. vc.title = sender?.textLabel?.text
  254. show(vc, sender: sender)
  255. case .reset:
  256. dataManager = nil
  257. tableView.reloadData()
  258. }
  259. }
  260. }
  261. // MARK: - Segues
  262. override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  263. super.prepare(for: segue, sender: sender)
  264. var targetViewController = segue.destination
  265. if let navVC = targetViewController as? UINavigationController, let topViewController = navVC.topViewController {
  266. targetViewController = topViewController
  267. }
  268. switch targetViewController {
  269. case let vc as LegacyInsulinDeliveryTableViewController:
  270. vc.doseStore = dataManager?.doseStore
  271. default:
  272. break
  273. }
  274. }
  275. }
  276. extension MasterViewController: DailyValueScheduleTableViewControllerDelegate {
  277. func dailyValueScheduleTableViewControllerWillFinishUpdating(_ controller: DailyValueScheduleTableViewController) {
  278. if let indexPath = tableView.indexPathForSelectedRow {
  279. switch Section(rawValue: indexPath.section)! {
  280. case .configuration:
  281. switch ConfigurationRow(rawValue: indexPath.row)! {
  282. // case .basalRate:
  283. // if let controller = controller as? BasalScheduleTableViewController {
  284. // dataManager?.basalRateSchedule = BasalRateSchedule(dailyItems: controller.scheduleItems, timeZone: controller.timeZone)
  285. // }
  286. default:
  287. break
  288. }
  289. tableView.reloadRows(at: [indexPath], with: .none)
  290. default:
  291. break
  292. }
  293. }
  294. }
  295. }
  296. extension MasterViewController: InsulinSensitivityScheduleStorageDelegate {
  297. func saveSchedule(_ schedule: InsulinSensitivitySchedule, for viewController: InsulinSensitivityScheduleViewController, completion: @escaping (SaveInsulinSensitivityScheduleResult) -> Void) {
  298. self.dataManager?.insulinSensitivitySchedule = schedule
  299. completion(.success)
  300. }
  301. }
  302. private extension HKUnit {
  303. var allowedSensitivityValues: [Double] {
  304. if self == HKUnit.milligramsPerDeciliter {
  305. return (10...500).map { Double($0) }
  306. }
  307. if self == HKUnit.millimolesPerLiter {
  308. return (6...270).map { Double($0) / 10.0 }
  309. }
  310. return []
  311. }
  312. var allowedCorrectionRangeValues: [Double] {
  313. if self == HKUnit.milligramsPerDeciliter {
  314. return (60...180).map { Double($0) }
  315. }
  316. if self == HKUnit.millimolesPerLiter {
  317. return (33...100).map { Double($0) / 10.0 }
  318. }
  319. return []
  320. }
  321. }