VNUHCM
Previous lesson Next lesson
  • Learning content
  • Help
    Do you have any questions while learning?
    Learning instructions Frequently asked questions Email for support
    • Tiếng Việt
    • English
    • Member's information
    • Registered courses
    • Log out
  • Cohota
  • HƯỚNG DẪN HỌC TẬP

  • View detail >>
    You have completed 0% of the course
  • HƯỚNG DẪN SINH VIÊN ĐĂNG NHẬP HỆ THỐNG
    • Hướng dẫn đăng nhập
    • Hướng dẫn vào khóa học
  • Introduction
    • Welcome
  • Unit 1: Values
    • Introduction - Unit 1: Values
    • Get Started With Values
    • Play with Values
    • Playground Basics
    • Naming and Identifiers
    • Simulation
    • Strings
    • Constants and Variables
    • Word Games
    • Build a PhotoFrame App
    • Design for People
  • Episode 1: The TV Club
    • Introduction - Episode 1: The TV Club
    • Searching for Content
    • Sharing Personal Information
    • Ordering Online
    • Reflection: Episode 1
  • Unit 2: Algorithms
    • Introduction - Unit 2: Algorithms
    • Get Started with Algorithms
    • Play with Programs
    • Functions
    • Types
    • Parameters and Results
    • Making Decisions
    • BoogieBot
    • Data Visualization
    • Build a QuestionBot App
    • Design an Experience
  • Episode 2: The Viewing Party
    • Introduction - Episode 2: The Viewing Party
    • Accessing the Show
    • Streaming on the Network
    • Reflection: Episode 2
  • Unit 3: Organizing Data
    • Introduction - Unit 3: Organizing Data
    • Get Started with Organizing Data
    • Play with Complex Data
    • Instances, Methods, and Properties
    • Arrays and Loops
    • Structures
    • Enums and Switch
    • Testing Code
    • Processing Data
    • Pixel Art
    • Password Security
    • Visualization Revisited
    • Build a BouncyBall App
    • Design a Prototype
  • Episode 3: Sharing Photos
    • Introduction - Episode 3: Sharing Photos
    • Capturing Images
    • Posting on Social Media
    • Reflection: Episode 3
  • Unit 4: Building Apps
    • Introduction - Unit 4: Building Apps
    • Get Started with App Development
    • Play with App Components
    • Color Picker
    • ChatBot
    • Rock, Paper, Scissors
    • MemeMaker
    • Build an ElementQuiz App
    • Design for Impact
  • Appendix
    • Episode Technical Concepts
    • Glossary
Course overview
Assessment

Progress
Criteria name Weighting (%) Score Progress (%)
Unit 4: Building Apps

Build an ElementQuiz App

Unit 4: Building Apps|Build

What you’ll build

  • An app for studying chemical elements with flash cards and a quiz.

What you’ll learn

  • How to create and display images in code.
  • How to use the single-path UI update pattern to manage your UI code.
  • How to get text from the keyboard.
  • How to display an alert and respond when its button is tapped.

Key Vocabulary

  • Alert
  • Steps to reproduce
  • Text field

Introduction

Your previous app projects have ranged from simple to complex, focusing on different elements of development. You’ve thought through the brains and logic of an app, as well as the user interface elements for presenting data and gathering user input. In this project, you’ll apply all those skills to build an app that quizzes users on the elements of the periodic table.

You’ll start with a flash card–style interface. The user will see the element symbol and atomic weight, and can tap a button to reveal the name of the element. Next, you’ll create a quiz mode, enabling the user to input the correct element name and to receive a score at the end.

Final app showing the flash card interface

Final app showing the quiz interface with the keyboard showing

Final app showing the quiz interface on the final question with the Show Answer button visible

Part 1 - Creating the Interface

To start, you’ll build the flash card interface in your storyboard.

  1. In Xcode, create a new project.

  2. Use the iOS App template.

  3. Name the project “ElementQuiz” and save it to the desktop.

  4. Select the Main storyboard in the project navigator.

  5. Select iPhone 14 Pro from the Devices icon at the bottom of the screen.

Adding Elements

The flash card interface consists of four elements, from top to bottom:

  • An image view to display an image of the chemical element
  • A label to display either a question mark or the name of the element
  • A button to show the answer
  • A button to move on to the next element

Drag these items from the Object library onto the canvas. Remember that you can use the filter bar at the top of the library to narrow your search. Put the image view at the top and the answer label right below it, then place the two buttons side by side under the label. Center them vertically by using the dotted guidelines that appear as you’re dragging them. (If you select both buttons at once, you can center them as a group.)

Initial layout of the scene

Image View Size

Some image views display many different kinds, shapes, and sizes of images. Others display images of a consistent shape and size. Since all the chemical element images are the same size, it’s a good idea to set the size of the image view to match.

Change the size of the image view:

  1. Select the image view.

  2. In the Size inspector, set the width to 140 and the height to 140.

Configure Label and Buttons

Next you’ll need to configure the label and the buttons.

Configure the label:

  1. Select the label.
  2. In the Attributes inspector, in the Font field, click the “T” button to display the Font popup menu. 1
  3. Change the Style to Bold and the Size to 24, then click Done.
  4. Set the Alignment to center.
  5. Change the string in the label to “Answer Label.”

Image showing the Font popover from the Attributes inspector for the label

Even though the contents of this label will be changed at runtime, setting a string value to something other than “Label” makes the purpose of the label clear, both in the storyboard canvas and in the outline view to the left.

Configure the buttons:

  1. Set the title of the button on the left to “Show Answer.”
  2. Set the title of the button on the right to “Next Element.”

If the label and buttons are no longer centered after making your changes, center them again. The final layout should look something like the image below.

Layout of the interface of the app after adjustments to image view, label, and buttons

Part 2 - Adding Data

When you build and run the ElementQuiz app on the iPhone 14 Pro simulator, you’ll see almost exactly what you just laid out on the storyboard (except the image view, which won’t appear at all). Of course, nothing happens when you click the buttons, because you haven’t written any code yet. To begin filling your UI with actual data, you’ll add images to your project. You’ll also create a simple data model to help you populate the image view and the answer label.

Adding Images

