Text input in UITableView

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).")
}
}
}
view raw Note.swift hosted with ❤ by GitHub

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 {}
view raw Form.swift hosted with ❤ by GitHub

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.
editing_form_final

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

References

UITableViewCell (Apple)
UITableViewController (Apple)

One Reply to “Text input in UITableView”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s