0

I would like to create a custom UITableViewCell which displays some date and shows a UIDatePicker as keyboard when selected.

While I found several topics dealing with this question they all offer the same solution: Add a UITextField to the cell and set the UIDatepPicker as inputView of this TextField.

This solution works fine but I do not like it. It is quite hacky and it should not be necessary use a TextField if no text should be edited. All I need is a label showing the data and a DatePicker keyboard to change this date.


What I tried:

Diving a little deeper I found out that UITableViewCell has its own inputView property which it inherits from UIResponder. However, I cannot override (Cannot override with a stored property 'inputView') or assign this property (Cannot assign to property: 'inputView' is a get-only property). The same is true for canBecomeFirstResponder and other properties which would have to implemented / changed in order to let the cell work firstResponder / inputView.

I wonder how UITextField is implemented since this is also a UIResponder subclass.

Long story short:

Is it possible to create my own UIView (or even better UITableViewCell) subclass which acts as a kind of input view and shows a custom keyboard?

Or is using a (hidden) TextField really the best solution?

Andrei Herford
  • 17,570
  • 19
  • 91
  • 225
  • Quick search, here is just one example that may fit your needs: https://stackoverflow.com/a/34703276/6257435 – DonMag Feb 11 '20 at 13:20
  • Thanks @DonMag but answer you linked explains how to show a UIDatePicker inside a TableViewCell and has nothing to do with the question (how to show a keyboard / inputview for a cell without using a hidden TextField)... – Andrei Herford Feb 11 '20 at 13:25
  • Sorry, thought from your description that would be an option... To clarify then... you want to tap a cell (or an object in the cell) and display a date picker as if it was a keyboard? Or, you want to show a keyboard with a date picker on top of it? – DonMag Feb 11 '20 at 13:37
  • Even though the way you are talking about seems "hacky", it's the standard practice. I understand that you don't want a `UITextField` and just want the date keyboard to appear when you press the cell, but I would approach this by adding a `UITextField` to your cell and styling it like a `UILabel`. Then, when you tap it, or the cell it will edit the field that looks like a label with your keyboard set to a date picker – Mr.P Feb 11 '20 at 13:37

2 Answers2

0

As already mentioned in a comment there is nothing "hacky" about it. This is a standard procedure. Although your input view may look like a label or a button it is still by all means an input field which opens a dialog similar to keyboard which uses a date picker.

But if you are really into not-using-this then there are several alternatives. I assume that when a cell (or a part of it) is pressed then a date picker should appear somewhere. Well then the answer is pretty straight forward; create an even which will trigger when the region is pressed. You can use a button, you could use a table view cell delegate to check when row is pressed or you could even add a gesture recognizer if you think it suits you better.

In any case once you have an event you can present your date picker anywhere you want. This can be presenting a new window or putting it in your view controller or even in your table view if you like.

But whatever you customize will negate the native logic off iOS devices which may bother some users. For instance if you don't make exactly the same animation when picker shows it might look weird to users that are used to having things the way they are natively.

Or maybe you can do it much nicer than native in which case go for it. But note you may have quite a task to accomplish. Animations, touch events, dismissing date picker, repositioning your table view so that date picker does not overlap your cell...

Matic Oblak
  • 16,318
  • 3
  • 24
  • 43
0

You can give a cell its own inputView -- you have to override it, and it just takes a few steps to implement.

Here is a simple example (put together very quickly, so don't consider this "production ready" code):

enter image description here

Tapping on a row will cause the cell to becomeFirstResponder. Its inputView is a custom view with a UIDatePicker as a subview.

As you select a new date / time in the picker, it will update that cell (and the backing data source).

Tapping on the current cell will cause it to resignFirstResponder which will dismiss the DatePickerKeyboard.

Here is the code. The cell is code-based (not a Storyboard Prototype), and uses no IBOutlets or IBActions... just add a UITableViewController and assign it to 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)
    }

}
DonMag
  • 69,424
  • 5
  • 50
  • 86