In the Finder, open the folder named “ElementQuiz” in your course resources. Add the images in this folder to the Asset Catalog of your Xcode project:

  1. Select Assets from the project navigator (see image left).

  2. Drag the images from the Element Images folder in the Finder to the space below the AppIcon entry in the Xcode Asset Catalog.

Adding Outlets

As the user navigates through different elements in ElementQuiz, you’ll need to update the image view and the answer label.

  1. Create an outlet named imageView for the image view.
  2. Create an outlet named answerLabel for the answer label.

Adding Code

You’ve created a user interface and created outlets in your view controller. Now it’s time to add some code to update the user interface using those outlets.

Data Models

The app will show a list of chemical element symbols one at a time. Because the Array type is the way to store lists in Swift, the data model for this app will be an array of element names.

Add a constant property called elementList to ViewController, and initialize it with the following element names:

let elementList = ["Carbon", "Gold", "Chlorine", "Sodium"]`

You’ll also need to keep track of which element is currently displayed, so you can access the image and answer that correspond to it. Remember that you access items in an array by index, so your code will keep track of the current array index.

Add a variable property called currentElementIndex to ViewController, and initialize it to 0:

var currentElementIndex = 0

Note that elementList is constant—it won’t change as the app runs. But currentElementIndex is variable. As the user navigates through the elements, your code will update the current index.

Updating the Element

When the app first launches, you’ll display the first element. You’ll also need to update which element is displayed when the user clicks the Next Element button. You can combine the core logic for both steps into a single method.

Write an updateElement() method in ViewController that sets the answer label to "?" and creates and sets the correct image.

func updateElement() {
    let elementName = elementList[currentElementIndex]
    let image = UIImage(named: elementName)
    imageView.image = image

    answerLabel.text = "?"
}

Here’s a breakdown of what’s going on.

The line below accesses the element name from the list using the currentElementIndex property as the index:

let elementName = elementList[currentElementIndex]

This line creates a new UIImage instance by looking for an image in the Asset Catalog with a matching name:

let image = UIImage(named: elementName)

This line sets the image of the image view to the newly created image instance:

imageView.image = image

This line sets the text of the answer label (using the text property of the UILabel type) to a question mark. The app will display the actual answer when the Show Answer button is tapped:

answerLabel.text = "?"

To show the first element in the list when the app launches and the view loads, you’ll add a call to updateElement() to viewDidLoad():

override func viewDidLoad() {
    super.viewDidLoad()
    updateElement()
}

Checkpoint

Build and run the app. The image of the first element should appear, and the answer label should show a question mark. That’s good. But tapping the buttons still doesn’t cause any action. It’s time to make that happen.

Part 3 - Handling User Interaction

When the user taps the Show Answer button, you want the answer label to change from a question mark to the element name. When the user taps the Next Element button, you want your code to show the next chemical element. To make all this work, you need to add an action for each button and write the appropriate code that runs in response to user actions.

Creating Actions

  1. Create an action named showAnswer() connected to the Show Answer button.
  2. Create an action named next() connected to the Next Element button.

Showing the Answer

To show the answer, you’ll access the name of the element at the current index and set that name as the text of the answerLabel.

@IBAction func showAnswer(_ sender: Any) {
    answerLabel.text = elementList[currentElementIndex]
}

Checkpoint

Build and run the app. Tapping the Show Answer button should now show the correct element.

Navigating to the Next Element

Now you need to add functionality to move to the next element.

Since you access items in an array by index, you can calculate the value of the next element by adding 1 to the current index. Once the current index is updated, you can call the updateElement() to update the user interface to display the element at the new current index.

@IBAction func next(_ sender: Any) {
    currentElementIndex += 1
    updateElement()
}

Checkpoint

Build and run the app. Tapping the Next Element button should show the next element.

What do you think will happen if you keep tapping the Next Element button after you reach the end of the list? Try it and see.

A Crash

Tapping the Next Element button on the last element in the list had a bad result. It froze the app in the simulator. You’ll see something like this in Xcode:

Xcode showing a crash, with items logged to the console and a line of code highlighted in red

The ElementQuiz app has crashed.

When your app crashes, there’s no reason to panic. The first thing to do is look at the console, which will sometimes have useful information about what went wrong. In this case, the last line in the console says:

fatal error: Index out of range

The red highlight shows you the line in the code where the error occurred:

let elementName = elementList[currentElementIndex]

According to this information, an index is used that’s outside of the expected range. And since the only index in that line of code is currentElementIndex, it must be the one that’s causing the fatal error.

The crash happened on the line of code where the index is used. But that’s not where you’ll go to fix it. If the index goes out of range, the problem must be where the value of the index is updated.

In this app, the update happens in the next() method:

@IBAction func next(_ sender: Any) {
    currentElementIndex += 1
    updateElement()
}

Stop and think about the problem. What are its symptoms? When you tapped the Next Element button, things worked fine until you got to the last element in the list. Do you have an idea what’s causing the problem?

Remember that you can only access an index if that index exists in the array. Otherwise there will be an “Index out of range” error.

The elementList contains four items with indexes 0, 1, 2, and 3. When the last element is shown, the value of currentElementIndex is 3, the last index.

Currently, the code keeps adding 1 to the index. So when you tap the button again, next() is called, and 1 is added to currentElementIndex. The value is now 4. But since there’s no index 4 in the array, the next time currentElementIndex is used to access the array, the crash occurs.

Fixing the Crash

For ElementQuiz, you want the app to go back to the beginning of the list when the user gets to the last element and taps the Next Element button. To implement this behavior without causing a crash, you’ll need to detect when you’ve arrived at the end of the list. At that point, you’ll reset the index to the start of the list.

@IBAction func next(_ sender: Any) {
    currentElementIndex += 1
    if currentElementIndex >= elementList.count {
        currentElementIndex = 0
    }

    updateElement()
}

Checkpoint

Build and run the app. Tapping the Next Element button should show the next element. Tap the button at the end of the list to make sure the app cycles successfully back to the beginning.

Part 4 - Adding Modes

Users can practice their element knowledge using your app. Now it’s time to add a mode to quiz themselves on their knowledge. You’ll add a new control to the app to switch between flash card and quiz modes, and implement the code and interface for quizzes.

Mode Enum

Your app will be in one of two modes: quiz or flash card. This is the perfect use case for a custom enum. Define an enum at the top of ViewController, below import UIKit and above the declaration of the ViewController class.

enum Mode {
    case flashCard
    case quiz
}

Add a new variable property to your view controller named “mode” of type Mode, and initialize it to .flashCard.

var mode: Mode = .flashCard

So far, so good! Next you’ll need to add some elements to the storyboard.

Quiz UI Elements

To switch between quiz and flash card modes, you’ll add a segmented control at the top of the screen. If there isn’t enough room for it, select all the existing UI elements and move them lower in the scene.

  1. Drag a UISegmentedControl from the Object library onto the canvas.
  2. Center it horizontally at the top of the screen.
  3. Change the titles of the segments by double-clicking them. Type “Flash Cards” in the left segment, and type “Quiz” in the right segment.

The quiz mode will be challenging for the user, because the answers are free response rather than multiple choice. To enable free response input, you’ll need to give your users a way to type text using the iOS keyboard. There are two primary controls for text input: UITextView and UITextField. UITextView allows multiple-line editing for long-form text, while UITextField is designed for small amounts of single-line text. A text field is the appropriate control for this app.

  1. Make room for the text field by moving the buttons lower in the scene.
  2. Drag a text field from the Object library onto the canvas.
  3. Center it below the Answer Label and above the buttons.
  4. Stretch the text field so it’s the same width as the image view.

You’ll need outlets for both of these new UI elements.

  1. Create an outlet named modeSelector for the segmented control.
  2. Create an outlet named textField for the text field.

Checkpoint

Build and run the app. Right now the segmented control does nothing. However, you’ll notice that, even without adding any code, the text field automatically displays the keyboard when it’s tapped. If you don’t see the keyboard when the text field is selected, go to Simulator and choose Hardware > Keyboard > Toggle Software Keyboard. (Also note that if you select Connect Hardware Keyboard, you can type on your computer’s keyboard in addition to tapping the keys on the simulator.)

If the keyboard covers any of your interface, go back to the storyboard and rearrange its contents to avoid the area the keyboard occupies. Your UI should look something like the screenshot below.

ElementQuiz app interface showing an active text field and keyboard

You’ll notice that there’s no way to dismiss they keyboard once it’s there. You’ll tackle that problem later.

Refactoring—UI State Management

To manage quiz mode, you’ll soon have quite a bit more code. In fact, if you’re not careful, you could end up with some very complicated code to write and debug. A good practice with view controllers is to keep your user interface updates localized to as few places as possible—ideally in one method. So when you need to change anything on the screen, you can funnel all your code to that one method.

For example, your current code updates the UI in updateElement() and also in showAnswer(). How will these methods evolve as you build your quiz mode, and how many more methods will you need for updating your quiz-related UI?

This is a good time to start using the single-path UI update pattern. If you keep all your UI update code in one method, you can use that method to answer the question, “For the current state of the app, how should the UI look?” Every time you add to your app’s capabilities, you’ll know where to put any code that updates the UI. And that practice will keep the rest of your code easier to understand and debug.

Consolidation

First, rename the updateElement() method to updateFlashCardUI() so that its function is clear: to update the entire flash card UI based on the current state of the app. (Don’t forget to update the two calls to updateElement() as well—they’re in viewDidLoad() and next().)

Now take a look at the showAnswer() method, which makes changes directly to the UI by setting the text of the answer label. Following the single-path pattern, updateFlashCardUI() should be making those changes. But if the user’s action is communicated to showAnswer(), how will updateFlashCardUI() know that the user wants to show the answer?

The solution is to keep track of the state your app is in. Think about your app as the user interacts with it over time. At any point, you should be able to ask, “What is my app doing now?” The answer will determine how you set up your UI. Your app has just two states: displaying an element for the user to guess, and showing the name of the element. Create a new enum named State at the top of the file under the Mode enum.

enum State {
    case question
    case answer
}

Then, in the view controller, make a new variable property to keep track of the state of your app.

var state: State = .question

In showAnswer(), replace the statement to update the label with one to update the state of the app. Since you’re no longer using that method to directly change the UI, you’ll add a call to updateFlashCardUI().

@IBAction func showAnswer() {
    state = .answer

    updateFlashCardUI()
}

You’ll also need to change next() to update the state of the app.

@IBAction func next(_ sender: Any) {
    currentElementIndex += 1
    if currentElementIndex == elementList.count {
        currentElementIndex = 0
    }

    state = .question

    updateFlashCardUI()
}

Finally, you can update updateFlashCardUI(). Remember, that method is supposed to be answering the question, “For the current state of the app, how should the UI look?” The state of the app determines what you display in the answer label. (The new comment above the method will be useful as you build more and more code.)

// Updates the app’s UI in flash card mode.
func updateFlashCardUI() {
    let elementName = elementList[currentElementIndex]
    let image = UIImage(named: elementName)
    imageView.image = image

    if state == .answer {
        answerLabel.text = elementName
    } else {
        answerLabel.text = "?"
    }
}

Checkpoint

When you build and run your app, it should behave exactly as it did before. Remember, this is the point of refactoring: You’re adapting your code to accommodate new goals and tasks without changing what it does.

Handling Modes

There’s a bit more you should do before you start building the quiz mode. First, create a new empty method named updateQuizUI():

// Updates the app’s UI in quiz mode.
func updateQuizUI() {

}

Then create one final UI update method: the one that will act as the single point of entry for all UI updates. In this method, you’ll use a switch statement to determine which of the two specific update methods to call. This important step will help keep your mode-specific UI code separated and easier to read.

// Updates the app’s UI based on its mode and state.
func updateUI() {
    switch mode {
    case .flashCard:
        updateFlashCardUI()
    case .quiz:
        updateQuizUI()
    }
}

Finally, you’ll need to modify the methods that called updateFlashCardUI by changing them to call updateUI instead. Make that adjustment in these three methods:

  • viewDidLoad()
  • next()
  • showAnswer()

The only method that should ever call updateFlashCardUI (and updateQuizUI) should be updateUI. All other methods will rely on updateUI when they make changes to the interface.

To verify that you made the correct changes, use Command-F to search for the string “updateFlashCardUI.” There should be just two results: one declaration and one call.

Checkpoint

Build and run your code again to confirm that this additional refactoring hasn’t changed the app’s behavior.

Does this seem like a lot of effort with no real result? As you progress through the rest of the lesson and as your code becomes more complex, you’ll soon see the importance of your work.

Part 5 - Getting Keyboard Input

The most significant update you made to your storyboard was the text field. The text field and the keyboard it displays are common features of many iOS apps. In this part of the lesson, you’ll write the code that handles text input.

Delegation

You’ve already learned multiple ways to handle user interactions in iOS. An IBAction receives a specific event from an interface element, such as a button. A callback function can be supplied to a user interface object, like a Shape in the game app. There’s another way that iOS commonly delivers user input: via delegation.

You learned a little about delegation in the ChatBot app. Here, you’ll be using it in the context of UI controls, in which delegation is a little like IBAction—linking a piece of your UI to your code.

Declaration

Your view controller will act as the delegate of the text field. Instead of tying the text field directly to a method, you’ll tie it to the whole view controller. You can then implement certain standard methods that the text field knows how to call. You’ll start by conforming to the UITextFieldDelegate protocol. You do that by modifying the declaration of your UIViewController class.

class ViewController: UIViewController, UITextFieldDelegate {

To complete this lesson, it’s not necessary to understand how this kind of delegation works or what a protocol is. But if you want to learn more, you can Option-click UITextFieldDelegate to look at its documentation. Even without knowing what a protocol is, you’ll understand something about how a text field interacts with its delegate.

Storyboard

Now that you’ve modified your view controller, you can set up the text field’s delegate. In the storyboard scene, Control-click (or right-click) the text field, and find the delegate item in the small panel that appears. (It should be near the top, in the Outlets section.) To make the delegate connection, drag a blue line from the small circle to the right of delegate to the View Controller in the Document Outline.

Connecting the delegate outlet of a text field to the view controller in Interface Builder

Code

Go back to ViewController to implement your code. There are multiple callbacks available to text field delegates. You need to implement just one of them—which will be called when the user taps the Return button on the iOS keyboard.

Before you write the method itself, you’ll need to add some extra state to your app:

  1. Whether the user entered the right answer
  2. How many answers the user got right

Add two new variable properties to your view controller for this state.

// Quiz-specific state
var answerIsCorrect = false
var correctAnswerCount = 0

Then implement the text field callback. It returns a Bool so that iOS knows whether you’d like to use its default behavior after you’ve run your code.

// Runs after the user hits the Return key on the keyboard
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    // Get the text from the text field
    let textFieldContents = textField.text!

    // Determine whether the user answered correctly and update appropriate quiz
    // state
    if textFieldContents.lowercased() == elementList[currentElementIndex].lowercased() {
        answerIsCorrect = true
        correctAnswerCount += 1
    } else {
        answerIsCorrect = false
    }

    // The app should now display the answer to the user
    state = .answer

    updateUI()

    return true
}

Checkpoint

Build and run the app. After you type a response in the text field and press (or tap) Return, the answer label should display the name of the element.

UI Updates

How will the user know whether they got the right answer? It’s time to write some UI update code for quiz mode. Add the following code to updateQuizUI():

// Updates the app’s UI in quiz mode.
func updateQuizUI() {
    switch state {
    case .question:
        answerLabel.text = ""
    case .answer:
        if answerIsCorrect {
            answerLabel.text = "Correct!"
        } else {
            answerLabel.text = "❌"
        }
    }
}

Breaking down this code, you’ll see that it first uses the state property to select a code path:

switch state {

If the quiz is currently asking a question, the answer label should be blank:

answerLabel.text = ""

If the quiz is displaying the answer, the logic is a little more complex. If the answer is correct, the label should tell the user they’re right; if the answer is wrong, the label will display a red “X” emoji.

if answerIsCorrect {
    answerLabel.text = "Correct!"
} else {
    answerLabel.text = "❌"
}

Checkpoint

Build and run your app. Even though you expect to see feedback when you enter an answer in the text field and tap Return, the answer label still displays the name of the element. What’s wrong?

To find out, set a breakpoint where you’d expect to see your answer label change—on the first line of updateQuizUI(). To add a breakpoint, click the line number on the left side of the editor area, or position your text insertion point on the line and then select Debug > Breakpoints > Add Breakpoint at Current Line (Command-). A blue flag will appear behind the line number, pointing to the line of code on the right.

Now build and run again. You’ll notice that after you enter an answer, Xcode doesn’t stop at the breakpoint—which means that updateQuizUI() is never called. There’s only one place that calls updateQuizUI(), and that’s updateUI()—and it relies on the mode being set to .quiz. As it turns out, you haven’t changed the mode of your app; it’s still running in flash card mode. (Even if you set the segmented control to Quiz.)

Before you learn how to switch modes, you’ll still want to verify that your code is properly tabulating quiz results. For debugging purposes, you can add print statements to textFieldShouldReturn:

if answerIsCorrect {
    print("Correct!")
} else {
    print("❌")
}

Part 6 - Switching Modes

In this part, you’ll add the capability to switch between quiz mode and flash card mode.

Segmented Control Action

First, you’ll make a new action that changes the app’s mode when the user interacts with the segmented control.

@IBAction func switchModes(_ sender: Any) {
    if modeSelector.selectedSegmentIndex == 0 {
        mode = .flashCard
    } else {
        mode = .quiz
    }
}

You could add a call to updateUI() at the end of this method, but there’s a cool trick in Swift that makes your code a tiny bit more elegant. Add the following to your declaration of the mode property:

var mode: Mode = .flashCard {
    didSet {
        updateUI()
    }
}

If you recall computed properties, you’ll see a similar pattern: braces after a property declaration. But mode isn’t a computed property. Instead, you’ve added a property observer. Property observers are specialized code that runs every time a property changes. In this case, each time the value of mode is updated, the code in the didSet block will run.

Checkpoint

Build and run the app, then use the segmented control to switch to Quiz mode. This time, Xcode will stop at your breakpoint, highlighting the line in green and switching to the Debug navigator.

If you examine the navigator, you’ll see that ViewController.updateQuizUI is highlighted at the top under Thread 1. Below it is ViewController.updateUI, which called it. And below that, ViewController.mode.didset. In fact, you can trace down to see the call stack of this point in your code’s execution.

Disable your breakpoint by clicking it—it’ll turn a more muted shade of blue—or delete it by dragging it to the right away from the line numbers. To continue executing your code, choose Debug > Continue, or click the Continue button between the console and the editor area. 1

Xcode window stopped at a breakpoint in code

So you know for sure that the updateQuizUI() method is now being called. Do you see the tiny change in behavior in your app as a result of your code changes? The question mark in the answer label should appear and disappear when you switch between modes. That’s an indicator that the correct UI update code is running. If you try answering a question in quiz mode, you should now see what you expected in the answer label.

Of course, you’ll also notice that the image no longer updates in quiz mode. This shouldn’t surprise you, since the image update code is in updateFlashCardUI.

Refactoring—Quiz Image Updates

The code to set the image is identical in both quiz mode and flash card mode. So why not copy it from updateFlashCardUI and paste it into updateQuizUI? By now you’ve learned that, whenever possible, it’s better to avoid repeating code. A better solution is to lift the code from updateFlashCardUI up to the method that calls it: updateUI. That way, the image will be updated before any mode-specific UI code runs.

Cut these lines of code from updateFlashCardUI and paste them at the top of updateUI, adding a helpful comment.

// Shared code: updating the image
let elementName = elementList[currentElementIndex]
let image = UIImage(named: elementName)
imageView.image = image

You’ll see a compiler error, since you removed elementName from updateFlashCardUI. That’s easy to fix by adding it as a parameter. Update the declarations of updateFlashCardUI and updateQuizUI:

func updateFlashCardUI(elementName: String)
func updateQuizUI(elementName: String)

Then change updateUI so that it calls those methods properly:

// Mode-specific UI updates are split into two
// methods for readability.
switch mode {
case .flashCard:
    updateFlashCardUI(elementName: elementName)
case .quiz:
    updateQuizUI(elementName: elementName)
}

Checkpoint

Build and run the app. Quiz mode should properly update the image when you tap Next Element.

Keyboard Dismissal

Now that you can switch modes, you’ll notice that the keyboard remains visible when you switch from quiz mode to flash card mode (when it has no purpose). How can you dismiss the keyboard? You can handle these remaining issues with a few extra lines of code.

UITextField has a method that causes the keyboard to go away—it’s called resignFirstResponder. Where should this method be called? Since this qualifies as user interface code, you’ll put it in your UI update methods. Change updateFlashCardUI by adding the following two lines to hide the text field and dismiss the keyboard.

textField.isHidden = true
textField.resignFirstResponder()

While you’re there, you might add some comments to keep track of which parts of the UI you’re touching. The final method should look like this:

// Updates the app’s UI in flash card mode.
func updateFlashCardUI(elementName: String) {
    // Text field and keyboard
    textField.isHidden = true
    textField.resignFirstResponder()

    // Answer label
    if state == .answer {
        answerLabel.text = elementName
    } else {
        answerLabel.text = "?"
    }
}

You’ll do similar work in updateQuizUI, using the becomeFirstResponder method, to make sure that the keyboard is showing. But it’s a bit more nuanced. How would you describe the text field in quiz mode?

  • When displaying a question (a new element), the field should be empty and it should show the keyboard.
  • When displaying the answer, the keyboard should be hidden.

And regardless whether the app is displaying the question or the answer, or whether the keyboard is showing, the text field itself should always be visible in quiz mode. Modify updateQuizUI to add this new code, making comments as you did for updateFlashCardUI.

// Updates the app’s UI in quiz mode.
func updateQuizUI(elementName: String) {
    // Text field and keyboard
    textField.isHidden = false
    switch state {
    case .question:
        textField.text = ""
        textField.becomeFirstResponder()
    case .answer:
        textField.resignFirstResponder()
    }

    // Answer label
    switch state {
    case .question:
        answerLabel.text = ""
    case .answer:
        if answerIsCorrect {
            answerLabel.text = "Correct!"
        } else {
            answerLabel.text = "❌"
        }
    }
}

You might think it’s redundant to do things like hide the text field every time your app’s state changes. If it’s already hidden once in flash card mode, why hide it again every time updateUI is called? There are two reasons:

  • It’s not harmful to call some UI code like .isHidden and resignFirstResponder() more than once, even if you’re not changing the state of those UI elements.
  • It makes your code simpler to understand.

The second point is more important. Inside updateUI is a complete description of your app’s interface. So you can focus on all of your interface code (the what) in one consolidated place, without worrying about exactly where and when you should be doing something. In this case, there’s no problem hiding the text field—even if you hide it when it’s already hidden.

Checkpoint

Build and run the app. When you switch to quiz mode, the text field and keyboard should appear. When you answer a question, the keyboard should disappear. As you move to the next question, the keyboard should reappear and the text field should be cleared. When you switch from quiz mode back to flash card mode, the keyboard should disappear, along with the text field.

Good job! You’re well on your way to a great app. But there’s still more work to do!

Part 7 - Scoring the Quiz

Your users will want to know their score at the end of the quiz. Since you’re already tracking the number of correct answers, you just need to detect when the quiz is over and provide feedback on the user’s performance.

Displaying a score doesn’t neatly fit into the current two-state question/answer design. You’ll need to add a special new state just to display the user’s score.

New State

Start by adding a case to the State enum.

case score

You’ll notice that two compiler errors, reading “Switch must be exhaustive,” show up in updateQuizUI. Recall how switch statements work—Swift is helping you avoid bugs that can happen if you ignore possible values for a variable. You’ll tackle each switch statement one at a time.

Look at the first switch that updates the text field and keyboard. When displaying the score, you should hide the keyboard and hide the text field. Add that code as a new case:

case .score:
    textField.isHidden = true
    textField.resignFirstResponder()

Now look at the switch statement for the answer label. When you’re displaying the user’s score, the answer label should be empty. Add a new case for the empty label, and insert a print statement to print the score to the console for temporary debugging purposes:

case .score:
    answerLabel.text = ""
    print("Your score is \(correctAnswerCount) out of \(elementList.count).")

Detecting the End of the Quiz

Now you’ll need to determine when to set the app’s state to score. You already have code in the next() method that has to account for reaching the end of the list of elements. That’s a logical place to add your code. Update the if statement in the next() method as follows:

if currentElementIndex >= elementList.count {
    currentElementIndex = 0
    if mode == .quiz {
        state = .score
        updateUI()
        return
    }
}

Notice that you return midway through the method after you set the state to score. Your code can’t continue to the end of the method, where you set the state to question. You could rewrite the method to avoid an extra return statement, but the logic would be harder to read. It’s generally better to have just one return statement in a function or method, but returning early occasionally results in better code.

Checkpoint

Build and run your app. You’ll see that, after all four questions, your quiz is scored and the results are printed to the console.

Next, you’ll write the code to display an alert to the user.

Alert Controllers

A standard iOS alert appears over the rest of your app, forcing you to focus on it and dismiss it before resuming your activities. (This is called a modal interface.) Displaying an alert doesn’t take too much code, and it’s a nice skill to have.

Here’s the code you’ll need:

// Shows an iOS alert with the user’s quiz score.
func displayScoreAlert() {
    let alert = UIAlertController(title: "Quiz Score", message: "Your score is \(correctAnswerCount) out of \(elementList.count).", preferredStyle: .alert)

    let dismissAction = UIAlertAction(title: "OK", style: .default, handler: scoreAlertDismissed(_:))
    alert.addAction(dismissAction)

    present(alert, animated: true, completion: nil)
}

func scoreAlertDismissed(_ action: UIAlertAction) {
    mode = .flashCard
}

Take a look at the code, line by line:

First you created a new UIAlertController and set its title, message, and preferred style.

let alert = UIAlertController(title: "Quiz Score", message: "Your score is \(correctAnswerCount) out of \(elementList.count).", preferredStyle: .alert)

Then you created a new alert action, which describes the button that will go at the bottom of the alert. Its title is easy to understand, and its style can be one of several. (To learn more, look up “UIAlertAction.Style” in the documentation.) The third parameter is more interesting—it’s a callback. You’ve supplied your own function to run after the user taps the OK button. It may look a little weird to pass a function into another function, but it’s the same concept as setting a function as the value of a callback property, just as you did for shapes in your game app.

let dismissAction = UIAlertAction(title: "OK", style: .default, handler: scoreAlertDismissed(_:))

You added that action to the alert controller:

alert.addAction(dismissAction)

Then you presented the alert to the user with the present(_:animated:completion:) method, which is part of UIViewController. (The nil argument to the third parameter won’t look familiar; you can ignore it for this project. The concept of nil is part of the next level of Swift—valuable to explore if you have the time and curiosity.)

present(alert, animated: true, completion: nil)

The method to handle the OK button simply sets the app back to flash card mode.

Finally, to display your alert, you’ll need to add one more bit of code to the bottom of your updateQuizUI(elementName:) method.

// Score display
if state == .score {
    displayScoreAlert()
}

Finally, you can now delete your call to print the score to the console in textFieldShouldReturn(_:), since you no longer need it for debugging.

Checkpoint

Build and run your app. Your alert displays correctly, but there are some new wrinkles. After the alert is dismissed, you’ll notice that the segmented control doesn’t update, even though you return to flash card mode when the alert is dismissed.

Segmented Control Updates

The segmented control wasn’t in your updateUI code path because your app mode only changed when the user interacted with it. But now that you’re automatically switching modes, the segmented control has to be managed. Add the following code to updateFlashCardUI(elementName:):

// Segmented control
modeSelector.selectedSegmentIndex = 0

And add this code to updateQuizUI(elementName:):

// Segmented control
modeSelector.selectedSegmentIndex = 1

Checkpoint

Build and run your app. The segmented control should update properly after you finish a quiz.

If you use the segmented control to switch immediately back to quiz mode without touching any buttons, the alert will appear again. That’s because your app’s state was score when you switched back to flash card mode, and the state wasn’t updated before you went back to quiz mode.

You could set the state to question in the scoreAlertDismissed(_:) method, but that won’t solve all your problems. For example, try stopping and re-running the app and displaying the answer for the first element in flash card mode, and then switching to quiz mode. You’ll see the red “X,” without being given the opportunity to answer the question.

What happened? When you entered quiz mode, the app’s state was answer. Learn how to reset the app each time you switch modes.

Part 8 - Setting Up Modes

You’ve reached a point where the basic app functions are in place and there are just a few bugs to fix. The bugs, such as the two above, are tricky to put into words because they rely on a certain sequence of steps before you see them. You can follow a common practice for describing UI bugs that makes it clear—to anyone—exactly how to reproduce them.

Describing UI Bugs

When you describe a UI bug, you’ll need to list the sequence of steps a user will take before the bug occurs—the steps to reproduce the bug. You’ll also want to describe the bug concisely and clearly with a title, and document explicitly what the behavior should be, along with what actually happened. Here are descriptions of both of the bugs that occurred above.

Quiz score alert displays when starting quiz mode

  1. Launch the app.
  2. Use the segmented control to switch to quiz mode.
  3. Answer all four questions to reach the end of the quiz.
  4. Tap the Next Element button to show the score alert.
  5. Tap the OK button to dismiss the alert.
  • The app switches back to flash card mode.
  1. Use the segmented control to switch to quiz mode.

Expected Behavior

The quiz begins, showing the keyboard and activating the text field, with a blank answer label.

Observed Behavior

The score alert is displayed with a result of 0 out of 4.

Starting a new quiz displays an incorrect answer

  1. Launch the app.
  2. Tap the Show Answer button.
- The app shows “Carbon” below the image.
  1. Use the segmented control to switch to quiz mode.

Expected Behavior

The quiz begins, showing the keyboard and activating the text field, with a blank answer label.

Observed Behavior

The quiz begins by showing a red “X” below the image. The keyboard is not visible and the text field is not active.

Setup Methods

To fix both bugs, you’ll add code to set up the state of a new flash card session or a new quiz. Start by adding two new empty setup methods, one for each mode.

// Sets up a new flash card session.
func setupFlashCards() {

}

// Sets up a new quiz.
func setupQuiz() {

}

The purpose of each function is to start a new session in a clean state, erasing or resetting anything that’s left over when the user switched modes.

Calling Setup Methods

The most natural place to call these methods is the point at which the mode is being changed. Update the code in the didSet block for the mode property as follows:

switch mode {
case .flashCard:
    setupFlashCards()
case .quiz:
    setupQuiz()
}

updateUI()

Flash Card Setup

The flash card mode relies on two key properties: currentElementIndex should be set to zero when the flash card session starts; and the state of the app should be question. Add that code to setupFlashCards():

state = .question
currentElementIndex = 0

Quiz Setup

A quiz requires a bit more code. In addition to setting the state and current element index, you’ll have to reset the additional quiz-specific properties answerIsCorrect and correctAnswerCount. Add the following code to setupQuiz().

state = .question
currentElementIndex = 0
answerIsCorrect = false
correctAnswerCount = 0

Checkpoint

Build and run the app. By following the listed steps in the bug descriptions, you can verify that issues are fixed.

Part 9 - Cleanup and Polishing

While you’re in bug-fixing mode, you can clean up other issues with the app. Take a minute to see if you can find the problems yourself, then read below. (In this part of the lesson, the bugs aren’t written up in detail, but you can try your hand at bug descriptions as you work on them.)

Show Answer Button

You can still tap the Show Answer button in quiz mode. You’re automatically displaying the answer in a quiz when the user finishes entering text, so there’s no use for that button. And since it has the effect of displaying a red “X” if it’s tapped, the button may be confusing to the user. Use the code below to manage the its visibility.

First, you’ll need to create a new outlet for the Show Answer button and connect it to the button in your storyboard. Do that now.

@IBOutlet weak var showAnswerButton: UIButton!

Add the following code to updateFlashCardUI():

// Buttons
showAnswerButton.isHidden = false

And add this code to updateQuizUI():

// Buttons
showAnswerButton.isHidden = true

Checkpoint

Build and run the app to verify that the Show Answer button doesn’t show up in quiz mode.

Next Element Button

While you’re thinking about buttons, examine the Next Element button. There are a few things that could be improved.

  1. In quiz mode, the button shouldn’t be enabled until the user supplies an answer. Right now, they can skip a question by tapping it (and they’re scored for an incorrect answer).
  2. In quiz mode, the button should have better titles: “Next Question” during the quiz, and “Show Score” at the end.

Start by adding an outlet for the Next Element button and connecting it in the storyboard.

@IBOutlet weak var nextButton: UIButton!

Update updateFlashCardUI() in the Buttons section, adding Next button code below the existing Show Answer button code:

// Buttons
showAnswerButton.isHidden = false
nextButton.isEnabled = true
nextButton.setTitle("Next Element", for: .normal)

You’ll notice a new method to set the title of a button. You might have expected a simple title property, but iOS lets you set different button titles for different control states. (If you want to know more, look up UIControl.State in the documentation.)

Update updateQuizUI() in the Buttons section, adding Next button code below the existing Show Answer button code:

// Buttons
showAnswerButton.isHidden = true
if currentElementIndex == elementList.count - 1 {
    nextButton.setTitle("Show Score", for: .normal)
} else {
    nextButton.setTitle("Next Question", for: .normal)
}
switch state {
case .question:
    nextButton.isEnabled = false
case .answer:
    nextButton.isEnabled = true
case .score:
    nextButton.isEnabled = false
}

That’s some complex code! Here’s a breakdown of what it’s doing. The if statement sets the button’s title based on the user’s position in the quiz. If they’re on the last question, the button’s title is set to "Show Score." The switch statement enables the button only in the answer state.

Checkpoint

Build and run the app. You’ll notice that the Next button isn’t wide enough to accommodate the "Next Question" title, so you’ll need to widen it in your storyboard.

Text Input

Next, turn your focus to the text field. You may have noticed that it can be reentered while the answer is being displayed, which can lead to scores like 6 out of 4 if the user enters the correct answer multiple times.

You can fix these issues by disabling the text field when it’s not needed. Add two lines to your text field management code in updateQuizUI(), each managing its isEnabled state.

// Text field and keyboard
textField.isHidden = false
switch state {
case .question:
    textField.isEnabled = true
    textField.text = ""
    textField.becomeFirstResponder()
case .answer:
    textField.isEnabled = false
    textField.resignFirstResponder()
case .score:
    textField.isHidden = true
    textField.resignFirstResponder()
}

Note that you don’t need to set isEnabled in the score case, since the field will be hidden.

Checkpoint

Build and run the app to verify that the text field is only enabled when the user is viewing a question.

Answer Display

Right now, the user doesn’t get good feedback when they enter the wrong answer. Make the following change in updateQuizUI() to display the correct element name when their answer is incorrect. (You’ll find this code in the Answer Label section inside the .answer case of the switch statement.)

answerLabel.text = "❌\nCorrect Answer: " + elementName

Build and run the app. You’ll notice that you don’t see all of the text. Why not?

The label is currently configured to display just one line of text, and it’s not tall enough for multiple lines. Fix that in your storyboard by doing two things:

  1. Resize the label to make it tall enough to handle two lines of text. Also make it wider to accommodate the full width of the answer text. You’ll probably want to extend it to the full width of the screen, minus the side margins that appear as you resize.
  2. In the Attributes inspector, change Lines to 0.
  3. Set Autoshrink to Minimum Font Size, and set its value to 12.

It might seem odd to set the number of lines to zero, but that’s the way to tell iOS that you can put any number of lines of text into the label. The third step ensures that, even if you have really long element names or other answers with lots of text, iOS will try to fit them by shrinking the font.

Checkpoint

Build and run the app, and verify that your new answer display works.

Keyboard

The final issue to polish in your app relates to the keyboard. You may have noticed that it displays autocorrect suggestions, which could help your users cheat.

In your storyboard, select the text field. Then make the following changes in the Attributes inspector under the Text Input Traits section.

  1. Set Correction to No.
  2. Set Spell Checking to No.

While you’re there, you can change the look of the Return key so it looks a bit more like the user is submitting an answer:

  1. Set Return Key to Done.

Checkpoint

Build and run the app. Your keyboard should no longer give any hints to the user, and the Return key should be replaced by a blue key labeled “Done.”

Wrap-Up

Notice how easy it was to make all these changes to your UI, one by one. Your code was localized to the UI update methods, since that’s where you describe how the app looks. Writing the code was just a matter of thinking in terms of the way your app should behave when it’s in a certain state.

Part 10 - Completing the App

Your quiz app has turned out quite nicely! It has enough features that a student could use it as a real study aid, and the interface is clean and simple. As a developer, you could easily use this project to create a quiz on any topic you choose, with any number of questions.

Random Order

There’s one more feature that will add a crowning touch to the app. If it’s going to be a really useful study aid, the app should randomize the quiz questions so that the user has to think on their feet.

To enable this functionality, you’ll be changing the way you handle your data model. Currently, you use the constant elementList property to manage the app’s data. But with this approach, there’s no way to change the array while the app is running. How can you generate a new, random array of elements every time a user begins the quiz?

By making elementList a variable property instead of a constant, you can randomize the list each time the user switches modes. Start by changing the property from a constant to a variable:

var elementList = ["Carbon", "Gold", "Chlorine", "Sodium"]

Next, in setupQuiz(), add the following line to randomize the array.

elementList = elementList.shuffled()

Checkpoint

Build and run the app. When you switch to quiz mode, you’ll see that the questions don’t follow the same order each time.

Predictable Order

Of course, you’ve also just randomized the flash cards. Perhaps it’s better to keep those in a standard order so that the user can study them more easily. Make the following changes to fix that.

Add a new constant property named fixedElementList that will always have the same order. To avoid duplicate data, you can initialize the elementList property to be empty. (That may not seem necessary, but remember that a real-life app will probably have a longer list of more complex data.)

let fixedElementList = ["Carbon", "Gold", "Chlorine", "Sodium"]
var elementList: [String] = []

Now, update setupFlashCards to set up the element list:

elementList = fixedElementList

And modify setupQuiz:

elementList = fixedElementList.shuffled()

If you build and run the app now, it will crash with an “Index out of range” error. Why is that?

  • When your app first launches, viewDidLoad() is called, which calls updateUI(). (You can verify this in the Debug navigator.)
  • But neither setupQuiz() nor setupFlashCards() has been called. (Again, if you want, you can verify this by setting a breakpoint in each of those methods and re-running the app.)
  • Which means that the elementList property is still empty;
  • And you’re trying to get a string from elementList to set the image view!

The solution is to delete the updateUI call in viewDidLoad(), and to set the mode property instead. That way your setup code is guaranteed to be called when the app launches.

override func viewDidLoad() {
    super.viewDidLoad()
    mode = .flashCard
}

Checkpoint

Build and run the app. The quiz questions should be randomized, while the flash cards display in the same order every time.

Other Possibilities

By now, you’re familiar enough with the app that you may have had your own ideas about how it should work or how it could be improved. You’re experienced enough that you might even want to challenge yourself to take the app to the next level.

Here are a few ideas you might want to try out:

Shuffling options

There could be a user control for shuffling elements, so that users can control whether the quiz or the flash cards are predictable or randomized. Would you use a new button for this? Or maybe another segmented control? How would you update your code?

Tracking common mistakes

As the user studies over time, you could keep track of the most commonly missed questions, and then tailor the flash cards to emphasize those elements. How would you calculate and store that information? And how would you use the data to alter the flash card sessions?

Eliminating learned items

Some elements are easier to learn than others. You could give users a way to remove an element from the flash card stack once they’ve learned it. Would you add a button for that? How would it affect your data model?

Multiple-choice mode

Instead of the free-response style, quizzes could offer a set of multiple-choice answers. Would you use buttons? How would you store the data? How would you change your code?

These are just a few ways to improve your app. If you’re enjoying this project, take these ideas or others, and see where you can go.

Designing a Model

For the ElementQuiz app, a simple data model of an array of strings is sufficient to enable the app to work as designed. It works for the flash card interface because the string value is both the answer that’s displayed and the name of the image that’s displayed.

But if you wanted to build a more complex app, you might create a ChemicalElement struct to contain more information:

struct ChemicalElement {
    let symbol: String
    let name: String
    let atomicWeight: Int
    let imageName: String
}

The data model above could support things like quizzing on more than the element name. It could also allow the name of the image to be different from the name of the element that’s displayed in the user interface. You could make quizzes that have longer answers, even phrases. This data model would also be one way to support multiple-choice answers.

When building apps, developers often need to make decisions such as these. In the ElementQuiz app, there’s a trade-off between the simplicity of a single string and the structure and functionality of a custom type. When the demands of the app change, you might need to change the data model as part of your refactoring.

Looking Back

If you examine ViewController carefully, you can still see some of your earliest code peeking through. Check out lines like:

var currentElementIndex = 0

and

let image = UIImage(named: elementName)

So your ElementQuiz app is not completely different from your starting point. You can think of your development process in this project as evolving the app from a simple, single-celled organism to a breathing, complex animal with a nervous system. Feels good, doesn’t it?

Making an app is a process of constant creation: something from nothing—or rather, from the ideas, goals, and emotions that are inside your head. Creating apps is not quite like anything else in the world, and the satisfaction you get from building an app and sending it into the world is just as special.

Enjoy this feeling! It’s a reward for all the hard work, thinking, debugging, and persistence you applied. App developers get other rewards, too, but this one is all your own—nobody can take it from you. Delight in your successes, which can help you through the tough times when nothing seems to work.

Reflection Questions

What part of making your app was challenging in a way you didn’t expect?

What part of making your app was easier than you expected?

Who do you think would most enjoy using the app you built? (Remember: Who’s your audience?)

What new app features do you think your audience would request first?

Which do you think would be the simplest to build?

Think about your favorite apps, and come up with an enum for each that describes the states that the app has.

Summary

In this lesson, you created a sophisticated app. You followed several important app development patterns that helped you along the way.

Model-View-Controller

You modeled your app’s state with a handful of properties. Even though you didn’t have a separate file for your models, you used them to fully describe what the app was doing at any given time. Then you wrote controller logic to manipulate that state and modify your interface to match, and respond to UI events by updating the model.

Incremental development

By developing step by step, you were able to work on the app in manageable chunks. Rather than trying to build the whole thing all at once, you had something that would run at the end of each step. You’ve followed this practice before, but this project has highlighted just how important it is when you’re building a complex app.

Single-path UI updates

Your code was organized so that you could reason about its behavior. You could use your state model to set up the UI all in one place, avoiding the problem of hunting down UI bugs all over your code.

Refactoring

You periodically rearranged your code to keep it organized. Without refactoring, you would have had to add fixes and functionality by bolting on code in awkward ways. And you could never hope to achieve code that’s easy to read. This is also a skill you’ve practiced in past projects, but it was especially important in this one.

Describing bugs

By describing in detail exactly how to reproduce an error in the app, you forced yourself to think clearly about how the bug might have happened, which gave you a clue about how to solve it. And if you were working on a team, you would have enabled your teammates to understand the bugs and to help to fix them.

Report error