Понимание inputView и firstResponder: Как создать UITableViewCell, который показывает UIDatePicker в качестве клавиатуры? - PullRequest
0 голосов
/ 11 февраля 2020

Я хотел бы создать пользовательский UITableViewCell, который отображает некоторую дату и показывает UIDatePicker в качестве клавиатуры при выборе.

Хотя я нашел несколько тем, касающихся этого вопроса, все они предлагают одно и то же решение: Добавьте UITextField в ячейку и установите UIDatepPicker как inputView этого TextField.

Это решение отлично работает, но мне оно не нравится. Это довольно странно, и нет необходимости использовать TextField, если текст не нужно редактировать. Все, что мне нужно, это метка с данными и клавиатура DatePicker, чтобы изменить эту дату.


Что я пробовал:

Погружение немного глубже, я узнал что UITableViewCell имеет свое собственное свойство inputView, которое наследуется от UIResponder. Однако я не могу переопределить (Cannot override with a stored property 'inputView') или назначить это свойство (Cannot assign to property: 'inputView' is a get-only property). То же самое верно для canBecomeFirstResponder и других свойств, которые должны были бы быть реализованы / изменены, чтобы позволить ячейке работать firstResponder / inputView.

Интересно, как реализовано UITextField, поскольку это также UIResponder подкласс.

Короче говоря:

Можно ли создать мой собственный UIView (или даже лучше UITableViewCell) подкласс, который действует как своего рода ввод и отображение пользовательской клавиатуры?

Или использование (скрытого) TextField действительно лучшее решение?

Ответы [ 2 ]

0 голосов
/ 11 февраля 2020

Вы можете дать ячейке собственную inputView - вам нужно override, и для ее реализации требуется всего несколько шагов.

Вот простой пример (собран очень быстро, поэтому не учитывайте этот код «готов к производству»):

enter image description here

Постукивание по строке приведет к ячейке becomeFirstResponder. Его inputView представляет собой пользовательское представление с UIDatePicker в качестве подпредставления.

Когда вы выбираете новую дату / время в средстве выбора, оно обновляет эту ячейку (и источник данных поддержки).

Нажатие на текущую ячейку приведет к resignFirstResponder, что приведет к отклонению DatePickerKeyboard.

Вот код. Ячейка основана на коде (не является прототипом раскадровки) и не использует IBOutlet s или IBAction s ... просто добавьте UITableViewController и присвойте ее InputViewTableViewController:

// protocol so we can send back the newly selected date
@objc protocol MyDatePickerProtocol {
    @objc func updateDate(_ newDate: Date)
}

// basic UIView with UIDatePicker added as subview
class DatePickKeyboard: UIView {

    var theDatePicker: UIDatePicker = UIDatePicker()

    weak var delegate: MyDatePickerProtocol?

    init(delegate: MyDatePickerProtocol) {
        self.delegate = delegate
        super.init(frame: .zero)
        configure()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

// UIDatePicker target
extension DatePickKeyboard {
    @objc func dpChanged(_ sender: Any?) -> Void {
        if let dp = sender as? UIDatePicker {
            // tell the delegat we have a new date
            delegate?.updateDate(dp.date)
        }
    }
}

// MARK: - Private initial configuration methods
private extension DatePickKeyboard {
    func configure() {
        autoresizingMask = [.flexibleWidth, .flexibleHeight]
        theDatePicker.addTarget(self, action: #selector(dpChanged(_:)), for: .valueChanged)
        addSubview(theDatePicker)
        theDatePicker.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            theDatePicker.centerXAnchor.constraint(equalTo: centerXAnchor),
            theDatePicker.centerYAnchor.constraint(equalTo: centerYAnchor),
            theDatePicker.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1.0),
            theDatePicker.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1.0),
        ])
    }
}

// table view cell with a single UILabel
// enable canBecomeFirstResponder and use
// DatePickerKeyboard view instead of default keyboard
class InputViewCell: UITableViewCell, MyDatePickerProtocol {

    var theLabel: UILabel = {
        let v = UILabel()
        return v
    }()

    var myInputView: UIView?

    var myCallback: ((Date)->())?

    var myDate: Date = Date()

    var theDate: Date {
        get {
            return self.myDate
        }
        set {
            self.myDate = newValue
            let df = DateFormatter()
            df.dateFormat = "MMM d, h:mm a"
            let s = df.string(from: self.myDate)
            theLabel.text = s
        }
    }

    override var canBecomeFirstResponder: Bool { return true }

