1

I have a ViewController with two UITextFields that use UIDatePicker(s) for input. One is the "From" date and the other is the "To" date, so this seems like a fairly common scenario.

The problem is that when one field's DatePicker is active, it's possible for the user to tap into the other field on screen. The active DatePicker is still tied to the first field, so the UI is confusing to users because the cursor is blinking in the other field but the input doesn't go there.

I think the right UI solution is to prevent the user from tapping into any other field until the DatePicker is dismissed. But I haven't been able to find a way to either detect that the firstResponder is changing or temporarily disable editing of other fields.

Here's my current code that isn't doing what I need it to do:

class AddTripLocationVC: UIViewController, UITextFieldDelegate {

@IBOutlet private weak var tripLocFromDateInput: UITextField!
@IBOutlet private weak var tripLocToDateInput: UITextField!

override func viewDidLoad() {
    super.viewDidLoad()

    //set up datepickers for text field inputs
    createDatePicker(forDateField: tripLocFromDateInput)
    createDatePicker(forDateField: tripLocToDateInput)

}

//automatically update the label fields when the date vairables are updated by the UIDatePicker
var locFromDate: Date? {
    didSet {
        guard locFromDate != nil else { return }
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "dd MMM yyyy"
        tripLocFromDateInput.text = dateFormatter.string(from: locFromDate!)
    }
}

var locToDate: Date? {
    didSet {
        guard locToDate != nil else { return }
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "dd MMM yyyy"
        tripLocFromDateInput.text = dateFormatter.string(from: locToDate!)
        //Comment after solution found...The problem was the line above
        //It should be:
        tripLocToDateInput.text = dateFormatter.string(from: locToDate!)
    }
}

func createDatePicker(forDateField dateField: UITextField) {

    let datePickerView = UIDatePicker()
    datePickerView.datePickerMode = .date
    dateField.inputView = datePickerView
    datePickerView.addTarget(self, action: #selector(handleDatePicker(sender:)), for: .valueChanged)

    let doneButton = UIBarButtonItem.init(title: "Done", style: .done, target: self, action: #selector(self.datePickerDone))
    let toolBar = UIToolbar.init(frame: CGRect(x: 0, y: 0, width: view.bounds.size.width, height: 44))
    toolBar.setItems([UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: nil, action: nil), doneButton],
                     animated: true)
    dateField.inputAccessoryView = toolBar
}

@objc
func handleDatePicker(sender: UIDatePicker) {

    if tripLocFromDateInput.isFirstResponder {
        locFromDate = sender.date
    } else {
        if tripLocToDateInput.isFirstResponder {
            locToDate = sender.date
        } else {
            print("Error: Can't find first responder field.")
        }
    }
}

@objc
func datePickerDone(dateField: UITextView) {

    if tripLocFromDateInput.isFirstResponder {
        tripLocFromDateInput.resignFirstResponder()
    } else {
        if tripLocToDateInput.isFirstResponder {
            tripLocToDateInput.resignFirstResponder()
        } else {
            print("Error: Can't find first responder field.")
        }
    }
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    self.view.endEditing(true)
}

func textFieldDidEndEditing(_ textField: UITextField) {
    textField.becomeFirstResponder()
}
}

Are there 'standard' approaches to the UI for this type of 'From' and 'To' date fields in iOS?

Asad Ali Choudhry
  • 4,985
  • 4
  • 31
  • 36
tkhelm
  • 345
  • 3
  • 14
  • Why do you set the `textField.becomeFirstResponder()` in `textFieldDidEndEditing` !!! – Rakesha Shastri Jun 13 '19 at 04:11
  • See my exchange below with ekscrypto. That's not working, as you anticipated. The question is what would work? – tkhelm Jun 13 '19 at 04:16
  • @ekscrypto Your working code helped me realize that my problem was actually with the two Date variables that were updated by handleDatePicker(). They were incorrectly updating the same field, which made the UI behave wrong (commented accordingly, above). I also see that even if the focus changes, the not-dismissed DatePicker does update whichever field is in focus, so the UI is not actually confusing even if the user taps in the other field. – tkhelm Jun 13 '19 at 05:28
  • Please have a look at this answer https://stackoverflow.com/a/10671725/5701085 – Asad Ali Choudhry Jun 13 '19 at 05:39
  • @Asad Ali Choudhry Please see the Comments below the accepted answer from ekscrypto. The approach you are also suggesting did not work, but his subsequent working code did. This caused me to realize that my related code was also working, and the problem was actually in the didSet{ } for one of the date variables that the DatePickers updated. I have made that code visible above (I didn't think it was relevant when I originally posted), and accepted the solution by ekscrypto, since his code also works fine. – tkhelm Jun 13 '19 at 05:49
  • okay if its working now, thats really great. – Asad Ali Choudhry Jun 13 '19 at 05:51

1 Answers1

0

You can register a UITextField delegate which contains two methods which will be of particular interest to you, particularly:

  • textFieldDidBeginEditing(:)
  • textFieldDidEndEditing(:)

To get more information, visit: https://developer.apple.com/documentation/uikit/uitextfielddelegate

Note that you can also use becomeFirstResponder() on any of your UITextField to make one active. Whichever is active should have the blinking cursor.


Edit: based on your updated code:

//
//  ViewController.swift
//  datepickers
//
//  Created by Dave Poirier on 2019-06-13.
//

import UIKit

class ViewController: UIViewController,  UITextFieldDelegate {

    @IBOutlet private weak var tripLocFromDateInput: UITextField!
    @IBOutlet private weak var tripLocToDateInput: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()

        //set up datepickers for text field inputs
        createDatePicker(forDateField: tripLocFromDateInput)
        createDatePicker(forDateField: tripLocToDateInput)

        //set text field delegates to be notified when focus changes
        tripLocFromDateInput.delegate = self
        tripLocToDateInput.delegate = self
    }


    func createDatePicker(forDateField dateField: UITextField) {

        let datePickerView = UIDatePicker()
        datePickerView.datePickerMode = .date
        dateField.inputView = datePickerView
        datePickerView.addTarget(self, action: #selector(handleDatePicker(sender:)), for: .valueChanged)

        let doneButton = UIBarButtonItem.init(title: "Done", style: .done, target: self, action: #selector(self.datePickerDone))
        let toolBar = UIToolbar.init(frame: CGRect(x: 0, y: 0, width: view.bounds.size.width, height: 44))
        toolBar.setItems([UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: nil, action: nil), doneButton],
                         animated: true)
        dateField.inputAccessoryView = toolBar
    }

    @objc
    func handleDatePicker(sender: UIDatePicker) {

        if tripLocFromDateInput.isFirstResponder {
            let locFromDate = sender.date
            tripLocFromDateInput.text = "FROM: \(locFromDate)"
        } else {
            if tripLocToDateInput.isFirstResponder {
                let locToDate = sender.date
                tripLocToDateInput.text = "TO: \(locToDate)"
            } else {
                print("Error: Can't find first responder field.")
            }
        }
    }

    @objc
    func datePickerDone(dateField: UITextView) {

        if tripLocFromDateInput.isFirstResponder {
            tripLocFromDateInput.resignFirstResponder()
        } else {
            if tripLocToDateInput.isFirstResponder {
                tripLocToDateInput.resignFirstResponder()
            } else {
                print("Error: Can't find first responder field.")
            }
        }
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        self.view.endEditing(true)
    }
}

}

The code above worked for me in a brand new project, created two input fields and connected the IBOutlets

ekscrypto
  • 3,718
  • 1
  • 24
  • 38
  • Thanks for the quick response. However, I have added the code I tried above, to force the firstResponder back to the current field if it leaves, and it's not working. A breakpoint in the textFieldDidEndEditing *is* getting triggered when the 'Done' button on the UIDatePicker is tapped, which forces the field that's being edited to become the firstResponder again, so the UIDatePicker refuses to be dismissed. So I must have misunderstood the solution you were suggesting.... – tkhelm Jun 13 '19 at 04:11
  • To be clear, what I'm looking for is a way to detect the focus (if not the firstResponder) leaving the current field *while* the DatePicker is still active – tkhelm Jun 13 '19 at 04:14
  • @tkhelm I updated my answer with some working code. Notice I added the call to super.touchesBegan() so the touch events can be propagated properly in case this ends up in a UIScrollView someday or you need gestures. – ekscrypto Jun 13 '19 at 04:31
  • 1
    Thank you for spending time on this. Your working code helped me realize that my problem was actually with the two Date variables that were updated by handleDatePicker(). They were incorrectly updating the same field, which made the UI behave wrong. I also see that even if the focus changes, the not-dismissed DatePicker does update whichever field is in focus, so the UI is not actually confusing even if the user taps in the other field. – tkhelm Jun 13 '19 at 04:57
  • 1
    BTW, for me, touchesBegan() is never called/executed. I can see why I should have included super.touchesBegan() but it doesn't seem relevant to the solution. It appears to not need to be included at all. – tkhelm Jun 13 '19 at 05:00