Skip to content

Instantly share code, notes, and snippets.

@JasonCanCode
Last active August 26, 2019 20:05
Show Gist options
  • Save JasonCanCode/3cc1acc9ac2aec022634c847ba6449fc to your computer and use it in GitHub Desktop.
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.
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