🕰️

【SwiftUI】TextField+DatePickerで起こるバグの対処法

2022/05/04に公開

はじめに

https://medium.com/@Yerazhas/custom-date-picker-textfield-in-swiftui-8505f8974f5b

この記事を参考にしてDatePickerが表示されるTextFieldを用意し、モードを .countdowntimer(分・秒選択)に変更したところ、1回目に選択した値が反応しないというバグが起こりました。

datePicker.datePickerMode = .countDownTimer

一度時刻を選択しても.valueChangedイベントが発火せず、2回目以降に選択すると正常に動きます。

対処法

https://appleharikyu.jp/iphone/?p=1246

調べているうちにたどり着いたこの記事によると、どうやら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
    }
}
GitHubで編集を提案

Discussion