Great progress so far! Little by little, our app is taking shape. Next thing to learn is how to process the user’s input and a variety of useful concepts along the way.
Managing input
Activate typing
An intuitive way for the user to request to provide keyboard input is to tap on an input (assuming it looks like an input 😁).
The text field handles this gesture as a trigger to become a first responder.
Back to the text field. As the user taps on it, it becomes a first responder, and iOS ensures the keyboard comes up.
As the user types, their input is placed into the field. That part is taken care of by iOS - the rest is on us. And that is at least to capture what they've typed when they finished.
Process Input
The user will finish typing when they click either the Add button within the cell or the Done button on the keyboard. Let's look at those two circumstances more closely.
The scenario when the user clicks the Add button is more straightforward as we already have a function that handles the button click and we can extract the text from the text property of the text field. Let's create a function placeholder for it:
func processInput() {
}
And then call it on button press:
@IBAction func addButtonPressed(_ sender: Any) {
processInput()
}
We'll complete the method implementation in a bit.
And how do we know when the user has pressed the Done button?
In the other scenario, when we need to capture the Done button press, we'll work with delegate methods; the first thing we'll need to do is declare our cell subclass as a UITextFieldDelegate
:
class NewTaskTableViewCell: UITableViewCell, UITextFieldDelegate {
// ...
}
All delegate methods are optional, so we don't have to worry about fixing anything.
Now alter the configuration method to assign the delegate (the cell subclass):
func configure() {
textInput.delegate = self
}
To capture the Done key press, we need to use the text field delegate method textFieldShouldReturn
where we need to perform the same processing of the user's input as on Add button press and then return true
:
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
processInput()
return true
}
To process input, we'll need to fetch the text property value of the text field. The Done button is only enabled when the user types something (we requested that by checking off the "Auto-enable Return Key" attribute in Interface Builder). But that something can be just spaces! And we don't want to allow that! 😵
So, let's implement a helper function that will clear the air for us before we commit to notifying the controller:
func fetchInput() -> String? {
if let caption = textInput.text?.trimmingCharacters(in: .whitespaces) {
return caption.count > 0 ? caption : nil
}
return nil
}
The implementation is pretty straight forward:
We extract the text value and trim the whitespaces off of it.
If the remaining length is still greater than 0, we return a trimmed caption to be shared with the cell delegate.
Otherwise, we return nil.
Now let's complete the processInput()
method:
func processInput() {
if let caption = fetchInput() {
delegate?.newTaskCell(self, newTaskCreated: caption)
}
textInput.text = ""
textInput.resignFirstResponder()
}
Let's review those lines:
We safely unwrap the value returned by a helper function that takes care of normalizing the input.
If there's a non-nil value, we call a delegate method to notify the cell delegate object that the user has requested to create a new task and provide the caption stored in the text field.
Assuming the delegate object will take care of the rest, we can now clear the text field making it available for the user to create another task.
Finally, we resign the text field from being first responder.
Improve user experience
Next challenge is to guarantee the same behaviour for the Add button as for the Done button. Which is - to make it appear enabled when user types something and disabled when the field is empty.
To address this, let's create a helper method:
func manageAddButton() {
var enabled = false
if let caption = fetchInput() {
if caption.count > 0 {
enabled = true
}
}
addButton.isEnabled = enabled
}
This method needs to be called on a number of occasions:
Initial configuration - configure() method
func configure() {
textInput.delegate = self
manageAddButton()
}
Every time a user types something or removes from the input. For this, we need to create an action for the text field to be triggered on
EditingChanged
event. This is done in the same fashion as creating an action for a button using Interface Builder, just instead of TouchUpInside, select EditingChanged event and name a methodtextInputChanged
:

