Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

My Locations

Section 4: 11 chapters
Show chapters Hide chapters

Store Search

Section 5: 13 chapters
Show chapters Hide chapters

23. Edit High Score Screen
Written by Eli Ganim

Now that you have the navigation flow from your main screen to the Edit High Score screen working, it’s time to actually implement the edit functionality for this screen!

Let’s change the look of the Edit screen. Currently, it is an empty table with a navigation bar on top — but it’s going 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 Edit High Score screen.

Static table cells

First, you need to add a table view cell to handle the data input for the Edit High Score 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 Edit High Score 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 can 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.

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 by selecting 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 click on any high score to open the Edit High Score screen. Tap on the cell with the text field and you’ll see the keyboard slide in from the bottom of the screen.

Disabling cell selection

Look what happens when you tap just outside of 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 EditHighScoreViewController.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!”

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

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 indexPath 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 IndexPath you were given. This confirms that this row can be selected.
  2. Return another IndexPath to select a different row.
  3. Return nil to prevent the row from being selected, which is what you did.

Working with the text field

You have a text field in a table view cell that the user can type into. How do you populate it with the current name from the HighScoreItem? And how do you read the text that the user has typed?

Adding an outlet for 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, you typed in the @IBOutlet declaration in the source file and make the connection in the storyboard.

You’re going to see 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 Edit High Score View Controller. Then, open the Assistant editor using the toolbar button on the top right.

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, below the toolbar, it should say Automatic and the Assistant editor should be displaying the EditHighScoreViewController.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 EditHighScoreViewController.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 pop-up appears:

The pop-up that lets you add a new outlet
The pop-up 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 thing.

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.

In code it 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?

Reading from 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 actually read what the user typed.

➤ In EditHighScoreViewController.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, go to the high scores screen, click on any high score to navigate to the Edit High Score screen and type something in the text field. When you press Done, the Edit High Score 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 the faithful debugging companions.

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.

Polishing it up

Before you write the code to take the text and update the high score item, let’s improve the design and workings of the Edit High Score screen a little.

Giving the text field focus on-screen opening

For instance, it would be nice if you didn’t have to tap on the text field 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 EditHighScoreViewController.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 Edit High Score 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 phones 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.

Styling 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:

  • Placeholder: High scorer name
  • Font: System 17
  • 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.

Handling 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 pop-up 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

➤ Go to the Edit High Score screen. Pressing Done on the keyboard will now close the screen and print the text to the debug area.

The keyboard now has a big blue Done button
The keyboard now has a big blue Done button

Disallowing 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 taps the Done button on the Edit High Score screen without entering any text?

Having a high score that has no name is not very useful. So, 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. 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 EditHighScoreViewController, 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 EditHighScoreViewController 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.

Becoming 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 EditHighScoreViewController.swift, add UITextFieldDelegate to the class declaration:

class EditHighScoreViewController: 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. One way is 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

Configuring 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 to enable or disable it.

➤ Open the Assistant editor and make sure EditHighScoreViewController.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 EditHighScoreViewController.swift, at the bottom and 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. This 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. Since Swift is still young, its String handling methods aren’t as good … but they are getting better. NSString is considered by some to be more powerful and often easier to use than Swift’s own String.

String and NSString are “bridged,” meaning that you can use NSString in place of String.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.

You can find the project files for the app up to this point under 23-Edit High Score 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.