How to handle Date in TextField?

Hi,

How to handle Date in TextField with all validations?

suppose if my date format is DD/MM/YYYY HH:mm,

and when i type the date ,for example 10

it should add /

and when i type month ,for example 10/12

it should add /

and when i type year ,for example 10/12/2019

it should add a space

and when i type the hour,for example 10/12/2019 10

it should add a colon

and then i can type the minutes like 10/12/2019 10:10


My code Snippet:

In textfield delegate method , shouldChangeCharactersIn ,


func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {



if textField.text?.count == 1 {

textField.placeholder = "DD/MM/YYYY HH:mm"

}


if (textField.text?.count == 2) || (textField.text?.count == 5) {

//Handle backspace being pressed

if !(string == "") {

txtValue.text = textField.text! + "/"

}

}else if (textField.text?.count == 10) {

if !(string == "") {

txtValue.text = textField.text! + " "

}

}else if (textField.text?.count == 13) {

if !(string == "") {

txtValue.text = textField.text! + ":"

}

}

return !(textField.text!.count > 15 && (string.count) > range.length)

}


The problem is ,

when backspace tapped from inbetween the date in the textfield,it should validate the date.How to handle this?


below gif image shows what is required.

Replies

I tested this code and have somme comments:


func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

     if textField.text?.count == 1 {
            textField.placeholder = "DD/MM/YYYY HH:mm"
     }

     if (textField.text?.count == 2) || (textField.text?.count == 5) {
                //Handle backspace being pressed
                if !(string == "") {
                    txtValue.text = textField.text! + "/"
                }
            } else if (textField.text?.count == 10) {
                if !(string == "") {
                    txtValue.text = textField.text! + " "
                }
            } else if (textField.text?.count == 13) {
                if !(string == "") {
                    txtValue.text = textField.text! + ":"
                }
            }
            return !(textField.text!.count > 15 && (string.count) > range.length)
}


Line 3: why 1 and not 0 length ?

It is surpriing for user to get the slash added after typing 3rd char.


Here is what I modified : when typing backspace, removes the '/' and the previous char.


You should also always check that date and time are valid (I did it for DD and MM and added a beep if input is not a valid DD)

Need to add some test (30/02 is not valid for instance)

That could be done once the whole date has been entered, by alerting user.


import AVFoundation

func playInputClick() {
    AudioServicesPlayAlertSound(SystemSoundID(1322))
}



   func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
       
        if textField.text?.count == 0 {
            textField.placeholder = "DD/MM/YYYY HH:mm"
        }
        // Test if valid data
        var isValid = true
        // Is DD valid ?
        if textField.text!.count <= 1 {
            let newString = txtValue.text! + string
            if let newValue = Int(newString) {
                isValid = ((textField.text!.count <= 1 && newValue >= 1) || (textField.text!.count == 0 && newValue <= 3)) && newValue <= 31
            } else {
                isValid = false // Not a number
            }
            if !isValid {
                playInputClick()
                return false
            }
        } else if textField.text!.count >= 3  && textField.text!.count <= 5 {    // Is MM Valid ?
            let newString = (txtValue.text! + string).dropFirst(3)  // removes 'DD/'
            if let newValue = Int(newString) {
                isValid = newValue >= 0 && newValue <= 12
            } else {
                isValid = false // Not a number
            }
            if !isValid {
                playInputClick()
                return false
            }
        }
       
        //Handle backspace being pressed
        if string.count == 0 && (textField.text?.last == "/" || textField.text?.last == " " || textField.text?.last == ":") {
            txtValue.text = String((txtValue.text?.dropLast(2)) ?? "")
            return false
        }
        if (textField.text?.count == 1) || (textField.text?.count == 4) {
            if string != "" {
                txtValue.text = textField.text! + string + "/"
                return false
            }
        } else if (textField.text?.count == 9) {
            if string != "" {
                txtValue.text = textField.text! + string + " "
                return false
            }
        } else if (textField.text?.count == 12) {
            if string != "" {
                txtValue.text = textField.text! + string + ":"
                return false
            }
        }
        return !(textField.text!.count > 15 && (string.count) > range.length)
    }

You wrote:

The problem is ,

when backspace tapped from inbetween the date in the textfield,it should validate the date.How to handle this?

Why validate ? Do you mean invalidate ? User may just want to correct an error ; the code I wrote allows such correction.

Validate is with return, not backspace


below gif image shows what is required.

Images do not show on forum.

>below gif image shows what is required.


You can add them....we're not allowed to see them.


Please see FAQ 2 here: For Best Results - Read the Label

Allowing the user to type freeform dates is really hard. Really really hard. You might be able to make this work in some limited circumstances but getting it to work for all users will require massive engineering effort. I strongly recommend that you rethink your UI, preferably switching to something based on

UIDatePicker
.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

ps DTS is closed 25…29 Nov in observance of the US Thanksgiving holiday.

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)
    }

}

Hi All,

Thank you all for your valuable support.

My date format is DD/MM/YYYY HH:mm.

How to prevent deleting slash and colon and to provide valid/handle date?

My date format is DD/MM/YYYY HH:mm.

All the code is dealing exactly with this. Isn't it ?


How to prevent deleting slash and colon

All the code is dealing exactly with this. Isn't it ?

