MasterViewController.swift 17 KB

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