🕰️
【SwiftUI】TextField+DatePickerで起こるバグの対処法
はじめに
この記事を参考にしてDatePickerが表示されるTextFieldを用意し、モードを .countdowntimer
(分・秒選択)に変更したところ、1回目に選択した値が反応しないというバグが起こりました。
datePicker.datePickerMode = .countDownTimer
一度時刻を選択しても.valueChanged
イベントが発火せず、2回目以降に選択すると正常に動きます。
対処法
調べているうちにたどり着いたこの記事によると、どうやらiOS7時代から存在する古のバグだったようで、キーボードが表示されたタイミングで初期値をセットすると直ると書いてありました。
UITextFieldのサブクラスであるDatePickerTextFieldのイニシャライザ内でオブザーバーを登録します。
NotificationCenter.default.addObserver(
self,
selector: #selector(self.keyboardDidShow(notification:)),
name: UIResponder.keyboardDidShowNotification,
object: nil
)
そしてキーボード表示時に実行する関数(初期値のセット)を用意します。
.countdowntimer
モードではdate
プロパティが使用されないので、countDownDuration
に秒数をセットする必要があります。
@objc func keyboardDidShow(notification: Notification) {
DispatchQueue.main.async {
self.datePicker.countDownDuration = TimeInterval(second)
}
}
second
にはDouble型で秒数を入れてあることを想定しています(適宜読み替えてください)。
おわりに
全体のコードです。
.countDownTimer
モードに合わせて、何箇所か変更を加えています。
//ref: https://medium.com/@Yerazhas/custom-date-picker-textfield-in-swiftui-8505f8974f5b
import SwiftUI
struct DatePickerInputView: UIViewRepresentable {
@Binding var minute: Int
let placeholder: String
init(minute: Binding<Int>, placeholder: String) {
self._minute = minute
self.placeholder = placeholder
}
func updateUIView(_ uiView: DatePickerTextField, context: Context) {
setText(uiView)
}
func makeUIView(context: Context) -> DatePickerTextField {
let datePickerTextField = DatePickerTextField(minute: $minute, frame: .zero)
datePickerTextField.placeholder = placeholder
setText(datePickerTextField)
return datePickerTextField
}
private func setText(_ datePickerTextField: DatePickerTextField) {
datePickerTextField.text = "\(minute / 60)hours \(minute % 60)min"
}
}
final class DatePickerTextField: UITextField {
@Binding var minute: Int
private let datePicker = UIDatePicker()
init(minute: Binding<Int>, frame: CGRect) {
self._minute = minute
super.init(frame: frame)
inputView = datePicker
datePicker.addTarget(self, action: #selector(datePickerDidSelect(_:)), for: .valueChanged)
datePicker.datePickerMode = .countDownTimer
NotificationCenter.default.addObserver(
self,
selector: #selector(self.keyboardDidShow(notification:)),
name: UIResponder.keyboardDidShowNotification,
object: nil
)
let toolBar = UIToolbar()
toolBar.sizeToFit()
let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let doneButton = UIBarButtonItem(title: "Done", style: .plain, target: self, action: #selector(dismissTextField))
toolBar.setItems([flexibleSpace, doneButton], animated: false)
inputAccessoryView = toolBar
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func datePickerDidSelect(_ sender: UIDatePicker) {
self.minute = Int(datePicker.countDownDuration / 60)
}
@objc func keyboardDidShow(notification: Notification) {
let secondDouble = Double(minute * 60)
DispatchQueue.main.async {
self.datePicker.countDownDuration = TimeInterval(self.secondDouble)
}
}
@objc private func dismissTextField() {
resignFirstResponder()
}
override func caretRect(for position: UITextPosition) -> CGRect {
return CGRect.zero
}
override func selectionRects(for range: UITextRange) -> [UITextSelectionRect] {
return []
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return false
}
}
Discussion