简体   繁体   中英

How to manage firstResponder with two UITextFields using UIDatePicker input

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?

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM