OverrideSelectionViewController.swift 22 KB

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