[英]Understanding inputView and firstResponder: How to create a UITableViewCell which shows a UIDatePicker as Keyboard?
我想創建一個自定義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 真的是最好的解決方案嗎?
正如評論中已經提到的那樣,它沒有什么“hacky”。 這是一個標准程序。 盡管您的輸入視圖可能看起來像一個標簽或按鈕,但它仍然是一個輸入字段,它打開一個類似於使用日期選擇器的鍵盤的對話框。
但是如果你真的不想使用這個,那么有幾種選擇。 我假設當一個單元格(或它的一部分)被按下時,日期選擇器應該出現在某處。 那么答案很簡單; 創建一個偶數,當該區域被按下時會觸發。 您可以使用按鈕,也可以使用表格視圖單元格委托來檢查何時按下行,或者如果您認為它更適合您,甚至可以添加手勢識別器。
在任何情況下,一旦您有活動,您就可以在任何您想要的地方展示您的日期選擇器。 如果您願意,這可以是呈現一個新窗口或將其放入您的視圖控制器或什至您的表格視圖中。
但是無論您自定義什么,都會否定 iOS 設備的本機邏輯,這可能會打擾一些用戶。 例如,如果您在選擇器顯示時沒有制作完全相同的動畫,對於習慣於以本機方式擁有事物的用戶來說,它可能看起來很奇怪。
或者也許你可以做得比本地好得多,在這種情況下就去做吧。 但請注意,您可能有相當多的任務要完成。 動畫、觸摸事件、關閉日期選擇器、重新定位您的表格視圖以便日期選擇器不會與您的單元格重疊...
你可以給一個單元格自己的inputView
—— 你必須override
它,它只需要幾個步驟來實現。
這是一個簡單的例子(很快就組合在一起,所以不要考慮這個“生產就緒”代碼):
點擊一行將導致單元格成為becomeFirstResponder
。 它的inputView
是一個自定義視圖,帶有一個UIDatePicker
作為子視圖。
當您在選擇器中選擇新的日期/時間時,它將更新該單元格(以及支持數據源)。
點擊當前單元格將導致它resignFirstResponder
這將關閉DatePickerKeyboard
。
這是代碼。 該單元格是基於代碼的(不是 Storyboard Prototype),並且不使用IBOutlet
或IBAction
......只需添加一個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)
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.