Last active
August 26, 2019 20:05
-
-
Save JasonCanCode/3cc1acc9ac2aec022634c847ba6449fc to your computer and use it in GitHub Desktop.
A UIPickerView subclass that behaves like a UIDatePicker but for time with seconds included. Military time is optional.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
class TimestampPickerView: UIPickerView { | |
private(set) var date: Date = Date() | |
var minimumDate: Date? | |
var maximumDate: Date? | |
var isMilitaryTime: Bool { | |
get { | |
return timestampDatasource?.isMilitaryTime ?? false | |
} | |
set { | |
timestampDatasource?.isMilitaryTime = newValue | |
reloadAllComponents() | |
} | |
} | |
private var timestampDatasource: TimestampPickerDatasource? | |
// MARK: - Initializers | |
init(frame: CGRect, date: Date, isMilitaryTime: Bool) { | |
super.init(frame: frame) | |
initialize(date: date, isMilitaryTime: isMilitaryTime) | |
} | |
override init(frame: CGRect) { | |
super.init(frame: frame) | |
initialize() | |
} | |
required init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
initialize() | |
} | |
private func initialize(date: Date = Date(), isMilitaryTime: Bool = false) { | |
timestampDatasource = TimestampPickerDatasource(date: date, isMilitaryTime: isMilitaryTime) | |
dataSource = timestampDatasource | |
delegate = timestampDatasource | |
setDate(date) | |
} | |
} | |
extension TimestampPickerView { | |
// MARK: - Public Computed Properties | |
var datePickerMode: UIDatePicker.Mode { | |
return .time | |
} | |
// MARK: - Public Functions | |
func setDate(_ date: Date, animated: Bool = false) { | |
self.date = date | |
let timeText = formatter.string(from: date) | |
let components = timeText.components(separatedBy: ":") | |
guard let componentOptions = timestampDatasource?.componentOptions, | |
components.count == componentOptions.count else { | |
return | |
} | |
reloadAllComponents() | |
for (i, text) in components.enumerated() { | |
let options = componentOptions[i] | |
if let row = options.firstIndex(of: text) { | |
selectRow(row, inComponent: i, animated: animated) | |
} | |
} | |
} | |
func validate() { | |
let newDate = dateFromTimestampText() | |
if let minimumDate = minimumDate, | |
newDate.compare(minimumDate) == .orderedAscending { | |
setDate(minimumDate, animated: true) | |
} else if let maximumDate = maximumDate, | |
newDate.compare(maximumDate) == .orderedDescending { | |
setDate(maximumDate, animated: true) | |
} else { | |
self.date = newDate | |
} | |
} | |
// MARK: - Private Computed Properties | |
private var timestampText: String { | |
guard let optionsCount = timestampDatasource?.componentOptions.count else { | |
assertionFailure("TimestampPicker could not determine the correct date.") | |
return "" | |
} | |
var timestampText = selection(forComponent: 0) | |
for i in 1 ..< optionsCount { | |
timestampText += ":\(selection(forComponent: i))" | |
} | |
return timestampText | |
} | |
private var formatter: DateFormatter { | |
let timeFormatter = DateFormatter() | |
if isMilitaryTime { | |
timeFormatter.dateFormat = "HH:mm:ss" | |
} else { | |
timeFormatter.dateFormat = "h:mm:ss:a" | |
} | |
return timeFormatter | |
} | |
// MARK: - Private Functions | |
private func selection(forComponent component: Int) -> String { | |
let row = selectedRow(inComponent: component) | |
guard let options = timestampDatasource?.componentOptions[component], | |
!options.isEmpty, | |
options.count > row else { | |
return "" | |
} | |
return options[row] | |
} | |
private func dateFromTimestampText() -> Date { | |
guard let correctTime = formatter.date(from: timestampText), | |
let newDate = VideoConnectionDatasource.newTimestampWithDateComponents(of: date, andTimeComponentsOf: correctTime) else { | |
assertionFailure("TimestampPicker could not determine the correct date.") | |
return Date() | |
} | |
return newDate | |
} | |
} | |
/// If you want to generically use this picker with UIDatePickers | |
protocol DateTimePickerType: class { | |
var datePickerMode: UIDatePicker.Mode { get } | |
var date: Date { get } | |
var isHidden: Bool { get set } | |
var minimumDate: Date? { get set } | |
var maximumDate: Date? { get set } | |
func setDate(_ date: Date, animated: Bool) | |
} | |
extension UIDatePicker: DateTimePickerType {} | |
extension TimestampPickerView: DateTimePickerType {} | |
// MARK: - Custom Datasource | |
private class TimestampPickerDatasource: NSObject { | |
var isMilitaryTime: Bool { | |
didSet { | |
updateHours() | |
} | |
} | |
private var hourOptions: [String] = [] | |
private let minuteOptions: [String] = Array(0...59).map { String(format: "%02d", $0) } | |
private let seccondOptions: [String] = Array(0...59).map { String(format: "%02d", $0) } | |
private let meridiemOptions: [String] = ["AM", "PM"] | |
fileprivate var componentOptions: [[String]] { | |
var options = [ | |
hourOptions, | |
minuteOptions, | |
seccondOptions | |
] | |
if !isMilitaryTime { | |
options.append(meridiemOptions) | |
} | |
return options | |
} | |
init(date: Date, isMilitaryTime: Bool) { | |
self.isMilitaryTime = isMilitaryTime | |
super.init() | |
updateHours() | |
} | |
private func updateHours() { | |
if isMilitaryTime { | |
hourOptions = Array(0...23).map { String(format: "%02d", $0) } | |
} else { | |
hourOptions = Array(1...12).map { String($0) } | |
} | |
} | |
} | |
extension TimestampPickerDatasource: UIPickerViewDataSource, UIPickerViewDelegate { | |
func numberOfComponents(in pickerView: UIPickerView) -> Int { | |
return componentOptions.count | |
} | |
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { | |
return componentOptions[component].count | |
} | |
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { | |
return componentOptions[component][row] | |
} | |
func pickerView(_ pickerView: UIPickerView, didSelectRow: Int, inComponent: Int) { | |
if let timestampPicker = pickerView as? TimestampPickerView { | |
timestampPicker.validate() | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment