Chapters

Hide chapters

UIKit Apprentice

Third Edition · iOS 18 · Swift 5.10 · Xcode 16

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 13 chapters
Show chapters Hide chapters

12. Add Item Screen
Written by Fahim Farook

Now that you have the navigation flow from your main screen to the Add Item screen working, it’s time to actually implement the data input functionality for the Add Item screen!

Let’s change the look of the Add Item screen. Currently it is an empty table with a navigation bar on top, but I want it to look like this:

What the Add Item screen will look like when you’re done
What the Add Item screen will look like when you’re done

This chapter covers the following:

  • Static table cells: Add a static table view cell to the table to display the text field for data entry.
  • Read from the text field: Access the contents of the text field.
  • Polish it up: Improve the look and functionality of the Add Item screen.

Static table cells

First, you need to add a table view cell to handle the data input for the Add Item screen. As is generally the case with UI changes, you start with the storyboard.

Storyboard changes

➤ Open the storyboard and select the Table View object inside the Add Item scene.

➤ In the Attributes inspector, change the Content setting from Dynamic Prototypes to Static Cells.

Changing the table view to static cells
Changing the table view to static cells

You use static cells when you know beforehand how many sections and rows the table view will have. This is handy for screens that require the user to enter data, such as the one you’re building here.

With static cells, you can design the rows directly in the storyboard. For a table with static cells you don’t need to provide a data source, and you can hook up the labels and other controls from the cells directly to outlets on the view controller.

As you’ll see in the Document Outline, the table view now has a Table View Section object under it, and three Table View Cells in that section — you may need to expand the Table View item first by clicking the disclosure triangle.

The table view has a section with three static cells
The table view has a section with three static cells

➤ Select the bottom two cells and delete them by pressing the delete key on your keyboard. You only need one cell for now.

➤ Select the Table View again and in the Attributes inspector set its Style to Grouped. That gives us the look we want.

The table view with grouped style
The table view with grouped style

Next up, you’ll add a text field component inside the table view cell that lets the user type text.

➤ Drag a Text Field object into the cell and size it up nicely. You might want to add left, top, right, and bottom Auto Layout constraints to the text field if you don’t want any Xcode warnings. You know how to do that on your own, right? (Hint: use the Add New Constraints button at the bottom of the Interface Builder screen after you’ve sized/positioned the field as you want.)

➤ In the Attributes inspector for the text field, set the Border Style to no border (select the dotted box):

Adding a text field to the table view cell
Adding a text field to the table view cell

➤ Run the app and press the + button to open the Add Item screen. Tap on the cell and you’ll see the keyboard slide in from the bottom of the screen.

Any time you make a text field active, the keyboard automatically appears. You can type into the text field by tapping on the letters. On the Simulator, you can also type using your Mac’s keyboard.

You can now type text into the table view cell
You can now type text into the table view cell

Note: If the keyboard does not appear in the Simulator, press ⌘K or use the I/O ▸ Keyboard ▸ Toggle Software Keyboard menu option. You can also use your normal Mac keyboard to type into the text field, even if the on-screen keyboard is not visible. If that doesn’t work, also select I/O ▸ Keyboard ▸ Connect Hardware Keyboard from the menu.

Disable cell selection

Look what happens when you tap just outside the text field’s area, but still in the cell (try tapping in the margins that surround the text field):

Whoops, that looks a little weird
Whoops, that looks a little weird

The row turns gray because you selected it. Oops, that’s not what you want — you should disable selections for this row. You can do this easily via code by adding the following table view delegate method to AddItemViewController.swift:

// MARK: - Table View Delegates
override func tableView(
  _ tableView: UITableView,
  willSelectRowAt indexPath: IndexPath
) -> IndexPath? {
  return nil
}

When the user taps on a cell, the table view sends the delegate a willSelectRowAt message that says: “Hi delegate, I am about to select this particular row.”

By returning the special value nil, the delegate answers: “Sorry, but you’re not allowed to!”

Return to sender

You’ve seen the return statement a few times now. You use return to send a value from a method back to the method that called it.

Let’s take a more detailed look at what happens.

