| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286 |
- //
- // DailyValueScheduleTableViewController.swift
- // Naterade
- //
- // Created by Nathan Racklyeft on 2/6/16.
- // Copyright © 2016 Nathan Racklyeft. All rights reserved.
- //
- import UIKit
- import LoopKit
- public protocol DailyValueScheduleTableViewControllerDelegate: AnyObject {
- func dailyValueScheduleTableViewControllerWillFinishUpdating(_ controller: DailyValueScheduleTableViewController)
- }
- func insertableIndices<T>(for scheduleItems: [RepeatingScheduleValue<T>], removing row: Int, with interval: TimeInterval) -> [Bool] {
- let insertableIndices = scheduleItems.enumerated().map { (enumeration) -> Bool in
- let (index, item) = enumeration
- if row == index {
- return true
- } else if index == 0 {
- return false
- } else if index == scheduleItems.endIndex - 1 {
- return item.startTime < TimeInterval(hours: 24) - interval
- } else if index > row {
- return scheduleItems[index + 1].startTime - item.startTime > interval
- } else {
- return item.startTime - scheduleItems[index - 1].startTime > interval
- }
- }
- return insertableIndices
- }
- open class DailyValueScheduleTableViewController: UITableViewController, DatePickerTableViewCellDelegate {
- private var keyboardWillShowNotificationObserver: Any?
- public convenience init() {
- self.init(style: .plain)
- }
- open override func viewDidLoad() {
- super.viewDidLoad()
- tableView.rowHeight = UITableView.automaticDimension
- tableView.estimatedRowHeight = 44
- if !isReadOnly {
- navigationItem.rightBarButtonItems = [insertButtonItem, editButtonItem]
- }
- tableView.keyboardDismissMode = .onDrag
- keyboardWillShowNotificationObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: OperationQueue.main, using: { [weak self] (note) -> Void in
- guard let strongSelf = self else {
- return
- }
- guard note.userInfo?[UIResponder.keyboardIsLocalUserInfoKey] as? Bool == true else {
- return
- }
- let animated = note.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double ?? 0 > 0
- if let indexPath = strongSelf.tableView.indexPathForSelectedRow {
- strongSelf.tableView.beginUpdates()
- strongSelf.tableView.deselectRow(at: indexPath, animated: animated)
- strongSelf.tableView.endUpdates()
- }
- })
- }
- open override func setEditing(_ editing: Bool, animated: Bool) {
- if let indexPath = tableView.indexPathForSelectedRow {
- tableView.beginUpdates()
- tableView.deselectRow(at: indexPath, animated: animated)
- tableView.endUpdates()
- }
- tableView.endEditing(false)
- navigationItem.rightBarButtonItems?[0].isEnabled = !editing
- super.setEditing(editing, animated: animated)
- }
- deinit {
- if let observer = keyboardWillShowNotificationObserver {
- NotificationCenter.default.removeObserver(observer)
- }
- }
- open override func viewWillDisappear(_ animated: Bool) {
- super.viewWillDisappear(animated)
- tableView.endEditing(true)
- }
- public weak var delegate: DailyValueScheduleTableViewControllerDelegate?
- public private(set) lazy var insertButtonItem: UIBarButtonItem = {
- return UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addScheduleItem(_:)))
- }()
- // MARK: - State
- public var timeZone = TimeZone.currentFixed {
- didSet {
- calendar.timeZone = timeZone
- let localTimeZone = TimeZone.current
- let timeZoneDiff = TimeInterval(timeZone.secondsFromGMT() - localTimeZone.secondsFromGMT())
- if timeZoneDiff != 0 {
- let localTimeZoneName = localTimeZone.abbreviation() ?? localTimeZone.identifier
- let formatter = DateComponentsFormatter()
- formatter.allowedUnits = [.hour, .minute]
- let diffString = formatter.string(from: abs(timeZoneDiff)) ?? String(abs(timeZoneDiff))
- navigationItem.prompt = String(
- format: LocalizedString("Times in %1$@%2$@%3$@", comment: "The schedule table view header describing the configured time zone difference from the default time zone. The substitution parameters are: (1: time zone name)(2: +/-)(3: time interval)"),
- localTimeZoneName, timeZoneDiff < 0 ? "-" : "+", diffString
- )
- }
- }
- }
- public var unitDisplayString: String = "U/hour"
- public var isReadOnly: Bool = false {
- didSet {
- if isReadOnly {
- isEditing = false
- }
- if isViewLoaded {
- navigationItem.setRightBarButtonItems(isReadOnly ? [] : [insertButtonItem, editButtonItem], animated: true)
- }
- }
- }
- private var calendar = Calendar.current
- var midnight: Date {
- return calendar.startOfDay(for: Date())
- }
- @objc func addScheduleItem(_ sender: Any?) {
- guard !isReadOnly else {
- return
- }
- // Updates the table view state. Subclasses should update their data model before calling super
- tableView.insertRows(at: [IndexPath(row: tableView.numberOfRows(inSection: 0), section: 0)], with: .automatic)
- }
- func insertableIndiciesByRemovingRow(_ row: Int, withInterval timeInterval: TimeInterval) -> [Bool] {
- fatalError("Subclasses must override \(#function)")
- }
- // MARK: - UITableViewDataSource
- open override func numberOfSections(in tableView: UITableView) -> Int {
- return 1
- }
- open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- fatalError("Subclasses must override \(#function)")
- }
- open override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
- return !isReadOnly && indexPath.section == 0 && indexPath.row > 0
- }
- open override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
- return self.tableView(tableView, canEditRowAt: indexPath)
- }
- open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- fatalError("Subclasses must override \(#function)")
- }
- open override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
- if editingStyle == .delete {
- // Updates the table view state. Subclasses should update their data model before calling super
- tableView.deleteRows(at: [indexPath], with: .automatic)
- }
- }
- // MARK: - UITableViewDelegate
- open override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
- guard indexPath.section == 0 else {
- return true
- }
- return !isReadOnly && indexPath.row > 0
- }
- open override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
- guard self.tableView(tableView, shouldHighlightRowAt: indexPath) else {
- return nil
- }
- tableView.endEditing(false)
- tableView.beginUpdates()
- hideDatePickerCells(excluding: indexPath)
- return indexPath
- }
- open override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
- tableView.beginUpdates()
- tableView.endUpdates()
- }
- open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- tableView.endEditing(false)
- tableView.endUpdates()
- tableView.deselectRow(at: indexPath, animated: true)
- }
- open override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
- guard sourceIndexPath.section == proposedDestinationIndexPath.section else {
- return sourceIndexPath
- }
- guard sourceIndexPath != proposedDestinationIndexPath, let cell = tableView.cellForRow(at: sourceIndexPath) as? RepeatingScheduleValueTableViewCell else {
- return proposedDestinationIndexPath
- }
- let interval = cell.datePickerInterval
- let indices = insertableIndiciesByRemovingRow(sourceIndexPath.row, withInterval: interval)
- let closestDestinationRow = indices.insertableIndex(closestTo: proposedDestinationIndexPath.row, from: sourceIndexPath.row)
- return IndexPath(row: closestDestinationRow, section: proposedDestinationIndexPath.section)
- }
- // MARK: - DatePickerTableViewCellDelegate
- public func datePickerTableViewCellDidUpdateDate(_ cell: DatePickerTableViewCell) {
- // Updates the TableView state. Subclasses should update their data model
- if let indexPath = tableView.indexPath(for: cell) {
- var indexPaths: [IndexPath] = []
- if indexPath.row > 0 {
- indexPaths.append(IndexPath(row: indexPath.row - 1, section: indexPath.section))
- }
- if indexPath.row < tableView.numberOfRows(inSection: 0) - 1 {
- indexPaths.append(IndexPath(row: indexPath.row + 1, section: indexPath.section))
- }
- DispatchQueue.main.async {
- self.tableView.reloadRows(at: indexPaths, with: .none)
- }
- }
- }
- }
- extension Array where Element == Bool {
- func insertableIndex(closestTo destination: Int, from source: Int) -> Int {
- if self[destination] {
- return destination
- } else {
- var closestRow = source
- for (index, valid) in self.enumerated() where valid {
- if abs(destination - index) < abs(destination - closestRow) {
- closestRow = index
- }
- }
- return closestRow
- }
- }
- }
|