Use this method to manage the Add button:
@IBAction func inputTextChanged(_ sender: Any) {
manageAddButton()
}
And, lastly, after we reset the text field value in processInput method:
func processInput() {
if let caption = fetchInput() {
delegate?.newTaskCell(self, newTaskCreated: caption)
}
textInput.text = ""
textInput.resignFirstResponder()
manageAddButton()
}
And here's the complete contents of the new task cell subclass:
import Foundation
import UIKit
protocol NewTaskCellDelegate {
func newTaskCell(_ cell: NewTaskTableViewCell, newTaskCreated caption: String)
}
class NewTaskTableViewCell: UITableViewCell, UITextFieldDelegate {
@IBOutlet weak var textInput: UITextField!
@IBOutlet weak var addButton: UIButton!
var delegate: NewTaskCellDelegate?
@IBAction func addButtonPressed(_ sender: Any) {
processInput()
}
@IBAction func inputTextChanged(_ sender: Any) {
manageAddButton()
}
func configure() {
textInput.delegate = self
manageAddButton()
}
override func awakeFromNib() {
super.awakeFromNib()
configure()
}
func manageAddButton() {
var enabled = false
if let caption = fetchInput() {
if caption.count > 0 {
enabled = true
}
}
addButton.isEnabled = enabled
}
func fetchInput() -> String? {
if let caption = textInput.text?.trimmingCharacters(in: .whitespaces) {
return caption.count > 0 ? caption : nil
}
return nil
}
func processInput() {
if let caption = fetchInput() {
delegate?.newTaskCell(self, newTaskCreated: caption)
}
textInput.text = ""
textInput.resignFirstResponder()
manageAddButton()
}
//MARK: text field delegates
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
processInput()
return true
}
}
Here's a programming challenge for you - spot the difference in the behaviour of Done and Add buttons .
We are done with the text field functionality here.
Now - onto continuing with the view controller!
Committing a New Task
Switch to ViewController.swift.
Prepare the Controller
Let's declare that it confirms to our new delegate:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, TaskCellDelegate, NewTaskCellDelegate {
// ...
}
And fix the usual Xcode complaint by implementing the required delegate method:
func newTaskCell(_ cell: NewTaskTableViewCell, newTaskCreated caption: String) {
}
Then, alter the table view datasource delegate method that feeds at the cells (section 0):
case 0:
let cell = tableView.dequeueReusableCell(withIdentifier: "NewTaskCellID", for: indexPath) as! NewTaskTableViewCell
cell.delegate = self
return cell
All we needed to do is to typecast it to the new cell type and assign a delegate.
Now, let's complete adding a new task!
Add a Hot Task!
To add a new task we need to accomplish the following:
Create a HotTask object
Add a new object to a data source array. We use top priority as a default, therefore will be adding the new object to the priorityTasks array.
Add a new cell to the respective section on the table view.
Would you like to try on your own? - Sure you do, go ahead and implement the newTaskCell delegate method!