Method 1 method2() method3() return value method4() return value void Method 2 Method 3 Method 4

Methods call other methods and receive values in return.

You cannot just return any value. The value you return must be of the data type that is specified after the -> arrow that follows the method name.

For example, tableView(_:numberOfRowsInSection:) must return an Int value:

override func tableView(
  _ tableView: UITableView,
  numberOfRowsInSection section: Int
) -> Int {
  return 1
}

If instead your code was like this:

override func tableView(
  _ tableView: UITableView,
  numberOfRowsInSection section: Int
) -> Int {
  return "1"
}

Then, the compiler would give an error message, as "1" is a string, not an Int. To a human reader they look similar and you can easily understand the intent, but Swift isn’t that tolerant. Data types have to match or they just aren’t allowed.

Your most recent version of this method looks like this:

override func tableView(
  _ tableView: UITableView,
  numberOfRowsInSection section: Int
) -> Int {
  return items.count
}

That is also a valid return statement because items is an Array and the count property from Array is also of the type Int.

The tableView(_:cellForRowAt:) method is supposed to return a UITableViewCell object:

override func tableView(
  _tableView: UITableView,
  cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(
    withIdentifier: "TheCellIdentifier",
    for: indexPath)
  . . .
  return cell
}

The local constant cell contains a UITableViewCell object, so it’s OK to return the value of cell from the method.

The tableView(_:willSelectRowAt:) method is supposed to return an IndexPath object. However, you can also make it return nil, indicating no value/object.

override func tableView(
  _ tableView: UITableView,
  willSelectRowAt indexPath: IndexPath
) -> IndexPath? {
  return nil
}

That’s what the ? behind IndexPath is for: The question mark tells the Swift compiler that you can also return nil from this method. Note that returning nil from a method is only allowed if there is a question mark (or exclamation point) behind the return type. A type declaration with a question mark behind it is known as an optional — you’ll learn more about optionals in the next chapter.

The special value nil represents “no value” but it’s used to mean different things throughout the iOS SDK. Sometimes it means “nothing found” or “don’t do anything”. Here it means that the row should not be selected when the user taps it.

How do you know what nil means for a certain method? You can find that in the documentation of the method in question.

In the case of willSelectRowAt, the iOS documentation says:

Return Value: An index-path object that confirms or alters the selected row. Return an IndexPath object other than indexPath if you want another cell to be selected. Return nil if you don’t want the row selected.

This means you can either:

  1. Return the same index-path you were given. This confirms that this row can be selected.
  2. Return another index-path in order to select a different row.
  3. Return nil to prevent the row from being selected, which is what you did.

So remember, you need to use the return statement to exit a method that expects to return something. If you forget, then Xcode will give the following error: “Missing return in a function expected to return <data type>”.

You’ve also seen methods that do not return anything:

@IBAction func addItem()

and:

func configureCheckmark(for cell: UITableViewCell, with item: ChecklistItem)

These methods do not have an arrow (->) indicating a return value. Such a method does not pass a value back to the caller and therefore does not need a return statement. You can still use return to exit from such methods, but the return statement should not be followed by a value.

Strictly speaking, even methods without a return type do return a value, an empty tuple. Think of this as a special object that embodies the concept of “nothing” — don’t confuse this with nil, which is an actual value.

You sometimes see this written as:

func methodThatDoesNotReturnValue() -> ()

func anotherMethodThatDoesNotReturnValue() -> Void

The notation for an empty tuple is (), so in this context the parentheses mean there is no return value. The term Void is a synonym for ().

But really, if a method does not return anything it’s just as easy to leave out the -> arrow. Also note that @IBAction methods never return a value — this is a rule.

While it’s already impossible to select the row, as you’ve just told the table view you won’t allow it, there is one more thing you need to do to prevent the row from going gray. In fact, most of the time, this second change is enough to not show cell selection, even without the code change above. Table view cells have a selection color property. Even if you make it impossible for a row to be selected, sometimes UIKit still briefly draws the cell gray when you tap it. Therefore, it is best to also disable this selection color.

➤ In the storyboard, select the table view cell and go to the Attributes inspector. Set the Selection attribute to None. Now if you run the app, it is impossible to select the row and make it turn gray. Try and prove me wrong! :]

Read from the text field

You have a text field in a table view cell that the user can type into, but how do you read the text that the user has typed?

Add an outlet for the text field

When the user taps Done, you need to get that text and somehow put it into a new ChecklistItem and add it to the list of to-do items. This means the done() action needs to be able to refer to the text field.

You already know how to refer to controls from within your view controller: use an outlet. When you added outlets for the previous app, I told you to type in the @IBOutlet declaration in the source file and make the connection in the storyboard.

I’m going to show you a trick now that will save you some typing. You can let Interface Builder do all of this automatically by Control-dragging from the control in question directly into your source code file!

➤ First, go to the storyboard and select the Add Item View Controller. Then open the Assistant editor using the Adjust Editor Options toolbar button.

Click the toolbar button to open the Assistant editor
Click the toolbar button to open the Assistant editor

This may make the screen a little crowded – there might now be up to five horizontal panels open. If you’re running out of space, you might want to close the Project navigator, the Utilities pane, and/or the Document Outline using the relevant toolbar buttons.

The Assistant editor opens a new pane on the right of the screen by default — it might give you horizontal split views instead, if you have changed your default view settings.

In the Jump Bar (the bar below the toolbar) it should say Automatic and the Assistant editor should be displaying the AddItemViewController.swift file:

The Assistant editor
The Assistant editor

“Automatic” means the Assistant editor tries to figure out what other file is related to the one you’re currently editing. When you’re editing a storyboard, the related file is generally the selected view controller’s Swift file.

Sometimes Xcode can be a little dodgy here. If it shows you something other than AddItemViewController.swift, then click in the Jump Bar and manually select the correct file.

➤ With the storyboard and the Swift file side by side, select the text field. Then Control-drag from the text field into the Swift file.

Control-dragging from the text field into the Swift file
Control-dragging from the text field into the Swift file

When you let go, a popup appears:

The popup that lets you add a new outlet
The popup that lets you add a new outlet

➤ Choose the following options:

  • Connection: Outlet
  • Name: textField
  • Type: UITextField
  • Storage: Weak

Note: If “Type” does not say UITextField, but instead says UITableViewCell or UIView, then you selected the wrong component on Interface Builder.

Make sure you’re Control-dragging from the text field inside the cell, not the cell itself. Granted, it’s kinda hard to see, being white on white. If you’re having trouble selecting the text field, click that area several times in succession.

You can also Control-drag from “No Border Style Text Field” in the Document Outline.

➤ Press Connect and voila, Xcode automatically inserts an @IBOutlet for you and connects it to the text field object.

The new code looks like this:

@IBOutlet weak var textField: UITextField!

Just by dragging you have successfully hooked up the text field object with a new property named textField. How easy was that?

Read the contents of the text field

Now you’ll modify the done() action to write the contents of this text field to the Xcode Console, the pane at the bottom of the screen where print() messages show up. This is a quick way to verify that you can access what the user typed.

➤ In AddItemViewController.swift, change done() to:

@IBAction func done() {
  // Add the following line
  print("Contents of the text field: \(textField.text!)")

  navigationController?.popViewController(animated: true)
}

You can make these changes directly inside the Assistant editor. It’s very handy that you can edit the source code and the storyboard side-by-side.

➤ Run the app, press the + button and type something in the text field. When you press Done, the Add Item screen should close and Xcode should reveal the Debug pane with a message like this:

Contents of the text field: Hello, world!

Great, so that works! print() should be an old friend by now. It’s one of my faithful debugging companions :]

Recall that you can print the value of a variable by placing it inside \( and ) in a string. Here you used \(textField.text!) to print out the contents of the text field’s text property — I’ll explain what the exclamation point is for later.

Note: Because the iOS Simulator already outputs a lot of debug messages of its own, it may be a bit hard to find your print() messages in the Console. Luckily there is a Filter box at the bottom that lets you search for your own messages — just type in what you’re looking for into the filter box.

Polish it up

