OverrideSelectionViewController.swift 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  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()
  130. quantityFormatter.setPreferredNumberFormatter(for: glucoseUnit)
  131. return quantityFormatter
  132. }()
  133. private lazy var glucoseNumberFormatter = quantityFormatter.numberFormatter
  134. private lazy var durationFormatter: DateComponentsFormatter = {
  135. let formatter = DateComponentsFormatter()
  136. formatter.allowedUnits = [.hour, .minute]
  137. formatter.unitsStyle = .abbreviated
  138. return formatter
  139. }()
  140. public override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  141. let customSymbol = "⋯"
  142. let customName = LocalizedString("Custom", comment: "The text for a custom preset")
  143. let historyLabel = LocalizedString("History", comment: "The text for the override history")
  144. switch cellContent(for: indexPath) {
  145. case .scheduledOverride(let override):
  146. let cell = collectionView.dequeueReusableCell(withReuseIdentifier: OverridePresetCollectionViewCell.className, for: indexPath) as! OverridePresetCollectionViewCell
  147. cell.delegate = self
  148. if case .preset(let preset) = override.context {
  149. cell.symbolLabel.text = preset.symbol
  150. cell.nameLabel.text = preset.name
  151. } else {
  152. cell.symbolLabel.text = customSymbol
  153. cell.nameLabel.text = customName
  154. }
  155. cell.startTimeLabel.text = DateFormatter.localizedString(from: override.startDate, dateStyle: .none, timeStyle: .short)
  156. configure(cell, with: override.settings, duration: override.duration)
  157. cell.scheduleButton.isHidden = true
  158. if isEditingPresets {
  159. cell.applyOverlayToFade(animated: false)
  160. }
  161. return cell
  162. case .preset(let preset):
  163. let cell = collectionView.dequeueReusableCell(withReuseIdentifier: OverridePresetCollectionViewCell.className, for: indexPath) as! OverridePresetCollectionViewCell
  164. cell.delegate = self
  165. cell.symbolLabel.text = preset.symbol
  166. cell.nameLabel.text = preset.name
  167. configure(cell, with: preset.settings, duration: preset.duration)
  168. if isEditingPresets {
  169. cell.configureForEditing(animated: false)
  170. }
  171. return cell
  172. case .customOverride:
  173. let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomOverrideCollectionViewCell.className, for: indexPath) as! CustomOverrideCollectionViewCell
  174. cell.titleLabel.text = customName
  175. if isEditingPresets {
  176. cell.applyOverlayToFade(animated: false)
  177. }
  178. return cell
  179. case .history:
  180. let cell = collectionView.dequeueReusableCell(withReuseIdentifier: OverrideHistoryCollectionViewCell.className, for: indexPath) as! OverrideHistoryCollectionViewCell
  181. cell.titleLabel.text = historyLabel
  182. cell.titleLabel.accessibilityLabel = historyLabel
  183. if isEditingPresets {
  184. cell.applyOverlayToFade(animated: false)
  185. }
  186. return cell
  187. }
  188. }
  189. private func configure(_ cell: OverridePresetCollectionViewCell, with settings: TemporaryScheduleOverrideSettings, duration: TemporaryScheduleOverride.Duration) {
  190. if let targetRange = settings.targetRange {
  191. cell.targetRangeLabel.text = makeTargetRangeText(from: targetRange)
  192. } else {
  193. cell.targetRangeLabel.isHidden = true
  194. }
  195. if let insulinNeedsScaleFactor = settings.insulinNeedsScaleFactor {
  196. cell.insulinNeedsBar.progress = insulinNeedsScaleFactor
  197. } else {
  198. cell.insulinNeedsBar.isHidden = true
  199. }
  200. switch duration {
  201. case .finite(let interval):
  202. cell.durationLabel.text = durationFormatter.string(from: interval)
  203. case .indefinite:
  204. cell.durationLabel.text = "∞"
  205. }
  206. }
  207. private func makeTargetRangeText(from targetRange: ClosedRange<HKQuantity>) -> String {
  208. guard
  209. let minTarget = glucoseNumberFormatter.string(from: targetRange.lowerBound.doubleValue(for: glucoseUnit)),
  210. let maxTarget = glucoseNumberFormatter.string(from: targetRange.upperBound.doubleValue(for: glucoseUnit))
  211. else {
  212. return ""
  213. }
  214. 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: glucoseUnit))
  215. }
  216. public override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  217. if isEditingPresets {
  218. switch cellContent(for: indexPath) {
  219. case .scheduledOverride, .customOverride, .history:
  220. break
  221. case .preset(let preset):
  222. let editVC = AddEditOverrideTableViewController(glucoseUnit: glucoseUnit)
  223. editVC.inputMode = .editPreset(preset)
  224. editVC.delegate = self
  225. show(editVC, sender: collectionView.cellForItem(at: indexPath))
  226. }
  227. } else {
  228. switch cellContent(for: indexPath) {
  229. case .scheduledOverride(let override):
  230. let editOverrideVC = AddEditOverrideTableViewController(glucoseUnit: glucoseUnit)
  231. editOverrideVC.inputMode = .editOverride(override)
  232. editOverrideVC.customDismissalMode = .dismissModal
  233. editOverrideVC.delegate = self
  234. show(editOverrideVC, sender: collectionView.cellForItem(at: indexPath))
  235. case .preset(let preset):
  236. delegate?.overrideSelectionViewController(self, didConfirmPreset: preset)
  237. dismiss(animated: true)
  238. case .customOverride:
  239. let customOverrideVC = AddEditOverrideTableViewController(glucoseUnit: glucoseUnit)
  240. customOverrideVC.inputMode = .customOverride
  241. customOverrideVC.delegate = self
  242. show(customOverrideVC, sender: collectionView.cellForItem(at: indexPath))
  243. case .history:
  244. let model = OverrideHistorystate(
  245. overrides: overrideHistory,
  246. glucoseUnit: glucoseUnit
  247. )
  248. let overrideHistoryView = OverrideSelectionHistory(model: model)
  249. let hostedView = UIHostingController(rootView: overrideHistoryView)
  250. hostedView.title = LocalizedString("Override History", comment: "Title for override history view") // Hack to fix animations
  251. navigationController?.pushViewController(hostedView, animated: true)
  252. }
  253. }
  254. }
  255. @objc private func addNewPreset() {
  256. let addVC = AddEditOverrideTableViewController(glucoseUnit: glucoseUnit)
  257. addVC.inputMode = .newPreset
  258. addVC.delegate = self
  259. let navigationWrapper = UINavigationController(rootViewController: addVC)
  260. present(navigationWrapper, animated: true)
  261. }
  262. private var isEditingPresets = false {
  263. didSet {
  264. saveButton.isEnabled = !isEditingPresets
  265. cancelButton.isEnabled = !isEditingPresets
  266. }
  267. }
  268. @objc private func beginEditing() {
  269. isEditingPresets = true
  270. navigationItem.setRightBarButtonItems([saveButton, doneButton], animated: true)
  271. configureCellsForEditingChanged()
  272. if let scheduledOverrideSection = sections.firstIndex(of: .scheduledOverride) {
  273. let scheduledOverrideIndexPath = IndexPath(row: 0, section: scheduledOverrideSection)
  274. guard let scheduledOverrideCell = collectionView.cellForItem(at: scheduledOverrideIndexPath) as? OverridePresetCollectionViewCell else {
  275. return
  276. }
  277. scheduledOverrideCell.applyOverlayToFade(animated: true)
  278. }
  279. if let customOverrideCell = collectionView.cellForItem(at: indexPathOfCustomOverride()) as? CustomOverrideCollectionViewCell {
  280. customOverrideCell.applyOverlayToFade(animated: true)
  281. }
  282. }
  283. @objc private func endEditing() {
  284. isEditingPresets = false
  285. navigationItem.setRightBarButtonItems([saveButton, editButton], animated: true)
  286. configureCellsForEditingChanged()
  287. if let scheduledOverrideSection = sections.firstIndex(of: .scheduledOverride) {
  288. let scheduledOverrideIndexPath = IndexPath(row: 0, section: scheduledOverrideSection)
  289. guard let scheduledOverrideCell = collectionView.cellForItem(at: scheduledOverrideIndexPath) as? OverridePresetCollectionViewCell else {
  290. return
  291. }
  292. scheduledOverrideCell.removeOverlay(animated: true)
  293. }
  294. if let customOverrideCell = collectionView.cellForItem(at: indexPathOfCustomOverride()) as? CustomOverrideCollectionViewCell {
  295. customOverrideCell.removeOverlay(animated: true)
  296. }
  297. }
  298. private func configureCellsForEditingChanged() {
  299. for indexPath in collectionView.indexPathsForVisibleItems where indexPath.section == presetSection {
  300. if let cell = collectionView.cellForItem(at: indexPath) as? OverridePresetCollectionViewCell {
  301. if isEditingPresets {
  302. cell.configureForEditing(animated: true)
  303. } else {
  304. cell.configureForStandard(animated: true)
  305. }
  306. }
  307. }
  308. }
  309. public override func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool {
  310. if !isEditingPresets {
  311. return true
  312. }
  313. switch cellContent(for: indexPath) {
  314. case .scheduledOverride, .customOverride, .history:
  315. return false
  316. case .preset:
  317. return true
  318. }
  319. }
  320. public override func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
  321. isEditingPresets
  322. && indexPath.section == presetSection
  323. && indexPath != indexPathOfCustomOverride()
  324. }
  325. public override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
  326. let movedPreset = presets.remove(at: sourceIndexPath.row)
  327. presets.insert(movedPreset, at: destinationIndexPath.row)
  328. }
  329. public override func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath {
  330. guard proposedIndexPath.section == sections.firstIndex(of: .presets) else {
  331. return originalIndexPath
  332. }
  333. return proposedIndexPath == indexPathOfCustomOverride()
  334. ? originalIndexPath
  335. : proposedIndexPath
  336. }
  337. }
  338. extension OverrideSelectionViewController: UICollectionViewDelegateFlowLayout {
  339. private var sectionInsets: UIEdgeInsets {
  340. return UIEdgeInsets(top: 0, left: 12, bottom: 12, right: 12)
  341. }
  342. public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
  343. guard presets.isEmpty else { return .zero }
  344. return CGSize(width: collectionView.frame.width, height: 50)
  345. }
  346. public func collectionView(
  347. _ collectionView: UICollectionView,
  348. layout collectionViewLayout: UICollectionViewLayout,
  349. sizeForItemAt indexPath: IndexPath
  350. ) -> CGSize {
  351. let paddingSpace = sectionInsets.left * 2
  352. let width = view.frame.width - paddingSpace
  353. let height: CGFloat
  354. switch cellContent(for: indexPath) {
  355. case .scheduledOverride, .preset:
  356. height = 76
  357. case .customOverride, .history:
  358. height = 52
  359. }
  360. return CGSize(width: width, height: height)
  361. }
  362. public func collectionView(
  363. _ collectionView: UICollectionView,
  364. layout collectionViewLayout: UICollectionViewLayout,
  365. insetForSectionAt section: Int
  366. ) -> UIEdgeInsets {
  367. return sectionInsets
  368. }
  369. public func collectionView(
  370. _ collectionView: UICollectionView,
  371. layout collectionViewLayout: UICollectionViewLayout,
  372. minimumLineSpacingForSectionAt section: Int
  373. ) -> CGFloat {
  374. return sectionInsets.left
  375. }
  376. }
  377. extension OverrideSelectionViewController: AddEditOverrideTableViewControllerDelegate {
  378. public func addEditOverrideTableViewController(_ vc: AddEditOverrideTableViewController, didSavePreset preset: TemporaryScheduleOverridePreset) {
  379. if let selectedIndexPath = collectionView.indexPathsForSelectedItems?.first {
  380. presets[selectedIndexPath.row] = preset
  381. collectionView.reloadItems(at: [selectedIndexPath])
  382. collectionView.deselectItem(at: selectedIndexPath, animated: true)
  383. } else {
  384. presets.append(preset)
  385. collectionView.insertItems(at: [IndexPath(row: presets.endIndex - 1, section: presetSection)])
  386. delegate?.overrideSelectionViewController(self, didUpdatePresets: presets)
  387. }
  388. }
  389. public func addEditOverrideTableViewController(_ vc: AddEditOverrideTableViewController, didSaveOverride override: TemporaryScheduleOverride) {
  390. delegate?.overrideSelectionViewController(self, didConfirmOverride: override)
  391. }
  392. public func addEditOverrideTableViewController(_ vc: AddEditOverrideTableViewController, didCancelOverride override: TemporaryScheduleOverride) {
  393. delegate?.overrideSelectionViewController(self, didCancelOverride: override)
  394. }
  395. }
  396. extension OverrideSelectionViewController: OverridePresetCollectionViewCellDelegate {
  397. func overridePresetCollectionViewCellDidScheduleOverride(_ cell: OverridePresetCollectionViewCell) {
  398. guard
  399. let indexPath = collectionView.indexPath(for: cell),
  400. case .preset(let preset) = cellContent(for: indexPath)
  401. else {
  402. return
  403. }
  404. let customizePresetVC = AddEditOverrideTableViewController(glucoseUnit: glucoseUnit)
  405. customizePresetVC.inputMode = .customizePresetOverride(preset)
  406. customizePresetVC.delegate = self
  407. show(customizePresetVC, sender: nil)
  408. }
  409. func overridePresetCollectionViewCellDidPerformFirstDeletionStep(_ cell: OverridePresetCollectionViewCell) {
  410. for case let visibleCell as OverridePresetCollectionViewCell in collectionView.visibleCells
  411. where visibleCell !== cell && visibleCell.isShowingFinalDeleteConfirmation
  412. {
  413. visibleCell.configureForEditing(animated: true)
  414. }
  415. }
  416. func overridePresetCollectionViewCellDidDeletePreset(_ cell: OverridePresetCollectionViewCell) {
  417. guard let indexPath = collectionView.indexPath(for: cell) else {
  418. return
  419. }
  420. presets.remove(at: indexPath.row)
  421. if let name = cell.nameLabel.text {
  422. INInteraction.delete(with: name) { (error) in
  423. if let error = error {
  424. os_log(.error, "Failed to delete intent: %{public}@", String(describing: error))
  425. }
  426. }
  427. }
  428. collectionView.deleteItems(at: [indexPath])
  429. }
  430. }
  431. private extension Array where Element: Equatable {
  432. mutating func remove(_ element: Element) {
  433. if let index = self.firstIndex(of: element) {
  434. remove(at: index)
  435. }
  436. }
  437. }