DatePicker is certainly easier and safer.
But I find it a bit boring to use to enter dates far from a basic date (eg: it is much more tedious to selecti 20/02/1960 10:10 on picker than typing 200219601010.)
But that does requre more effort in code to make sure date is valid…
So, for the fun, I played with it to get a complete (and hopefully correct) implementation.
Heavily commented as it is hard to follow in some parts
import AVFoundation
func playInputClick() {
AudioServicesPlayAlertSound(SystemSoundID(1322))
}
@IBOutlet weak var dateInputField: UITextField! // date input field
@IBOutlet weak var dateLabel: UILabel! // Date in clear
private var dd = 0 // The day for testing
private var mm = 0 // The month for testing
private var yyyy = 0 // The year for testing
private var hh = 0 // The hour for testing
private var mn = 0 // The minutes for testing
let yearMini = 1 // Allow range from 1 to 2099
let yearMaxi = 2099 // Never more than 9999 (4 digits)
override func viewDidLoad() {
super.viewDidLoad()
dateLabel.text = "Test a date as dd/mm/yyyy hh:mn
year between \(yearMini) and \(yearMaxi)"
}
// MARK:- TextView Delegate Methods
// --------------------- dateEntered ----------------------------------------------------
// Description: Date completed. Convert as full date (using dd, mm, yyyy, hh, mn).
// Parameters
// sender: UITextField
// Comments:
// Called by hitting return (EditingDidEnd)
// autoEnable return key in IB
// -------------------------------------------------------------------------------------------------
@IBAction func dateEntered(_ sender: UITextField) {
let calendar = Calendar.current
let dateComponents = DateComponents(calendar: calendar, year: self.yyyy, month: self.mm, day: self.dd, hour: self.hh, minute: self.mn)
let date = calendar.date(from: dateComponents)!
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM" // Just get the month, for leap year verification
let realMonth = Int(dateFormatter.string(from: date))
if realMonth == nil || realMonth! != self.mm {
dateLabel.textColor = .red // For 29/2 on a non-leap year
playInputClick()
} else {
dateLabel.textColor = .blue
}
dateFormatter.dateFormat = "EEEE, MMM d, yyyy @HH:mm"
dateLabel.text = dateFormatter.string(from: date)
}
// --------------------- startEditingDate ----------------------------------------------------
// Description: Starts editing: reset placeholder in label
// Parameters
// sender: UITextField
// Comments:
// Called on EditingChanged event, incl Clear button
// Called on EditingDidBegin
// -------------------------------------------------------------------------------------------------
@IBAction func startEditingDate(_ sender: UITextField) {
if sender.text == nil || sender.text == "" {
dateLabel.text = "Test a date as dd/mm/yyyy hh:mn
year between \(yearMini) and \(yearMaxi)"
dateLabel.textColor = .black
}
}
// --------------------- shouldChangeCharactersIn ----------------------------------------------------
// Description: Check that date components are correct, on the fly
// Parameters
// textField: UITextField txtValue
// shouldChangeCharactersIn range: NSRange
// replacementString string: String le dernier char tapé
// Comments:
// -------------------------------------------------------------------------------------------------
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// Test if valid data
var isValid = true
// Is DD valid ?
if textField.text!.count <= 1 { // Just 1 char max has been typed, we analyse the next one typed (string)
// if we had dd/ already, count >= 2, not this case
let newString = dateInputField.text! + string // backspace inserts nothing
if let newValue = Int(newString) { // So we typed something in newString and it is a valid number
// a valid dd is always between 1 and 31
// But if textField.text!.count == 0, first char may be 0, waiting for the second
isValid = ((textField.text!.count <= 1 && newValue >= 1) || (textField.text!.count == 0 && newValue <= 3)) && newValue <= 31
self.dd = newValue // For further use
} else { // Did not type a Int
isValid = false // Not a number
}
if !isValid { // Not valid, let's beep and return false: we ignore the typing
playInputClick()
return false
}
} else if textField.text!.count >= 3 && textField.text!.count <= 4 { // Between 3 and 4 char max have been typed or inserted (dd/ to dd/m), we analyse the next one typed (string)
// backspace inserts nothing: string is ""
// Is MM Valid ?
let newString = String((dateInputField.text! + string).dropFirst(3)) // removes 'DD/'
if string == "" { // case of backspace
// skip if we typed backspace after a /
// isValid remains true, we do nothing
} else { // we had txtValue.text! + string = dd/ and something
if let newValue = Int(newString) {
// So we typed something in MM (newString) and it is a valid number
// a valid mm must be between 1 and 12
// But if textField.text!.count == 3, first char of newString may be 0, waiting for the second
// isValid = newValue >= 0 && newValue <= 12 Not precise enough
// If we backspace at dd/m, we have 4 chars, string is "", but m may be 0: do not require newvalue >= 1
isValid = textField.text!.count == 3 || (textField.text!.count == 4 && (newValue >= 1 || string == "") && newValue <= 12)
self.mm = newValue
// If we have typed a non zero value, is it a valid month ?
if isValid { // Some months have 30 days max
if [4, 6, 9, 11].contains(mm) {
isValid = dd <= 30
}
if mm == 2 {
isValid = dd <= 29 // May be a leap year: will be handled when displaying label
}
}
} else {
isValid = false // mm Not a number
}
if !isValid {
playInputClick()
return false
}
} // continue test a date as dd/mm/yyyy hh:mm
} else if textField.text!.count >= 6 && textField.text!.count <= 9 { // Between 6 and 9 char max have been typed or inserted (dd/mm/ to dd/mm/yyy), we analyse the next one typed (string)
// backspace inserts nothing: string is ""
// Is YYYY Valid ?
let newString = String((dateInputField.text! + string).dropFirst(6)) // removes 'DD/mm/'
if string == "" { // case of backspace
// skip if we typed backspace after a /
// isValid remains true, we do nothing
} else { // we had txtValue.text! + string = dd/mm/ and something
if let newValue = Int(newString) {
// So we typed something in YYYY (newString) and it is a valid number
// a valid yyyy must be between yearMini and yearMaxi ?
// if textField.text!.count == 6, first char of newString can be 0, waiting for the second
isValid = newValue >= 1 && newValue <= yearMaxi // Not precise enough
isValid = isValid || (newValue == 0 && string == "") // Typed backspace when year isonly leading 0
// if textField.text!.count == 6, first char of newString can also be 0, waiting for the second or third or fourth
isValid = isValid || (textField.text!.count >= 6 && textField.text!.count <= 8 && string == "0")
// If we backspace at dd/m, we have 4 chars, string is "", but m may be 0: do not require newvalue >= 1
self.yyyy = newValue
// If we have typed a complete yyyy, is it a valid year ? backspace remains possible
if isValid && textField.text!.count == 9 && string != "" { // Must be between yearMini and yearMaxi
isValid = yyyy >= yearMini && yyyy <= yearMaxi
}
} else {
isValid = false // yyyy Not a number
}
if !isValid {
playInputClick()
return false
}
}
// Now test time
} else if textField.text!.count >= 11 && textField.text!.count <= 12 { // Between 11 and 12 char max have been typed or inserted (dd/mm/yyyy to dd/mm/yyyy h), we analyse the next one typed (string)
// backspace inserts nothing: string is ""
// Is hh Valid ?
let newString = String((dateInputField.text! + string).dropFirst(11)) // removes 'DD/MM/YYYY '
if string == "" { // case of backspace
// skip if we typed backspace after a space
// isValid remains true, we do nothing
} else { // we had txtValue.text! + string = dd/mm/yyyy_ and something
if let newValue = Int(newString) {
// So we typed something in hh:mm (newString) and it is a valid number
// a valid hh must be between 0 and 23
// if textField.text!.count == 11, first char of newString can be 0, waiting for the second
isValid = newValue >= 0 && newValue <= 23
self.hh = newValue
// If we have typed a complete hh, is it a valid hour ? backspace remains possible
} else {
isValid = false // yyyy Not a number
}
if !isValid {
playInputClick()
return false
}
}
} else if textField.text!.count >= 14 && textField.text!.count <= 15 { // Between 14 and 15 char max have been typed or inserted (dd/mm/yyyy hh: to dd/mm/yyyy hh:m), we analyse the next one typed (string)
// backspace inserts nothing: string is ""
// Is mn Valid ?
let newString = String((dateInputField.text! + string).dropFirst(14)) // removes 'DD/MM/YYYY hh:'
if string == "" { // case of backspace
// skip if we typed backspace after a :
// isValid remains true, we do nothing
} else { // we had txtValue.text! + string = dd/mm/yyyy hh: and something
if let newValue = Int(newString) {
// So we typed something in hh:mm (newString) and it is a valid number
// a valid hh must be between 0 and 59
// if textField.text!.count == 14, first char of newString can be 0, waiting for the second
isValid = newValue >= 0 && newValue <= 59
self.mn = newValue
// If we have typed a complete mn, is it a valid minuyes number ? backspace remains possible
} else {
isValid = false // yyyy Not a number
}
if !isValid {
playInputClick()
return false
}
}
}
//Handle backspace being pressed : string.count == 0
// We remove the special character that was added plus the new that was typed
if string.count == 0 && (textField.text?.last == "/" || textField.text?.last == " " || textField.text?.last == ":") {
dateInputField.text = String((dateInputField.text?.dropLast(2)) ?? "")
return false
}
if (textField.text?.count == 1) || (textField.text?.count == 4) {
if string != "" {
// We did not type a backspace after d or after dd/m: we add the typed char (string) and a new '/' separator
dateInputField.text = textField.text! + string + "/"
return false
}
} else if (textField.text?.count == 9) {
// We have dd/mm/yyy and typed a new char
if string != "" { // Not a backspace: let's add " " after yyyy
dateInputField.text = textField.text! + string + " "
return false
}
} else if (textField.text?.count == 12) {
// We have dd/mm/yyyy h and typed a new char
if string != "" { // Not a backspace: let's add ":" after hh
dateInputField.text = textField.text! + string + ":"
return false
}
}
if string == "\t" && textField.text!.count >= 15 { // "Tab": we force exit
dateEntered(textField)
textField.resignFirstResponder()
return false
}
if string == "\n" && textField.text!.count >= 15 { return true } // We hit 'Return' at the end, with a valid date
return !(textField.text!.count > 15 && (string.count) > range.length)
}
}