Before you write the code to take the text and insert it as a new item into the items list, let’s improve the design and workings of the Add Item screen a little.

Give the text field focus on screen opening

For instance, it would be nice if you didn’t have to tap on the text field in order to bring up the keyboard. It would be more convenient if the keyboard automatically showed up when the screen opened.

➤ To accomplish this, add a new method to AddItemViewController.swift.

override func viewWillAppear(_ animated: Bool) {
  super.viewWillAppear(animated)
  textField.becomeFirstResponder()
}

The view controller receives the viewWillAppear() message just before it becomes visible. That is a perfect time to make the text field active. You do this by sending it the becomeFirstResponder() message.

If you’ve done programming on other platforms, this is often called “giving the control focus”. In iOS terminology, the control becomes the first responder.

➤ Run the app and go to the Add Item screen; you can start typing right away.

Again, note that the keyboard may not appear on the Simulator. Press ⌘+K to bring it up. The keyboard will always appear when you run the app on an actual device, though.

It’s often little features like these that make an app a joy to use. Having to tap on the text field before you can start typing gets old really fast. In this fast-paced age, using their mobiles on the go, users don’t have the patience for that. Such minor annoyances may be reason enough for users to switch to a competitor’s app. I always put a lot of effort into making my apps as frictionless as possible.

Style the text field

With that in mind, let’s style the input field a bit.

➤ Open the storyboard and select the text field. Go to the Attributes inspector and set the following attributes:

  • Font: System 17
  • Placeholder: Name of the Item
  • Adjust to Fit: Uncheck this
  • Capitalization: Sentences
  • Return Key: Done

The text field attributes
The text field attributes

There are several options here that let you configure the keyboard that appears when the text field becomes active.

If this were a field that only allowed numbers, for example, you would set the Keyboard Type to Number Pad. If it were an email address field, you’d set it to E-mail Address. For our purposes, the Default keyboard is appropriate.

You can also change the text that is displayed on the keyboard’s Return Key. By default it says “return” but you set it to “Done”. This is just the text on the button; it doesn’t automatically close the screen. You still have to make the keyboard’s Done button trigger the same action as the Done button from the navigation bar.

Handle the keyboard Done button

➤ Make sure the text field is selected and open the Connections inspector. Drag from the Did End on Exit event to the view controller and pick the done action.

If you still have the assistant editor open, you can also drag directly to the source code for the done() method.

Connecting the text field to the done() action method
Connecting the text field to the done() action method

To see the connections for the done action, click on the circle in the gutter next to the method name. The popup shows that done() is now connected to both the bar button and the text field:

Viewing the connections for the done() method
Viewing the connections for the done() method

➤ Run the app. Pressing Done on the keyboard will now close the screen and print the text to the debug area.

Disallow empty input

Now that you have user input working, It’s always good to validate what the user entered to make sure that the input is acceptable. For instance, what should happen if the user immediately taps the Done button on the Add Item screen without entering any text?

Adding a to-do item to the list that has no description text is not very useful. So, in order to prevent this, you should disable the Done button when no text has been typed yet.

Of course, you have two Done buttons to take care of, one on the keyboard, and one in the navigation bar. Let’s start with the Done button from the keyboard as this is the simplest one to fix.

➤ On the Attributes inspector for the text field, check Auto-enable Return Key.

That’s it. Now when you run the app, the Done button on the keyboard is disabled when there is no text in the text field. Try it out!

The Auto-enable Return Key option disables the return key when there is no text
The Auto-enable Return Key option disables the return key when there is no text

For the Done button in the navigation bar, you have to do a little more work. You have to check the contents of the text field after every keystroke to see if it is now empty or not. If it is, then you disable the button.

The user can always press Cancel, but Done only works when there is text.

In order to listen to changes to the text field – which may come from taps on the keyboard but also from cut/paste – you need to make the view controller a delegate for the text field.

The text field will send events to its delegate to let it know what is going on. The delegate, which will be the AddItemViewController, can then respond to these events and take appropriate actions.

A view controller is allowed to be the delegate for more than one object. The AddItemViewController is already a delegate (and data source) for the UITableView — because it is a UITableViewController. Now it will also become the delegate for the text field object, UITextField.

