// // 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() quantityFormatter.setPreferredNumberFormatter(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) -> 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.string(from: glucoseUnit)) } 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 = OverrideHistorystate( 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) } } }