Here's my version:
func newTaskCell(_ cell: NewTaskTableViewCell, newTaskCreated caption: String) {
// create new HOT task:)
let newTask = HotTask.init(caption: caption)
// insert a new row in the beginning of priority section
insertTask(newTask, at: IndexPath(row: 0, section: 1))
}
That's right, the only new thing we needed to implement here was an instance of a new task, and we've got the rest already handled by a previously implemented helper method insertTask!
That's all !
Managing the Keyboard
Ho-Ho! Run a test! Start typing and then while the keyboard is up, imagine you want to scroll down your task list to remind yourself of the tasks you already added ... You can't! It bounces right back down under the keyboard !
That's no good! - Let's fix it!
Here's what we want to happen:
When the keyboard comes up, we want to limit the area where the table view should be scrolling
When the keyboard is dismissed, we want the table view to come back to initial scrolling capacity.
To make this happen we need to be notified when the keyboard shows and hides. We don't control those activities, therefore we'll have to collaborate with iOS via Notification Centre.
Discover Notification Center functionality
Notifications imply two parties:
sender
receiver, or multiple receivers
Using Notification Center in iOS requires implementing 2 elements:
A receiver object must sign up for a particular notification, for example, when the keyboard is about to show or to hide.
A receiver needs to implement a handling function to be executed when it receives a notification.
The signing up part is done by adding an observer for a particular event and specifying a method to be executed when the event takes place:
func registerForKeyboardNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
@objc func keyboardWillShow(notification: NSNotification) {
}
@objc func keyboardWillHide(notification: NSNotification){
}
And we need to call this as soon as the app launches - in configure() method:
func configure() {
tableView.delegate = self
tableView.dataSource = self
populateInitialTasks()
updateProgress()
registerForKeyboardNotifications()
}
What's left is to actually implement the methods we want to perform before the keyboard will show and hide. To adjust the scrolling space, we'll use the contentInset
property of the table view. Let's implement a helper function for it:
func adjustLayoutForKeyboard(targetHeight: CGFloat) {
tableView.contentInset.bottom = targetHeight
}
And call this function for both notifications:
@objc func keyboardWillShow(notification: NSNotification) {
let keyboardFrame = notification.userInfo![UIKeyboardFrameEndUserInfoKey] as! CGRect
adjustLayoutForKeyboard(targetHeight: keyboardFrame.size.height)
}
@objc func keyboardWillHide(notification: NSNotification){
adjustLayoutForKeyboard(targetHeight: 0)
}
And here's a complete code of the view controller so far:
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, TaskCellDelegate, NewTaskCellDelegate {
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var progressLabel: UILabel!
var priorityTasks = [HotTask]()
var bonusTasks = [HotTask]()
// MARK: Cells delegates
func taskCell(_ cell: TaskTableViewCell, completionChanged completion: Bool) {
// identify indexPath for a cell
if let indexPath = tableView.indexPath(for: cell) {
// fetch datasource for indexPath
if let task = hotTaskDataSource(indexPath: indexPath) {
// update the completion state
task.completed = completion
}
}
}
func newTaskCell(_ cell: NewTaskTableViewCell, newTaskCreated caption: String) {
// create new HOT task:)
let newTask = HotTask.init(caption: caption)
// add new object to the datasource array
priorityTasks.insert(newTask, at: 0)
// put the table view in updating mode
tableView.beginUpdates()
// insert a new cell to the table
tableView.insertRows(at: [IndexPath.init(row: 0, section: 1)], with: .automatic)
// finish updating table
tableView.endUpdates()
}
// MARK: Keyboard management
func registerForKeyboardNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
@objc func keyboardWillShow(notification: NSNotification) {
let keyboardFrame = notification.userInfo![UIKeyboardFrameEndUserInfoKey] as! CGRect
adjustLayoutForKeyboard(targetHeight: keyboardFrame.size.height)
}
@objc func keyboardWillHide(notification: NSNotification){
adjustLayoutForKeyboard(targetHeight: 0)
}
func adjustLayoutForKeyboard(targetHeight: CGFloat) {
tableView.contentInset.bottom = targetHeight
}
// MARK: Table view delegates
func numberOfSections(in tableView: UITableView) -> Int {
return 3
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0:
return 1
case 1:
return priorityTasks.count
case 2:
return bonusTasks.count
default:
return 0
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.section {
case 0:
let cell = tableView.dequeueReusableCell(withIdentifier: "NewTaskCellID", for: indexPath) as! NewTaskTableViewCell
cell.delegate = self
return cell
case 1:
let cell = tableView.dequeueReusableCell(withIdentifier: "TaskCellID", for: indexPath) as! TaskTableViewCell
let task = hotTaskDataSource(indexPath: indexPath)
cell.setCaption(task?.caption)
cell.delegate = self
return cell
case 2:
let cell = tableView.dequeueReusableCell(withIdentifier: "TaskCellID", for: indexPath) as! TaskTableViewCell
let task = hotTaskDataSource(indexPath: indexPath)
cell.setCaption(task?.caption)
cell.delegate = self
return cell
default:
return UITableViewCell.init()
}
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section {
case 1:
return "Top Priority"
case 2:
return "Bonus"
default:
return nil
}
}
func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
return nil
}
func hotTaskDataSource(indexPath: IndexPath) -> HotTask? {
switch indexPath.section {
case 1:
return priorityTasks[indexPath.row]
case 2:
return bonusTasks[indexPath.row]
default:
return nil
}
}
func updateProgress() {
// calculate the initial values for task count
let totalTasks = priorityTasks.count + bonusTasks.count
let completedTasks = priorityTasks.filter { (task) -> Bool in
return task.completed == true
}.count + bonusTasks.filter { (task) -> Bool in
return task.completed == true
}.count
// declare a caption variable
var caption = "What's going on?!"
// handle range possible scenarios
if totalTasks == 0 { // no tasks
caption = "It's lonely here - add some tasks!"
}
else if completedTasks == 0 { // nothing completed
caption = "Get started - \(totalTasks) to go!"
}
else if completedTasks == totalTasks { // all completed
caption = "Well done - \(totalTasks) completed!"
}
else { // completedTasks less totalTasks
caption = "\(completedTasks) down \(totalTasks - completedTasks) to go!"
}
// assign the progress caption to the label
progressLabel.text = caption
}
func populateInitialTasks () {
priorityTasks.removeAll()
priorityTasks.append(HotTask.init(caption: "Pickup MacBook Pro from Apple store Pickup MacBook Pro from Apple store - type twice for extra length!;)"))
priorityTasks.append(HotTask.init(caption: "Practice Japanese"))
priorityTasks.append(HotTask.init(caption: "Buy ingredients for a cake for Alie's bday"))
bonusTasks.removeAll()
let hotTask = HotTask.init(caption: "Shop for funnky socks")
hotTask.priority = .bonus
bonusTasks.append(hotTask)
}
func configure() {
tableView.delegate = self
tableView.dataSource = self
populateInitialTasks()
updateProgress()
registerForKeyboardNotifications()
}
override func viewDidLoad() {
super.viewDidLoad()
configure()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Test this now!

It's working !
Let's Recap!
In order for a text input to accept keyboard input, it must become a first responder and to finish accepting input it must reigns first responder. There are two corresponding methods that can be used to manage it programmatically:
textField.becomeFirstResponder()textField.resignFirstResponder()An object that needs to perform text field input must conform to
UITextFieldDelegate
protocol.To capture an intention to finish providing keyboard input, we can implement a delegate method:
textFieldShouldReturn
.Capturing a user's typing can be done by creating a text field action on
EditingChanged
event.To manage the keyboard, an application must sign up for related events in Notification Center by adding an observer and provide an event handling method for each addition.