These are two different delegates and you make the view controller play both roles. Later on you’ll add even more delegates for this app.

How to become a delegate

Delegates are used everywhere in the iOS SDK, so it’s good to remember that it always takes three steps to become a delegate.

  1. You declare yourself capable of being a delegate. To become the delegate for UITextField you need to include UITextFieldDelegate in the class line for the view controller. This tells the compiler that this particular view controller can actually handle the notification messages that the text field sends to it.

  2. You let the object in question, in this case the UITextField, know that the view controller wishes to become its delegate. If you forget to tell the text field that it has a delegate, it will never send you any notifications.

  3. Implement the delegate methods. It makes no sense to become a delegate if you’re not responding to the messages you’re being sent!

Often, delegate methods are optional, so you don’t need to implement all of them. For example, UITextFieldDelegate actually declares seven different methods but you only care about textField(_:shouldChangeCharactersIn:replacementString:) for this app.

➤ In AddItemViewController.swift, add UITextFieldDelegate to the class declaration:

class AddItemViewController: UITableViewController, UITextFieldDelegate {

The view controller now says, “I can be a delegate for text field objects.”

You also have to let the text field know that you have a delegate for it.

➤ Go to the storyboard and select the text field.

There are several different ways in which you can hook up the text field’s delegate outlet to the view controller. I prefer to go to its Connections inspector and drag from delegate to the view controller’s little yellow icon:

Drag from the Connections inspector to connect the text field delegate
Drag from the Connections inspector to connect the text field delegate

Configure the Done button

You also have to add an outlet for the Done bar button item, so you can send it messages from within the view controller in order to enable or disable it.

➤ Open the Assistant editor and make sure AddItemViewController.swift is visible in the assistant pane.

Control-drag from the Done bar button into the Swift file and let go. Name the new outlet doneBarButton.

This adds the following outlet:

@IBOutlet weak var doneBarButton: UIBarButtonItem!

➤ Add the following to AddItemViewController.swift, at the bottom (before the final curly brace):

// MARK: - Text Field Delegates
func textField(
  _ textField: UITextField,
  shouldChangeCharactersIn range: NSRange,
  replacementString string: String
) -> Bool {
  let oldText = textField.text!
  let stringRange = Range(range, in: oldText)!
  let newText = oldText.replacingCharacters(
    in: stringRange,
    with: string)
  if newText.isEmpty {
    doneBarButton.isEnabled = false
  } else {
    doneBarButton.isEnabled = true
  }
  return true
}

This is one of the UITextField delegate methods. It is invoked every time the user changes the text, whether by tapping on the keyboard or via cut/paste.

First, you figure out what the new text will be:

let oldText = textField.text!
let stringRange = Range(range, in:oldText)!
let newText = oldText.replacingCharacters(
  in: stringRange,
  with: string)

The textField(_:shouldChangeCharactersIn:replacementString:) delegate method doesn’t give you the new text, only which part of the text should be replaced — the range — and the text it should be replaced with — the replacement string.

You need to calculate what the new text will be by taking the text field’s text and doing the replacement yourself. This gives you a new string object that you store in the newText constant.

NSRange vs. Range and NSString vs. String

In the above code, you get a parameter as NSRange and you convert it to a Range value. If you’re wondering what a range is, the clue is in the name :] A range object gives you a range of values, or in this case, a range of characters — with a lower bound and an upper bound.

So, why did we convert the original NSRange value to a Range value, you ask? NSRange is an Objective-C structure whereas Range is its Swift equivalent — they are similar, but not exactly the same. So, while an NSRange parameter is used by the UITextField (which internally, and historically, is Objective-C based) in its delegate method, in our Swift code, if we wanted to do any String operations, such as replacingCharacters, then we need a Range value instead. Swift methods generally use Range values and do not understand NSRange values, which is why we converted the NSRange value to a Swift-understandable Range value.