Prevent deleting when ?


and to provide valid/handle date?

The code tests for date validity on the fly and at the end. What do you mean ?


Did you try the code ?

Tell very precisely what you expect and what you get that is different, not with a general statement but with precise details.

I played with it to get a complete (and hopefully correct) implementation.

I haven’t looked at your code in detail but a quick check reveals that it runs into two of the standard pitfalls:

  • You’re assuming that the user is using the Gregorian calendar, but you’ve written the code to use

    Calendar.current
    . Switch your iOS device to the Buddhist calendar and see how things behave.
  • The code only deals with Wester Arabic digits. Try it with a locale that defaults to Eastern Arabic ones.

But I find it a bit boring to use to enter dates far from a basic date (eg: it is much more tedious to select 20/02/1960 10:10 on picker than typing 200219601010.)

Fair enough, but you either have to support everything that

UIDatePicker
supports or document your limitations. What you want to avoid is creating something that works in common situations and then blows up in the field when the user does something unusual. For example, the story I related in this thread.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

ps DTS is closed 25…29 Nov in observance of the US Thanksgiving holiday.

Thanks for the feedback. You're right.


For my learning, I have set the device as Saudi Arabia region and buddhist calendar (could not find more appropriate one).

When I run and test with a date (12/12/2017 10:10) that is entered according to the placeholder instructions (dd/mm/yyyy hh:mn), it returns the correct date (Monday, Dec 12, 2017 @10:10). Should it be different ?

Or do you mean that in a diiferent locale, user may expect to enter a date in a format different from placeholder's one (dd/mm/yyyy hh:mn), like today's year as 2562 in Budhist calendar ?


Anyway, those advices will be useful for the post author if he or she decides to use the code. Or better selects Date picker. Up to him or her.

Hi! I understand your ask and I supposed to be that are following something like a mockup. But you could put the UIToolbar with UIPickerView with date and get the date to push in your textField.

I don’t have time to do an end-to-end test today, so I’m going to focus on some snippets. Consider the code below, modelled after lines 14…17 of the code in your 26 Nov post:

let calendar = Calendar(identifier: .buddhist)
let dateComponents = DateComponents(calendar: calendar, year: 2019, month: 11, day: 28, hour: 1, minute: 2)
let date = calendar.date(from: dateComponents)!
print(date) // -> 1476-11-28 01:03:15 +0000

As you can see, the result is off by roughly 500 years.

Now consider the code below, modelled after lines 66…73:

var isValid: Bool
let newString = "\u{0661}" + "\u{0662}"
if let newValue = Int(newString) {
    …
    isValid = true
} else {
    isValid = false // Not a number
}
print(isValid)  // always false

Here we’re supposing that the text field contains U+0661 ARABIC-INDIC DIGIT ONE and the user has typed U+0662 ARABIC-INDIC DIGIT TWO. The

Int(_:)
initialiser only handles Western Arabic digits, and thus always returns
nil
in the presence of Eastern Arabic digts, and thus
isValid
is never true.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

ps DTS is closed 25…29 Nov in observance of the US Thanksgiving holiday.

Thanks a lot.


That does make a lot of sense.


On the other hand, I played with a DatePicker, trying to set the date as 1/1/0001. Not really easy…

On the other hand, I played with a DatePicker, trying to set the date as 1/1/0001. Not really easy…

Indeed. Then again, if you try to do anything useful with a date back that far you will run into calendar problems. Let me introduce you to the terrifying concept that is the proleptic Gregorian calendar.

Also, if you’re working with a worldwide audience, you have to be careful about any date before 1930. Before then it’s possible you might be working with Julian dates. For example, Lenin’s birthday was 10 Apr and he was born in 1870, but the actual date of his birth was 22 April 1870 (Gregorian).

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

It is a bit fascinating.


How is this handled in DatePicker ? I do not see any jump between Feb 19, ,1500 (a wednesday) and Feb 20, 1500 (a thursday)

It is a bit fascinating.

Yeah.

I do not see any jump between Feb 19, 1500 (a wednesday) and Feb 20, 1500 (a thursday)

The Gregorian calendar was first rolled out in Oct 1582. Thu, 4 Oct 1582 was followed by Fri, 15 Oct 1582.

UIDatePicker
definitely knows about this. Consider the following test program:
import UIKit

func d(_ string: String) -> Date {
    let df = ISO8601DateFormatter()
    df.formatOptions = [.withInternetDateTime]
    return df.date(from: string)!
}

class ViewController: UIViewController {

    @IBOutlet var datePicker: UIDatePicker!

    override func viewDidLoad() {
        super.viewDidLoad()
        self.datePicker.datePickerMode = .date
        self.datePicker.date = d("1582-10-31T01:01:01Z")
    }
}

If you wind the day back to 15, everything acts normally. Then wind the day back one more to 14. It ‘bounces’ to 23 (I’m not sure what the logic is there). Similarly for 14 through 5. Now go back to 15 and, from there, wind back to 4. This ‘sticks’.

Of course, the Gregorian calendar wasn’t adopted simultaneously everywhere, so the correct behaviour here very much depends on user expectations. And, indeed, the user might not fully understand the problem. A user in Greece entering their grandparent’s birthday into a genealogy app might not grok that the birthday is relative to the Julian calendar.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"