    override var inputView: UIView {
        get {
            return self.myInputView!
        }
        set {
            self.myInputView = newValue
        }
    }

    @discardableResult
    override func becomeFirstResponder() -> Bool {
        let becameFirstResponder = super.becomeFirstResponder()
        if let dpv = self.inputView as? DatePickKeyboard {
            dpv.theDatePicker.date = self.myDate
        }
        updateUI()
        return becameFirstResponder
    }

    @discardableResult
    override func resignFirstResponder() -> Bool {
        let resignedFirstResponder = super.resignFirstResponder()
        updateUI()
        return resignedFirstResponder
    }

    func updateUI() -> Void {
        // change the appearance if desired
        backgroundColor = isFirstResponder ? .yellow : .clear
    }

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }

    func commonInit() -> Void {
        theLabel.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(theLabel)
        let v = contentView.layoutMarginsGuide
        NSLayoutConstraint.activate([
            theLabel.topAnchor.constraint(equalTo: v.topAnchor),
            theLabel.bottomAnchor.constraint(equalTo: v.bottomAnchor),
            theLabel.leadingAnchor.constraint(equalTo: v.leadingAnchor),
            theLabel.trailingAnchor.constraint(equalTo: v.trailingAnchor),
        ])
        inputView = DatePickKeyboard(delegate: self)
    }

    @objc func updateDate(_ newDate: Date) -> Void {
        self.theDate = newDate
        myCallback?(newDate)
    }

}

// must conform to UIKeyInput, even if we're not using the standard funcs
extension InputViewCell: UIKeyInput {
    var hasText: Bool { return false }
    func insertText(_ text: String) { }
    func deleteBackward() { }
}

// simple table view controller
class InputViewTableViewController: UITableViewController {

    var theData: [Date] = [Date]()

    override func viewDidLoad() {
        super.viewDidLoad()

        // generate some date data to work with - 25 dates incrementing by 2 days
        var d = Calendar.current.date(byAdding: .day, value: -50, to: Date())
        for _ in 1...25 {
            theData.append(d!)
            d = Calendar.current.date(byAdding: .day, value: 2, to: d!)
        }

        // register our custom cell
        tableView.register(InputViewCell.self, forCellReuseIdentifier: "InputViewCell")
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return theData.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let c = tableView.dequeueReusableCell(withIdentifier: "InputViewCell", for: indexPath) as! InputViewCell
        c.theDate = theData[indexPath.row]
        c.myCallback = { d in
            self.theData[indexPath.row] = d
        }
        c.selectionStyle = .none
        return c
    }

    // on row tap, either become or resign as first responder
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if let c = tableView.cellForRow(at: indexPath) as? InputViewCell {
            if c.isFirstResponder {
                c.resignFirstResponder()
            } else {
                c.becomeFirstResponder()
            }
        }
        tableView.deselectRow(at: indexPath, animated: false)
    }

}
0 голосов
/ 11 февраля 2020

Как уже упоминалось в комментарии, в этом нет ничего "хакерского". Это стандартная процедура. Хотя ваше представление ввода может выглядеть как метка или кнопка, оно все равно непременно является полем ввода, которое открывает диалоговое окно, похожее на клавиатуру, в которой используется средство выбора даты.

Но если вы действительно не пользуетесь это то есть несколько альтернатив. Я предполагаю, что когда ячейка (или ее часть) нажата, то где-то должен появиться сборщик даты. Ну, тогда ответ довольно прост; создайте четность, которая будет срабатывать при нажатии области. Вы можете использовать кнопку, вы можете использовать делегат ячейки табличного представления, чтобы проверить, когда строка нажата, или вы даже можете добавить распознаватель жестов, если считаете, что он вам подходит.

В любом случае, если у вас есть событие Вы можете представить свой выбор даты в любом месте. Это может быть представление нового окна или помещение его в контроллер представления или даже в просмотр таблицы, если хотите.

Но все, что вы настраиваете, будет сводить на нет нативную логику c off iOS устройств, которые могут беспокоить некоторые пользователи. Например, если вы не делаете точно такую ​​же анимацию, когда сборщик показывает, это может показаться странным для пользователей, которые привыкли иметь вещи такими, какими они являются изначально.

Или, может быть, вы можете сделать это намного лучше, чем в нативной какой случай go для него. Но обратите внимание, что у вас может быть довольно сложная задача для выполнения sh. Анимации, сенсорные события, отмена выбора даты, изменение положения таблицы, чтобы выбор даты не перекрывал вашу ячейку ...

...