了解 inputView 和 firstResponder:如何创建一个将 UIDatePicker 显示为键盘的 UITableViewCell?

Posted

技术标签:

【中文标题】了解 inputView 和 firstResponder:如何创建一个将 UIDatePicker 显示为键盘的 UITableViewCell?【英文标题】:Understanding inputView and firstResponder: How to create a UITableViewCell which shows a UIDatePicker as Keyboard? 【发布时间】:2020-05-26 21:46:12 【问题描述】:

我想创建一个自定义的UITableViewCell,它显示一些日期并在选择时将UIDatePicker 显示为键盘。

虽然我发现了几个处理这个问题的主题,但它们都提供了相同的解决方案:将 UITextField 添加到单元格并将 UIDatepPicker 设置为此 TextField 的 inputView

这个解决方案效果很好,但我不喜欢它。这很 hacky,如果不应该编辑文本,则不需要使用 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 真的是最好的解决方案?

【问题讨论】:

快速搜索,这里只是一个可能满足您需求的示例:***.com/a/34703276/6257435 感谢@DonMag,但您链接的答案解释了如何在 TableViewCell 内显示 UIDatePicker 并且与问题无关(如何在不使用隐藏 TextField 的情况下显示单元格的键盘/输入视图)。 .. 对不起,从您的描述中认为这是一个选项...澄清一下...您想点击一个单元格(或单元格中的一个对象)并显示一个日期选择器,就好像它是键盘吗?或者,您想显示一个顶部带有日期选择器的键盘? 尽管你所说的方式看起来“hacky”,但这是标准做法。我知道您不想要UITextField,只想在您按下单元格时出现日期键盘,但我会通过在您的单元格中添加UITextField 并将其样式设置为UILabel 来解决此问题。然后,当您点击它或单元格时,它将编辑看起来像标签的字段,您的键盘设置为日期选择器 【参考方案1】:

正如评论中已经提到的那样,它没有任何“hacky”。这是一个标准程序。尽管您的输入视图可能看起来像一个标签或按钮,但它仍然是一个输入字段,它打开一个类似于使用日期选择器的键盘的对话框。

但如果你真的不喜欢使用这个,那么有几种选择。我假设当按下一个单元格(或其中的一部分)时,日期选择器应该出现在某处。那么答案很简单。创建一个将在按下该区域时触发的偶数。您可以使用按钮,可以使用表格视图单元格委托来检查何时按下行,或者如果您认为它更适合您,您甚至可以添加手势识别器。

无论如何,一旦您举办了活动,您就可以在任何您想要的地方展示您的日期选择器。这可以是显示一个新窗口或将其放入您的视图控制器中,如果您愿意,甚至可以放入您的表格视图中。

但是,无论您自定义什么,都会否定 ios 设备的本机逻辑,这可能会打扰一些用户。例如,如果您在选择器显示时没有制作完全相同的动画,那么对于习惯于以原生方式处理事物的用户来说,它可能看起来很奇怪。

或者也许你可以做得比原生更好,在这种情况下就去吧。但请注意,您可能需要完成相当多的任务。动画、触摸事件、关闭日期选择器、重新定位您的表格视图以使日期选择器不会与您的单元格重叠......

【讨论】:

【参考方案2】:

可以给一个单元格自己的inputView——你必须override它,它只需要几个步骤来实现。

这里是一个简单的例子(很快就放在一起,所以不要考虑这个“生产就绪”的代码):

点击一行将导致单元格为becomeFirstResponder。它的inputView 是一个自定义视图,其中UIDatePicker 作为子视图。

当您在选择器中选择新的日期/时间时,它将更新该单元格(以及支持数据源)。

点击当前单元格将使其变为resignFirstResponder,这将关闭DatePickerKeyboard

这里是代码。该单元格是基于代码的(不是 Storyboard Prototype),不使用 IBOutlets 或 IBActions...只需添加一个 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)
    


【讨论】:

以上是关于了解 inputView 和 firstResponder:如何创建一个将 UIDatePicker 显示为键盘的 UITableViewCell?的主要内容,如果未能解决你的问题,请参考以下文章

iOS:自定义 UITextField 的 inputView 大小

UIPickerView 作为多个 UITextField 的 inputView

带有搜索栏的 UITableView 作为 inputView

从 Textfield.inputview 加载 xib UIView

UITableView 中的 UITextField 与 DatePicker 的 InputView

如何在使用 inputView 后显示键盘