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
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.
Part 1 - Creating the Interface
To start, you’ll build the flash card interface in your storyboard.
-
In Xcode, create a new project.
-
Use the iOS App template.
-
Name the project “ElementQuiz” and save it to the desktop.
-
Select the Main storyboard in the project navigator.
-
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.)

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:
-
Select the image view.
-
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:
- Select the label.
- In the Attributes inspector, in the Font field, click the “T” button to display the Font popup menu. 1
- Change the Style to Bold and the Size to 24, then click Done.
- Set the Alignment to center.
- Change the string in the label to “Answer 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:
- Set the title of the button on the left to “Show Answer.”
- 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.

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:
-
Select Assets from the project navigator (see image left).
-
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.
- Create an outlet named
imageView for the image view.
- 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
- Create an action named
showAnswer() connected to the Show Answer button.
- 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:

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.
- Drag a UISegmentedControl from the Object library onto the canvas.
- Center it horizontally at the top of the screen.
- 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.
- Make room for the text field by moving the buttons lower in the scene.
- Drag a text field from the Object library onto the canvas.
- Center it below the Answer Label and above the buttons.
- 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.
- Create an outlet named
modeSelector for the segmented control.
- 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.
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.)
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():
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.
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.

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:
- Whether the user entered the right answer
- How many answers the user got right
Add two new variable properties to your view controller for this 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.
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
let textFieldContents = textField.text!
if textFieldContents.lowercased() == elementList[currentElementIndex].lowercased() {
answerIsCorrect = true
correctAnswerCount += 1
} else {
answerIsCorrect = false
}
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():
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
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.
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:
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:
func updateFlashCardUI(elementName: String) {
textField.isHidden = true
textField.resignFirstResponder()
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.
func updateQuizUI(elementName: String) {
textField.isHidden = false
switch state {
case .question:
textField.text = ""
textField.becomeFirstResponder()
case .answer:
textField.resignFirstResponder()
}
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:
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.
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:):
modeSelector.selectedSegmentIndex = 0
And add this code to updateQuizUI(elementName:):
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
- Launch the app.
- Use the segmented control to switch to quiz mode.
- Answer all four questions to reach the end of the quiz.
- Tap the Next Element button to show the score alert.
- Tap the OK button to dismiss the alert.
- The app switches back to flash card mode.
- 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
- Launch the app.
- Tap the Show Answer button.
- The app shows “Carbon” below the image.
- 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.
func setupFlashCards() {
}
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():
showAnswerButton.isHidden = false
And add this code to updateQuizUI():
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.
- 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).
- 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:
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:
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.
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:
- 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.
- In the Attributes inspector, change Lines to 0.
- 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.
- Set Correction to No.
- 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:
- 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.