There was a different way to approach this problem as well — though it might not be as “Swift-y” :] We could have converted the Swift String value into its Objective-C equivalent — NSString. String and NSString are “bridged”, meaning that you can use NSString in place of String. And NSString too has a replacingCharacters(in:with:) method, and that method takes an NSRange as a parameter!

So, you could have simply converted the String value to an NSString value and then used the NSString replacingCharacters(in:with:) method with the passed in range value instead of the above code.

But personally, I prefer to use Swift types and classes in my code as much as possible. So, I opted to go with the solution above :]

By the way, String isn’t the only object that is bridged to an Objective-C type. Another example is Array and its Objective-C counterpart NSArray. Because the iOS frameworks are written in a different language than Swift, sometimes these little Objective-C holdovers pop up when you least expect them.

Once you have the new text, you check if it’s empty, and enable or disable the Done button accordingly:

if newText.isEmpty {
  doneBarButton.isEnabled = false
} else {
  doneBarButton.isEnabled = true
}

However, you could simplify the above code even further. Since newText.isEmpty returns a true or false value, you can discard the if condition and use the value returned by newText.isEmpty to decide whether the Done button should be enabled or not.

doneBarButton.isEnabled = !newText.isEmpty

Basically, if the text is not empty, enable the button. Otherwise, don’t enable it. That’s much more compact, and concise, right?

Remember this trick – whenever you see code like this,

if some condition {
  something = true
} else {
  something = false
}

you can write it simply as:

something = (some condition)

In practice it doesn’t really matter which version you use. I prefer the shorter one; that’s what the pros do. Just remember that comparison operators such as == and > always return true or false, so the extra if really isn’t necessary.

➤ Run the app and type some text into the text field. Now remove that text and you’ll see that the Done button in the navigation bar properly gets disabled when the text field becomes empty.

Fixing issues

One problem: The Done button is initially enabled when the Add Item screen opens, but there is no text in the text field at that point. So, it really should be disabled. This is simple enough to fix.

➤ In the storyboard, select the Done bar button and go to the Attributes inspector. Uncheck the Enabled box.

The Done button is now properly disabled when you first enter the Add Item screen:

The Done button is not enabled if there is no text
The Done button is not enabled if there is no text

There’s one other possible issue here. You won’t see the issue unless you enable the Clear Button though. I hear you ask “What’s the Clear Button?”.

Select the text field in the Add Item scene in the storyboard and take a look at the Attributes inspector. You will note that there’s a setting named Clear Button. This enables a handy button in text fields which allows you to clear text.

The Clear Button
The Clear Button

The Clear Button is set to “Never appears” by default, but you can set it to “Appears while editing” to give your users a quick and easy way to clear text. This can be a very user-friendly feature.

Unfortunately, if you enable this option and clear your text while you’re in the Add Item screen, the Done button will not get disabled. This is because the Clear Button does not call the textField(_:shouldChangeCharactersIn:replacementString:) delegate method. Instead, the Clear Button calls a different delegate method — textFieldShouldClear(_:) method.

So, if you want to handle the Clear button correctly and disable the Done button, then you’d have to add the following delegate method to AddItemViewController.swift:

func textFieldShouldClear(_ textField: UITextField) -> Bool {
  doneBarButton.isEnabled = false
  return true
}

Using FileMerge to compare files

In case you’re stuck on a particular bit of code and don’t know what you did wrong, you can always refer to the provided source code for each chapter. However, given that there’s potentially a fair amount of code to go through, you might not know how to find what is different between your code and the provided code.

You can compare your own work with my version of the app using the FileMerge tool. Open this tool from the Xcode menu bar, under Xcode ▸ Open Developer Tool ▸ FileMerge:

Open FileMerge
Open FileMerge

You give FileMerge two files or folders to compare:

After working hard for a few seconds or so, FileMerge tells you what is different:

Double-click on a filename from the list to view the differences between the two files:

FileMerge is a wonderful tool for spotting the differences between two files or even entire folders. I use it all the time!

If something from the book doesn’t work as it should, then do a “diff” — that’s what the difference between two source files is called — between your own files and the ones from the Source Code folder to see if you can find any anomalies.

You can find the project files for the app up to this point under 12-Add-item-screen in the Source Code folder.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.