This time we are going to take a look on how to create a form with text input using UITableView. Displaying static text in UITableView is easy but for enabling text input in UITableView and propagating the change back to a model object requires a couple of steps.
Introduction
In this example project we are going to create a table view what consists of several rows with text input. The content of the table view is defined by an object Form
. Form’s responsibility is to define the items table view displays and also propagating changes back to a model object. In the end we will have a table view where user can edit properties of a model object.
Creating a model object
Model object what we are going to use is an object representing a note. It just has two properties: topic and title. Whenever topic or title changes, it is logged to the console which is enough for verifying updates to model.
final class Note { | |
init(topic: String, text: String) { | |
self.topic = topic | |
self.text = text | |
} | |
var topic: String = "" { | |
didSet { | |
print("Topic changed to \(topic).") | |
} | |
} | |
var text: String = "" { | |
didSet { | |
print("Text changed to \(text).") | |
} | |
} | |
} |
Creating a Form
Like mentioned before, form is going to dictate the content of the table view by defining sections and items. In this simple case, we just have one section with two rows.
final class Form { | |
let sections: [FormSection] | |
init(sections: [FormSection]) { | |
self.sections = sections | |
} | |
} | |
final class FormSection { | |
let items: [FormItem] | |
init(items: [FormItem]) { | |
self.items = items | |
} | |
} | |
protocol FormItem {} |
TextInputFormItem
represents a single row with editable text field. It defines text, placeholder and change handler. Text is used as initial value and if there is no text, table view cell will display placeholder string instead. When user changes the text in the table view row, change handler is called with the new value. This is where we are going to update the model object with a new value.
struct TextInputFormItem: FormItem { | |
let text: String | |
let placeholder: String | |
let didChange: (String) -> () | |
} |
Setting up table view
FormViewController is a UITableViewController subclass and it glues Form and table view together. It uses Form for determining how many sections and items table view has. In addition, based on the item type, it chooses appropriate table view cell for it. In this simple project, we only have items of one type but it is simple to expand it further to support more cell types.
final class FormViewController: UITableViewController { | |
// MARK: Creating a Form View | |
let form: Form | |
init(form: Form) { | |
self.form = form | |
super.init(style: .grouped) | |
} | |
required init?(coder aDecoder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
// MARK: Managing the View | |
private enum ReuseIdentifiers: String { | |
case textInput | |
} | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
tableView.rowHeight = 44 | |
tableView.register(TextInputTableViewCell.self, forCellReuseIdentifier: ReuseIdentifiers.textInput.rawValue) | |
} | |
// MARK: Providing Table View Content | |
private func model(at indexPath: IndexPath) -> FormItem { | |
return form.sections[indexPath.section].items[indexPath.item] | |
} | |
override func numberOfSections(in tableView: UITableView) -> Int { | |
return form.sections.count | |
} | |
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { | |
return form.sections[section].items.count | |
} | |
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | |
let object = model(at: indexPath) | |
if let textRow = object as? TextInputFormItem { | |
let cell = tableView.dequeueReusableCell(withIdentifier: ReuseIdentifiers.textInput.rawValue, for: indexPath) as! TextInputTableViewCell | |
cell.configure(for: textRow) | |
return cell | |
} | |
else { | |
fatalError("Unknown model \(object).") | |
} | |
} | |
} |
Adding editable text field to UITableViewCell
For text input we are going to use a custom cell. This cell adds an editable text field to its contentView. Secondly, it handles touches began event for moving the first responder to the editable text field when users taps on the row and finally calls change handler when text in the editable text field changes.
final class TextInputTableViewCell: UITableViewCell { | |
// MARK: Initializing a Cell | |
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { | |
super.init(style: style, reuseIdentifier: reuseIdentifier) | |
contentView.addSubview(editableTextField) | |
NSLayoutConstraint.activate([ | |
editableTextField.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), | |
editableTextField.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), | |
editableTextField.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor) | |
]) | |
} | |
required init?(coder aDecoder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
// MARK: Reusing Cells | |
override func prepareForReuse() { | |
super.prepareForReuse() | |
changeHandler = { _ in } | |
} | |
// MARK: Managing the Content | |
func configure(for model: TextInputFormItem) { | |
editableTextField.text = model.text | |
editableTextField.placeholder = model.placeholder | |
changeHandler = model.didChange | |
} | |
lazy private var editableTextField: UITextField = { | |
let textField = UITextField(frame: .zero) | |
textField.translatesAutoresizingMaskIntoConstraints = false | |
textField.addTarget(self, action: #selector(TextInputTableViewCell.textDidChange), for: .editingChanged) | |
return textField | |
}() | |
// MARK: Handling Text Input | |
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { | |
editableTextField.becomeFirstResponder() | |
} | |
private var changeHandler: (String) -> () = { _ in } | |
@objc private func textDidChange() { | |
changeHandler(editableTextField.text ?? "") | |
} | |
} |
Creating form for model object
Finally it is time to create a Form
. We’ll just have one section with two items: one item for Note’s topic and the other one for Note’s text. Whenever text is edited in table view, we’ll get the change callback and then we can propagate the change back to the Note
.
extension FormViewController { | |
convenience init(note: Note) { | |
let form = Form(sections: [ | |
FormSection(items: [ | |
TextInputFormItem(text: note.topic, | |
placeholder: "Add title", | |
didChange: { text in | |
note.text = text | |
}), | |
TextInputFormItem(text: note.text, | |
placeholder: "Add description", | |
didChange: { text in | |
note.text = text | |
}) | |
]) | |
]) | |
self.init(form: form) | |
} | |
} |
Summary
We created a table view with editable text field by subclassing UITableViewCell and adding UITextField to it. Then we created Form
and used it to decouple model object from table view. Form’s items provide content for table view cells and handle propagating changes back to the model object.
If this was helpful, please let me know on Twitter @toomasvahter. Thank you for reading.
Example Project
UITableViewCellTextInput (GitHub) Xcode 10.1, Swift 4.2.1
2 replies on “Text input in UITableView”
[…] Text input in UITableView (November 4, 2018) […]
LikeLike
[…] Text input in UITableView (November 4, 2018) […]
LikeLike