iosswiftdatepickeruicontrolevents

Date Picker with UIControlEvents.valueChanged doesn't work on the first value change event


I'm using the code below to learn about Date Picker. It works, but the weird thing is the expected changes will not happen when I scroll the date picker for the first time. After that it will work normally. Wonder what happened.

import UIKit
import Foundation

class ViewController: UIViewController {

// Place this code inside your ViewController or where you want ;)

var txtField: UITextField = UITextField(frame: CGRect(x: 100, y: 0, width: 300, height: 50))

override func viewDidLoad() {
    super.viewDidLoad()

    // date picker setup
    let datePickerView:UIDatePicker = UIDatePicker()

    // choose the mode you want
    // other options are: DateAndTime, Time, CountDownTimer
    datePickerView.datePickerMode = UIDatePickerMode.countDownTimer

    // choose your locale or leave the default (system)
    //datePickerView.locale = NSLocale.init(localeIdentifier: "it_IT")

    datePickerView.addTarget(self, action: #selector(onDatePickerValueChanged(_:)), for: UIControlEvents.valueChanged)
    txtField.inputView = datePickerView

    // datepicker toolbar setup
    let toolBar = UIToolbar()
    toolBar.barStyle = UIBarStyle.default
    toolBar.isTranslucent = true
    let space = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.flexibleSpace, target: nil, action: nil)
    let doneButton = UIBarButtonItem(title: "Done", style: UIBarButtonItemStyle.done, target: self, action: #selector(ViewController.doneDatePickerPressed))

    // if you remove the space element, the "done" button will be left aligned
    // you can add more items if you want
    toolBar.setItems([space, doneButton], animated: false)
    toolBar.isUserInteractionEnabled = true
    toolBar.sizeToFit()

    txtField.inputAccessoryView = toolBar

    self.view.addSubview(txtField)
}

func doneDatePickerPressed(){
    self.view.endEditing(true)
}

func onDatePickerValueChanged(_ sender: UIDatePicker) {
    let (h, m, _) = secondsToHoursMinutesSeconds (duration: sender.countDownDuration)
    self.txtField.text = String("\(h) Hours  \(m) Minutes")
}

func secondsToHoursMinutesSeconds (duration : Double) -> (Int, Int, Int) {
    let seconds:Int = Int(duration)
    return (seconds / 3600, (seconds % 3600) / 60, (seconds % 3600) % 60)
}

}

Solution

  • Strange. This appears to be a bug in date pickers that are set to UIDatePickerMode.countDownTimer mode.

    You can get around most of the fail cases by implementing the UITextFieldDelegate method textFieldShouldBeginEditing(_:) and setting the picker's value after a short delay. Here is the code from my test project. (In my project the time interval field is called optionalTimeField, and the time interval value is an optional called optionalTime. You'd need to change it to fit your code:

    EDIT:

    See new function textField(_:,shouldChangeCharactersInRange:,replacementString:) below

    extension ViewController: UITextFieldDelegate {
      
       //Add this function to prevent keyboard editing
       func textField(_ textField: UITextField, 
               shouldChangeCharactersIn range: NSRange, 
               replacementString string: String) -> Bool {
           return false
      }
    
      func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
        if textField === optionalTimeField {
          //Set the time interval of the date picker after a short delay
          DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            self.datePickerView.countDownDuration = self.optionalTime ?? 60
          }
          
        }
        return true
      }
    }
    

    I would suggest filing a bug report with Apple bug reporter. Based on the thread @Santosh linked, it looks like this bug has been in the UIDatePicker since iOS 7, which is bad.

    One bug that I don't see a work-around for is that if you try to select a time interval of 0 hours, 0 minutes, it switches to 1 minute, and after that, the next time interval you select does not trigger the value changed method.

    There are various strange things about the interval date picker, like the fact that you can't specify it to choose seconds. If it's important enough, you might want to create your own time interval picker based on a generic UIPickerView. It shouldn't be that hard. (Especially if you don't need to worry about localization to multiple locales that use different time formats.) You'd just create a picker view with spinners for hour, minute, and (optionally) seconds, populate it with your legal values, and away you'd go. You could set it up to have optional minimum and maximum interval values.