| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513 |
- //
- // OverrideSelectionViewController.swift
- // Loop
- //
- // Created by Michael Pangburn on 1/2/19.
- // Copyright © 2019 LoopKit Authors. All rights reserved.
- //
- import UIKit
- import HealthKit
- import LoopKit
- import SwiftUI
- import Intents
- import os.log
- public protocol OverrideSelectionViewControllerDelegate: AnyObject {
- func overrideSelectionViewController(_ vc: OverrideSelectionViewController, didUpdatePresets presets: [TemporaryScheduleOverridePreset])
- func overrideSelectionViewController(_ vc: OverrideSelectionViewController, didConfirmPreset preset: TemporaryScheduleOverridePreset)
- func overrideSelectionViewController(_ vc: OverrideSelectionViewController, didConfirmOverride override: TemporaryScheduleOverride)
- func overrideSelectionViewController(_ vc: OverrideSelectionViewController, didCancelOverride override: TemporaryScheduleOverride)
- }
- public final class OverrideSelectionViewController: UICollectionViewController, IdentifiableClass {
- public var glucoseUnit: HKUnit!
- public var scheduledOverride: TemporaryScheduleOverride?
- public var presets: [TemporaryScheduleOverridePreset] = [] {
- didSet {
- delegate?.overrideSelectionViewController(self, didUpdatePresets: presets)
- }
- }
-
- public var overrideHistory: [TemporaryScheduleOverride] = []
- public weak var delegate: OverrideSelectionViewControllerDelegate?
- private lazy var saveButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addNewPreset))
- private lazy var editButton = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(beginEditing))
- private lazy var doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(endEditing))
- private lazy var cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancel))
- public override func viewDidLoad() {
- super.viewDidLoad()
- title = LocalizedString("Custom Preset", comment: "The title for the custom preset selection screen")
- collectionView?.backgroundColor = .systemGroupedBackground
- navigationItem.rightBarButtonItems = [saveButton, editButton]
- navigationItem.leftBarButtonItem = cancelButton
- }
- @objc private func cancel() {
- dismiss(animated: true)
- }
- enum Section: Int, CaseIterable {
- case scheduledOverride = 0
- case presets
- }
- private var sections: [Section] {
- var sections = Section.allCases
- if scheduledOverride == nil {
- sections.remove(.scheduledOverride)
- }
- return sections
- }
- private var presetSection: Int {
- sections.firstIndex(of: .presets)!
- }
- private func section(for sectionIndex: Int) -> Section {
- return sections[sectionIndex]
- }
- private enum CellContent {
- case scheduledOverride(TemporaryScheduleOverride)
- case preset(TemporaryScheduleOverridePreset)
- case customOverride
- case history
- }
- private func cellContent(for indexPath: IndexPath) -> CellContent {
- switch section(for: indexPath.section) {
- case .scheduledOverride:
- guard let scheduledOverride = scheduledOverride else {
- preconditionFailure("`sections` must contain `.scheduledOverride`")
- }
- return .scheduledOverride(scheduledOverride)
- case .presets:
- if presets.indices.contains(indexPath.row) {
- return .preset(presets[indexPath.row])
- } else if indexPathOfCustomOverride().row == indexPath.row {
- return .customOverride
- } else {
- return .history
- }
- }
- }
- private func indexPathOfCustomOverride() -> IndexPath {
- let section = sections.firstIndex(of: .presets)!
- let row = self.collectionView(collectionView, numberOfItemsInSection: section) - 2
- return IndexPath(row: row, section: section)
- }
- public override func numberOfSections(in collectionView: UICollectionView) -> Int {
- return sections.count
- }
- public override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
- switch self.section(for: section) {
- case .scheduledOverride:
- return 1
- case .presets:
- // +1 for custom override and +1 for history
- return presets.count + 2
- }
- }
- public override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
- switch kind {
- case UICollectionView.elementKindSectionHeader:
- let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: OverrideSelectionHeaderView.className, for: indexPath) as! OverrideSelectionHeaderView
- switch section(for: indexPath.section) {
- case .scheduledOverride:
- header.titleLabel.text = LocalizedString("SCHEDULED PRESET", comment: "The section header text for a scheduled custom preset")
- case .presets:
- if scheduledOverride != nil {
- header.titleLabel.text = LocalizedString("PRESETS", comment: "The section header text for custom presets")
- } else {
- header.titleLabel.text?.removeAll()
- }
- }
- return header
- case UICollectionView.elementKindSectionFooter:
- let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: OverrideSelectionFooterView.className, for: indexPath) as! OverrideSelectionFooterView
- footer.textLabel.text = LocalizedString("Tap '+' to create a new custom preset.", comment: "Text directing the user to configure their first custom preset")
- return footer
- default:
- fatalError("Unexpected supplementary element kind \(kind)")
- }
- }
- public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
- let height: CGFloat = section == 0 && scheduledOverride == nil ? 8 : 50
- return CGSize(width: collectionView.bounds.width, height: height)
- }
- private lazy var quantityFormatter: QuantityFormatter = {
- let quantityFormatter = QuantityFormatter(for: glucoseUnit)
- return quantityFormatter
- }()
- private lazy var glucoseNumberFormatter = quantityFormatter.numberFormatter
- private lazy var durationFormatter: DateComponentsFormatter = {
- let formatter = DateComponentsFormatter()
- formatter.allowedUnits = [.hour, .minute]
- formatter.unitsStyle = .abbreviated
- return formatter
- }()
- public override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
- let customSymbol = "⋯"
- let customName = LocalizedString("Custom", comment: "The text for a custom preset")
- let historyLabel = LocalizedString("History", comment: "The text for the override history")
- switch cellContent(for: indexPath) {
- case .scheduledOverride(let override):
- let cell = collectionView.dequeueReusableCell(withReuseIdentifier: OverridePresetCollectionViewCell.className, for: indexPath) as! OverridePresetCollectionViewCell
- cell.delegate = self
- if case .preset(let preset) = override.context {
- cell.symbolLabel.text = preset.symbol
- cell.nameLabel.text = preset.name
- } else {
- cell.symbolLabel.text = customSymbol
- cell.nameLabel.text = customName
- }
- cell.startTimeLabel.text = DateFormatter.localizedString(from: override.startDate, dateStyle: .none, timeStyle: .short)
- configure(cell, with: override.settings, duration: override.duration)
- cell.scheduleButton.isHidden = true
- if isEditingPresets {
- cell.applyOverlayToFade(animated: false)
- }
- return cell
- case .preset(let preset):
- let cell = collectionView.dequeueReusableCell(withReuseIdentifier: OverridePresetCollectionViewCell.className, for: indexPath) as! OverridePresetCollectionViewCell
- cell.delegate = self
- cell.symbolLabel.text = preset.symbol
- cell.nameLabel.text = preset.name
- configure(cell, with: preset.settings, duration: preset.duration)
- if isEditingPresets {
- cell.configureForEditing(animated: false)
- }
- return cell
- case .customOverride:
- let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomOverrideCollectionViewCell.className, for: indexPath) as! CustomOverrideCollectionViewCell
- cell.titleLabel.text = customName
- if isEditingPresets {
- cell.applyOverlayToFade(animated: false)
- }
- return cell
- case .history:
- let cell = collectionView.dequeueReusableCell(withReuseIdentifier: OverrideHistoryCollectionViewCell.className, for: indexPath) as! OverrideHistoryCollectionViewCell
- cell.titleLabel.text = historyLabel
- cell.titleLabel.accessibilityLabel = historyLabel
- if isEditingPresets {
- cell.applyOverlayToFade(animated: false)
- }
- return cell
- }
- }
- private func configure(_ cell: OverridePresetCollectionViewCell, with settings: TemporaryScheduleOverrideSettings, duration: TemporaryScheduleOverride.Duration) {
- if let targetRange = settings.targetRange {
- cell.targetRangeLabel.text = makeTargetRangeText(from: targetRange)
- } else {
- cell.targetRangeLabel.isHidden = true
- }
- if let insulinNeedsScaleFactor = settings.insulinNeedsScaleFactor {
- cell.insulinNeedsBar.progress = insulinNeedsScaleFactor
- } else {
- cell.insulinNeedsBar.isHidden = true
- }
- switch duration {
- case .finite(let interval):
- cell.durationLabel.text = durationFormatter.string(from: interval)
- case .indefinite:
- cell.durationLabel.text = "∞"
- }
- }
- private func makeTargetRangeText(from targetRange: ClosedRange<HKQuantity>) -> String {
- guard
- let minTarget = glucoseNumberFormatter.string(from: targetRange.lowerBound.doubleValue(for: glucoseUnit)),
- let maxTarget = glucoseNumberFormatter.string(from: targetRange.upperBound.doubleValue(for: glucoseUnit))
- else {
- return ""
- }
- 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())
- }
- public override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
- if isEditingPresets {
- switch cellContent(for: indexPath) {
- case .scheduledOverride, .customOverride, .history:
- break
- case .preset(let preset):
- let editVC = AddEditOverrideTableViewController(glucoseUnit: glucoseUnit)
- editVC.inputMode = .editPreset(preset)
- editVC.delegate = self
- show(editVC, sender: collectionView.cellForItem(at: indexPath))
- }
- } else {
- switch cellContent(for: indexPath) {
- case .scheduledOverride(let override):
- let editOverrideVC = AddEditOverrideTableViewController(glucoseUnit: glucoseUnit)
- editOverrideVC.inputMode = .editOverride(override)
- editOverrideVC.customDismissalMode = .dismissModal
- editOverrideVC.delegate = self
- show(editOverrideVC, sender: collectionView.cellForItem(at: indexPath))
- case .preset(let preset):
- delegate?.overrideSelectionViewController(self, didConfirmPreset: preset)
- dismiss(animated: true)
- case .customOverride:
- let customOverrideVC = AddEditOverrideTableViewController(glucoseUnit: glucoseUnit)
- customOverrideVC.inputMode = .customOverride
- customOverrideVC.delegate = self
- show(customOverrideVC, sender: collectionView.cellForItem(at: indexPath))
- case .history:
- let model = OverrideHistoryViewModel(
- overrides: overrideHistory,
- glucoseUnit: glucoseUnit
- )
- let overrideHistoryView = OverrideSelectionHistory(model: model)
- let hostedView = UIHostingController(rootView: overrideHistoryView)
- hostedView.title = LocalizedString("Override History", comment: "Title for override history view") // Hack to fix animations
- navigationController?.pushViewController(hostedView, animated: true)
- }
- }
- }
- @objc private func addNewPreset() {
- let addVC = AddEditOverrideTableViewController(glucoseUnit: glucoseUnit)
- addVC.inputMode = .newPreset
- addVC.delegate = self
- let navigationWrapper = UINavigationController(rootViewController: addVC)
- present(navigationWrapper, animated: true)
- }
- private var isEditingPresets = false {
- didSet {
- saveButton.isEnabled = !isEditingPresets
- cancelButton.isEnabled = !isEditingPresets
- }
- }
- @objc private func beginEditing() {
- isEditingPresets = true
- navigationItem.setRightBarButtonItems([saveButton, doneButton], animated: true)
- configureCellsForEditingChanged()
- if let scheduledOverrideSection = sections.firstIndex(of: .scheduledOverride) {
- let scheduledOverrideIndexPath = IndexPath(row: 0, section: scheduledOverrideSection)
- guard let scheduledOverrideCell = collectionView.cellForItem(at: scheduledOverrideIndexPath) as? OverridePresetCollectionViewCell else {
- return
- }
- scheduledOverrideCell.applyOverlayToFade(animated: true)
- }
- if let customOverrideCell = collectionView.cellForItem(at: indexPathOfCustomOverride()) as? CustomOverrideCollectionViewCell {
- customOverrideCell.applyOverlayToFade(animated: true)
- }
- }
- @objc private func endEditing() {
- isEditingPresets = false
- navigationItem.setRightBarButtonItems([saveButton, editButton], animated: true)
- configureCellsForEditingChanged()
- if let scheduledOverrideSection = sections.firstIndex(of: .scheduledOverride) {
- let scheduledOverrideIndexPath = IndexPath(row: 0, section: scheduledOverrideSection)
- guard let scheduledOverrideCell = collectionView.cellForItem(at: scheduledOverrideIndexPath) as? OverridePresetCollectionViewCell else {
- return
- }
- scheduledOverrideCell.removeOverlay(animated: true)
- }
- if let customOverrideCell = collectionView.cellForItem(at: indexPathOfCustomOverride()) as? CustomOverrideCollectionViewCell {
- customOverrideCell.removeOverlay(animated: true)
- }
- }
- private func configureCellsForEditingChanged() {
- for indexPath in collectionView.indexPathsForVisibleItems where indexPath.section == presetSection {
- if let cell = collectionView.cellForItem(at: indexPath) as? OverridePresetCollectionViewCell {
- if isEditingPresets {
- cell.configureForEditing(animated: true)
- } else {
- cell.configureForStandard(animated: true)
- }
- }
- }
- }
- public override func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool {
- if !isEditingPresets {
- return true
- }
- switch cellContent(for: indexPath) {
- case .scheduledOverride, .customOverride, .history:
- return false
- case .preset:
- return true
- }
- }
- public override func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
- isEditingPresets
- && indexPath.section == presetSection
- && indexPath != indexPathOfCustomOverride()
- }
- public override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
- let movedPreset = presets.remove(at: sourceIndexPath.row)
- presets.insert(movedPreset, at: destinationIndexPath.row)
- }
- public override func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath {
- guard proposedIndexPath.section == sections.firstIndex(of: .presets) else {
- return originalIndexPath
- }
- return proposedIndexPath == indexPathOfCustomOverride()
- ? originalIndexPath
- : proposedIndexPath
- }
- }
- extension OverrideSelectionViewController: UICollectionViewDelegateFlowLayout {
- private var sectionInsets: UIEdgeInsets {
- return UIEdgeInsets(top: 0, left: 12, bottom: 12, right: 12)
- }
- public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
- guard presets.isEmpty else { return .zero }
- return CGSize(width: collectionView.frame.width, height: 50)
- }
- public func collectionView(
- _ collectionView: UICollectionView,
- layout collectionViewLayout: UICollectionViewLayout,
- sizeForItemAt indexPath: IndexPath
- ) -> CGSize {
- let paddingSpace = sectionInsets.left * 2
- let width = view.frame.width - paddingSpace
- let height: CGFloat
- switch cellContent(for: indexPath) {
- case .scheduledOverride, .preset:
- height = 76
- case .customOverride, .history:
- height = 52
- }
- return CGSize(width: width, height: height)
- }
- public func collectionView(
- _ collectionView: UICollectionView,
- layout collectionViewLayout: UICollectionViewLayout,
- insetForSectionAt section: Int
- ) -> UIEdgeInsets {
- return sectionInsets
- }
- public func collectionView(
- _ collectionView: UICollectionView,
- layout collectionViewLayout: UICollectionViewLayout,
- minimumLineSpacingForSectionAt section: Int
- ) -> CGFloat {
- return sectionInsets.left
- }
- }
- extension OverrideSelectionViewController: AddEditOverrideTableViewControllerDelegate {
- public func addEditOverrideTableViewController(_ vc: AddEditOverrideTableViewController, didSavePreset preset: TemporaryScheduleOverridePreset) {
- if let selectedIndexPath = collectionView.indexPathsForSelectedItems?.first {
- presets[selectedIndexPath.row] = preset
- collectionView.reloadItems(at: [selectedIndexPath])
- collectionView.deselectItem(at: selectedIndexPath, animated: true)
- } else {
- presets.append(preset)
- collectionView.insertItems(at: [IndexPath(row: presets.endIndex - 1, section: presetSection)])
- delegate?.overrideSelectionViewController(self, didUpdatePresets: presets)
- }
- }
- public func addEditOverrideTableViewController(_ vc: AddEditOverrideTableViewController, didSaveOverride override: TemporaryScheduleOverride) {
- delegate?.overrideSelectionViewController(self, didConfirmOverride: override)
- }
- public func addEditOverrideTableViewController(_ vc: AddEditOverrideTableViewController, didCancelOverride override: TemporaryScheduleOverride) {
- delegate?.overrideSelectionViewController(self, didCancelOverride: override)
- }
- }
- extension OverrideSelectionViewController: OverridePresetCollectionViewCellDelegate {
- func overridePresetCollectionViewCellDidScheduleOverride(_ cell: OverridePresetCollectionViewCell) {
- guard
- let indexPath = collectionView.indexPath(for: cell),
- case .preset(let preset) = cellContent(for: indexPath)
- else {
- return
- }
- let customizePresetVC = AddEditOverrideTableViewController(glucoseUnit: glucoseUnit)
- customizePresetVC.inputMode = .customizePresetOverride(preset)
- customizePresetVC.delegate = self
- show(customizePresetVC, sender: nil)
- }
- func overridePresetCollectionViewCellDidPerformFirstDeletionStep(_ cell: OverridePresetCollectionViewCell) {
- for case let visibleCell as OverridePresetCollectionViewCell in collectionView.visibleCells
- where visibleCell !== cell && visibleCell.isShowingFinalDeleteConfirmation
- {
- visibleCell.configureForEditing(animated: true)
- }
- }
- func overridePresetCollectionViewCellDidDeletePreset(_ cell: OverridePresetCollectionViewCell) {
- guard let indexPath = collectionView.indexPath(for: cell) else {
- return
- }
- presets.remove(at: indexPath.row)
- if let name = cell.nameLabel.text {
- INInteraction.delete(with: name) { (error) in
- if let error = error {
- os_log(.error, "Failed to delete intent: %{public}@", String(describing: error))
- }
- }
- }
-
- collectionView.deleteItems(at: [indexPath])
- }
- }
- private extension Array where Element: Equatable {
- mutating func remove(_ element: Element) {
- if let index = self.firstIndex(of: element) {
- remove(at: index)
- }
- }
- }
|