OverrideSelectionViewController.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. //
  2. // OverrideSelectionViewController.swift
  3. // Loop
  4. //
  5. // Created by Michael Pangburn on 1/2/19.
  6. // Copyright © 2019 LoopKit Authors. All rights reserved.
  7. //
  8. import UIKit
  9. import HealthKit
  10. import LoopKit
  11. public protocol OverrideSelectionViewControllerDelegate: AnyObject {
  12. func overrideSelectionViewController(_ vc: OverrideSelectionViewController, didUpdatePresets presets: [TemporaryScheduleOverridePreset])
  13. func overrideSelectionViewController(_ vc: OverrideSelectionViewController, didConfirmOverride override: TemporaryScheduleOverride)
  14. func overrideSelectionViewController(_ vc: OverrideSelectionViewController, didCancelOverride override: TemporaryScheduleOverride)
  15. }
  16. public final class OverrideSelectionViewController: UICollectionViewController, IdentifiableClass {
  17. public var glucoseUnit: HKUnit!
  18. public var scheduledOverride: TemporaryScheduleOverride?
  19. public var presets: [TemporaryScheduleOverridePreset] = [] {
  20. didSet {
  21. delegate?.overrideSelectionViewController(self, didUpdatePresets: presets)
  22. }
  23. }
  24. public weak var delegate: OverrideSelectionViewControllerDelegate?
  25. private lazy var saveButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addNewPreset))
  26. private lazy var editButton = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(beginEditing))
  27. private lazy var doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(endEditing))
  28. private lazy var cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancel))
  29. public override func viewDidLoad() {
  30. super.viewDidLoad()
  31. title = NSLocalizedString("Temporary Override", comment: "The title for the override selection screen")
  32. collectionView?.backgroundColor = .groupTableViewBackground
  33. navigationItem.rightBarButtonItems = [saveButton, editButton]
  34. navigationItem.leftBarButtonItem = cancelButton
  35. }
  36. @objc private func cancel() {
  37. dismiss(animated: true)
  38. }
  39. enum Section: Int, CaseIterable {
  40. case scheduledOverride = 0
  41. case presets
  42. }
  43. private var sections: [Section] {
  44. var sections = Section.allCases
  45. if scheduledOverride == nil {
  46. sections.remove(.scheduledOverride)
  47. }
  48. return sections
  49. }
  50. private var presetSection: Int {
  51. sections.firstIndex(of: .presets)!
  52. }
  53. private func section(for sectionIndex: Int) -> Section {
  54. return sections[sectionIndex]
  55. }
  56. private enum CellContent {
  57. case scheduledOverride(TemporaryScheduleOverride)
  58. case preset(TemporaryScheduleOverridePreset)
  59. case customOverride
  60. }
  61. private func cellContent(for indexPath: IndexPath) -> CellContent {
  62. switch section(for: indexPath.section) {
  63. case .scheduledOverride:
  64. guard let scheduledOverride = scheduledOverride else {
  65. preconditionFailure("`sections` must contain `.scheduledOverride`")
  66. }
  67. return .scheduledOverride(scheduledOverride)
  68. case .presets:
  69. if presets.indices.contains(indexPath.row) {
  70. return .preset(presets[indexPath.row])
  71. } else {
  72. return .customOverride
  73. }
  74. }
  75. }
  76. private func indexPathOfCustomOverride() -> IndexPath {
  77. let section = sections.firstIndex(of: .presets)!
  78. let row = self.collectionView(collectionView, numberOfItemsInSection: section) - 1
  79. return IndexPath(row: row, section: section)
  80. }
  81. public override func numberOfSections(in collectionView: UICollectionView) -> Int {
  82. return sections.count
  83. }
  84. public override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  85. switch self.section(for: section) {
  86. case .scheduledOverride:
  87. return 1
  88. case .presets:
  89. // +1 for custom override
  90. return presets.count + 1
  91. }
  92. }
  93. public override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
  94. switch kind {
  95. case UICollectionView.elementKindSectionHeader:
  96. let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: OverrideSelectionHeaderView.className, for: indexPath) as! OverrideSelectionHeaderView
  97. switch section(for: indexPath.section) {
  98. case .scheduledOverride:
  99. header.titleLabel.text = NSLocalizedString("SCHEDULED OVERRIDE", comment: "The section header text for a scheduled override")
  100. case .presets:
  101. header.titleLabel.text = NSLocalizedString("PRESETS", comment: "The section header text override presets")
  102. }
  103. return header
  104. case UICollectionView.elementKindSectionFooter:
  105. let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: OverrideSelectionFooterView.className, for: indexPath) as! OverrideSelectionFooterView
  106. footer.textLabel.text = NSLocalizedString("Tap '+' to create a new override preset.", comment: "Text directing the user to configure their first override preset")
  107. return footer
  108. default:
  109. fatalError("Unexpected supplementary element kind \(kind)")
  110. }
  111. }
  112. private lazy var quantityFormatter: QuantityFormatter = {
  113. let quantityFormatter = QuantityFormatter()
  114. quantityFormatter.setPreferredNumberFormatter(for: glucoseUnit)
  115. return quantityFormatter
  116. }()
  117. private lazy var glucoseNumberFormatter = quantityFormatter.numberFormatter
  118. private lazy var durationFormatter: DateComponentsFormatter = {
  119. let formatter = DateComponentsFormatter()
  120. formatter.allowedUnits = [.hour, .minute]
  121. formatter.unitsStyle = .abbreviated
  122. return formatter
  123. }()
  124. public override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  125. let customSymbol = "⋯"
  126. let customName = NSLocalizedString("Custom", comment: "The text for a custom override")
  127. switch cellContent(for: indexPath) {
  128. case .scheduledOverride(let override):
  129. let cell = collectionView.dequeueReusableCell(withReuseIdentifier: OverridePresetCollectionViewCell.className, for: indexPath) as! OverridePresetCollectionViewCell
  130. cell.delegate = self
  131. if case .preset(let preset) = override.context {
  132. cell.symbolLabel.text = preset.symbol
  133. cell.nameLabel.text = preset.name
  134. } else {
  135. cell.symbolLabel.text = customSymbol
  136. cell.nameLabel.text = customName
  137. }
  138. cell.startTimeLabel.text = DateFormatter.localizedString(from: override.startDate, dateStyle: .none, timeStyle: .short)
  139. configure(cell, with: override.settings, duration: override.duration)
  140. cell.scheduleButton.isHidden = true
  141. if isEditingPresets {
  142. cell.applyOverlayToFade(animated: false)
  143. }
  144. return cell
  145. case .preset(let preset):
  146. let cell = collectionView.dequeueReusableCell(withReuseIdentifier: OverridePresetCollectionViewCell.className, for: indexPath) as! OverridePresetCollectionViewCell
  147. cell.delegate = self
  148. cell.symbolLabel.text = preset.symbol
  149. cell.nameLabel.text = preset.name
  150. configure(cell, with: preset.settings, duration: preset.duration)
  151. if isEditingPresets {
  152. cell.configureForEditing(animated: false)
  153. }
  154. return cell
  155. case .customOverride:
  156. let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomOverrideCollectionViewCell.className, for: indexPath) as! CustomOverrideCollectionViewCell
  157. cell.titleLabel.text = customName
  158. if isEditingPresets {
  159. cell.applyOverlayToFade(animated: false)
  160. }
  161. return cell
  162. }
  163. }
  164. private func configure(_ cell: OverridePresetCollectionViewCell, with settings: TemporaryScheduleOverrideSettings, duration: TemporaryScheduleOverride.Duration) {
  165. if let targetRange = settings.targetRange {
  166. cell.targetRangeLabel.text = makeTargetRangeText(from: targetRange)
  167. } else {
  168. cell.targetRangeLabel.isHidden = true
  169. }
  170. if let insulinNeedsScaleFactor = settings.insulinNeedsScaleFactor {
  171. cell.insulinNeedsBar.progress = insulinNeedsScaleFactor
  172. } else {
  173. cell.insulinNeedsBar.isHidden = true
  174. }
  175. switch duration {
  176. case .finite(let interval):
  177. cell.durationLabel.text = durationFormatter.string(from: interval)
  178. case .indefinite:
  179. cell.durationLabel.text = "∞"
  180. }
  181. }
  182. private func makeTargetRangeText(from targetRange: ClosedRange<HKQuantity>) -> String {
  183. guard
  184. let minTarget = glucoseNumberFormatter.string(from: targetRange.lowerBound.doubleValue(for: glucoseUnit)),
  185. let maxTarget = glucoseNumberFormatter.string(from: targetRange.upperBound.doubleValue(for: glucoseUnit))
  186. else {
  187. return ""
  188. }
  189. return String(format: NSLocalizedString("%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: glucoseUnit))
  190. }
  191. public override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  192. if isEditingPresets {
  193. switch cellContent(for: indexPath) {
  194. case .scheduledOverride, .customOverride:
  195. break
  196. case .preset(let preset):
  197. let editVC = AddEditOverrideTableViewController(glucoseUnit: glucoseUnit)
  198. editVC.inputMode = .editPreset(preset)
  199. editVC.delegate = self
  200. show(editVC, sender: collectionView.cellForItem(at: indexPath))
  201. }
  202. } else {
  203. switch cellContent(for: indexPath) {
  204. case .scheduledOverride(let override):
  205. let editOverrideVC = AddEditOverrideTableViewController(glucoseUnit: glucoseUnit)
  206. editOverrideVC.inputMode = .editOverride(override)
  207. editOverrideVC.customDismissalMode = .dismissModal
  208. editOverrideVC.delegate = self
  209. show(editOverrideVC, sender: collectionView.cellForItem(at: indexPath))
  210. case .preset(let preset):
  211. let override = preset.createOverride(enactTrigger: .local)
  212. delegate?.overrideSelectionViewController(self, didConfirmOverride: override)
  213. dismiss(animated: true)
  214. case .customOverride:
  215. let customOverrideVC = AddEditOverrideTableViewController(glucoseUnit: glucoseUnit)
  216. customOverrideVC.inputMode = .customOverride
  217. customOverrideVC.delegate = self
  218. show(customOverrideVC, sender: collectionView.cellForItem(at: indexPath))
  219. }
  220. }
  221. }
  222. @objc private func addNewPreset() {
  223. let addVC = AddEditOverrideTableViewController(glucoseUnit: glucoseUnit)
  224. addVC.inputMode = .newPreset
  225. addVC.delegate = self
  226. let navigationWrapper = UINavigationController(rootViewController: addVC)
  227. present(navigationWrapper, animated: true)
  228. }
  229. private var isEditingPresets = false {
  230. didSet {
  231. saveButton.isEnabled = !isEditingPresets
  232. cancelButton.isEnabled = !isEditingPresets
  233. }
  234. }
  235. @objc private func beginEditing() {
  236. isEditingPresets = true
  237. navigationItem.setRightBarButtonItems([saveButton, doneButton], animated: true)
  238. configureCellsForEditingChanged()
  239. if let scheduledOverrideSection = sections.firstIndex(of: .scheduledOverride) {
  240. let scheduledOverrideIndexPath = IndexPath(row: 0, section: scheduledOverrideSection)
  241. guard let scheduledOverrideCell = collectionView.cellForItem(at: scheduledOverrideIndexPath) as? OverridePresetCollectionViewCell else {
  242. return
  243. }
  244. scheduledOverrideCell.applyOverlayToFade(animated: true)
  245. }
  246. if let customOverrideCell = collectionView.cellForItem(at: indexPathOfCustomOverride()) as? CustomOverrideCollectionViewCell {
  247. customOverrideCell.applyOverlayToFade(animated: true)
  248. }
  249. }
  250. @objc private func endEditing() {
  251. isEditingPresets = false
  252. navigationItem.setRightBarButtonItems([saveButton, editButton], animated: true)
  253. configureCellsForEditingChanged()
  254. if let scheduledOverrideSection = sections.firstIndex(of: .scheduledOverride) {
  255. let scheduledOverrideIndexPath = IndexPath(row: 0, section: scheduledOverrideSection)
  256. guard let scheduledOverrideCell = collectionView.cellForItem(at: scheduledOverrideIndexPath) as? OverridePresetCollectionViewCell else {
  257. return
  258. }
  259. scheduledOverrideCell.removeOverlay(animated: true)
  260. }
  261. if let customOverrideCell = collectionView.cellForItem(at: indexPathOfCustomOverride()) as? CustomOverrideCollectionViewCell {
  262. customOverrideCell.removeOverlay(animated: true)
  263. }
  264. }
  265. private func configureCellsForEditingChanged() {
  266. for indexPath in collectionView.indexPathsForVisibleItems where indexPath.section == presetSection {
  267. if let cell = collectionView.cellForItem(at: indexPath) as? OverridePresetCollectionViewCell {
  268. if isEditingPresets {
  269. cell.configureForEditing(animated: true)
  270. } else {
  271. cell.configureForStandard(animated: true)
  272. }
  273. }
  274. }
  275. }
  276. public override func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool {
  277. if !isEditingPresets {
  278. return true
  279. }
  280. switch cellContent(for: indexPath) {
  281. case .scheduledOverride, .customOverride:
  282. return false
  283. case .preset:
  284. return true
  285. }
  286. }
  287. public override func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
  288. isEditingPresets
  289. && indexPath.section == presetSection
  290. && indexPath != indexPathOfCustomOverride()
  291. }
  292. public override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
  293. let movedPreset = presets.remove(at: sourceIndexPath.row)
  294. presets.insert(movedPreset, at: destinationIndexPath.row)
  295. }
  296. public override func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath {
  297. guard proposedIndexPath.section == sections.firstIndex(of: .presets) else {
  298. return originalIndexPath
  299. }
  300. return proposedIndexPath == indexPathOfCustomOverride()
  301. ? originalIndexPath
  302. : proposedIndexPath
  303. }
  304. }
  305. extension OverrideSelectionViewController: UICollectionViewDelegateFlowLayout {
  306. private var sectionInsets: UIEdgeInsets {
  307. return UIEdgeInsets(top: 0, left: 12, bottom: 12, right: 12)
  308. }
  309. public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
  310. guard presets.isEmpty else { return .zero }
  311. return CGSize(width: collectionView.frame.width, height: 50)
  312. }
  313. public func collectionView(
  314. _ collectionView: UICollectionView,
  315. layout collectionViewLayout: UICollectionViewLayout,
  316. sizeForItemAt indexPath: IndexPath
  317. ) -> CGSize {
  318. let paddingSpace = sectionInsets.left * 2
  319. let width = view.frame.width - paddingSpace
  320. let height: CGFloat
  321. switch cellContent(for: indexPath) {
  322. case .scheduledOverride, .preset:
  323. height = 76
  324. case .customOverride:
  325. height = 52
  326. }
  327. return CGSize(width: width, height: height)
  328. }
  329. public func collectionView(
  330. _ collectionView: UICollectionView,
  331. layout collectionViewLayout: UICollectionViewLayout,
  332. insetForSectionAt section: Int
  333. ) -> UIEdgeInsets {
  334. return sectionInsets
  335. }
  336. public func collectionView(
  337. _ collectionView: UICollectionView,
  338. layout collectionViewLayout: UICollectionViewLayout,
  339. minimumLineSpacingForSectionAt section: Int
  340. ) -> CGFloat {
  341. return sectionInsets.left
  342. }
  343. }
  344. extension OverrideSelectionViewController: AddEditOverrideTableViewControllerDelegate {
  345. public func addEditOverrideTableViewController(_ vc: AddEditOverrideTableViewController, didSavePreset preset: TemporaryScheduleOverridePreset) {
  346. if let selectedIndexPath = collectionView.indexPathsForSelectedItems?.first {
  347. presets[selectedIndexPath.row] = preset
  348. collectionView.reloadItems(at: [selectedIndexPath])
  349. collectionView.deselectItem(at: selectedIndexPath, animated: true)
  350. } else {
  351. presets.append(preset)
  352. collectionView.insertItems(at: [IndexPath(row: presets.endIndex - 1, section: presetSection)])
  353. delegate?.overrideSelectionViewController(self, didUpdatePresets: presets)
  354. }
  355. }
  356. public func addEditOverrideTableViewController(_ vc: AddEditOverrideTableViewController, didSaveOverride override: TemporaryScheduleOverride) {
  357. delegate?.overrideSelectionViewController(self, didConfirmOverride: override)
  358. }
  359. public func addEditOverrideTableViewController(_ vc: AddEditOverrideTableViewController, didCancelOverride override: TemporaryScheduleOverride) {
  360. delegate?.overrideSelectionViewController(self, didCancelOverride: override)
  361. }
  362. }
  363. extension OverrideSelectionViewController: OverridePresetCollectionViewCellDelegate {
  364. func overridePresetCollectionViewCellDidScheduleOverride(_ cell: OverridePresetCollectionViewCell) {
  365. guard
  366. let indexPath = collectionView.indexPath(for: cell),
  367. case .preset(let preset) = cellContent(for: indexPath)
  368. else {
  369. return
  370. }
  371. let customizePresetVC = AddEditOverrideTableViewController(glucoseUnit: glucoseUnit)
  372. customizePresetVC.inputMode = .customizePresetOverride(preset)
  373. customizePresetVC.delegate = self
  374. show(customizePresetVC, sender: nil)
  375. }
  376. func overridePresetCollectionViewCellDidPerformFirstDeletionStep(_ cell: OverridePresetCollectionViewCell) {
  377. for case let visibleCell as OverridePresetCollectionViewCell in collectionView.visibleCells
  378. where visibleCell !== cell && visibleCell.isShowingFinalDeleteConfirmation
  379. {
  380. visibleCell.configureForEditing(animated: true)
  381. }
  382. }
  383. func overridePresetCollectionViewCellDidDeletePreset(_ cell: OverridePresetCollectionViewCell) {
  384. guard let indexPath = collectionView.indexPath(for: cell) else {
  385. return
  386. }
  387. presets.remove(at: indexPath.row)
  388. collectionView.deleteItems(at: [indexPath])
  389. }
  390. }
  391. private extension Array where Element: Equatable {
  392. mutating func remove(_ element: Element) {
  393. if let index = self.firstIndex(of: element) {
  394. remove(at: index)
  395. }
  396. }
  397. }