3.
Building User Interfaces
Written by Joey deVilla
Now that you’ve accomplished the first task of putting a button on the screen and making it show an alert, you’ll simply go down the task list and tick off the other items.
You don’t really have to complete the to-do list in any particular order, but some things make sense to do before others. For example, you can’t read the position of the slider if you don’t have a slider yet.
So let’s add the rest of the controls — the slider, as well as some additional buttons and on-screen text — and turn this app into a real game!
When you’ve finished this chapter, the app will look like this:
Hey, wait a minute… that doesn’t look nearly as pretty as the game I promised you! The difference is that these are the standard controls. This is what they look like straight out of the box.
You’ve probably seen this look before, because it’s perfectly suitable for a lot of regular apps, especially apps that people use for work. However, the default look is a little boring for a game. That’s why you’ll put some special sauce on top later, to spiff things up.
In this chapter, you’ll cover the following:
- Portrait vs. landscape: Switch your app to landscape mode.
- Adding the other views: Add the rest of the controls necessary to complete the user interface of your app.
- Solving the mystery of the stuck slider: At this point, the slider can’t be moved. Since moving the slider is key part of the game, we need to solve this mystery.
- Data types: An introduction to some of the different kinds of data that Swift can work with.
- Making the slider less annoyingly precise: We don’t need the slider to report its position with six-decimal precision, but to the nearest whole number.
- Key points: A quick review of what you learned in this chapter.
Portrait vs. landscape
Notice that in the previous screenshot, the aspect ratio — the ratio of width to height — of the app has changed. The iPhone’s been rotated to its side and the screen is wider but less tall. This is called landscape orientation.
Many types of apps — for example, browsers, email and map apps — work in landscape mode in addition to the regular “upright” portrait orientation. Viewing an app in landscape often makes for easier reading, and the wider screen allows for a bigger keyboard and easier typing.
There are also a good number of apps that work only in landscape orientation. Many of these are games, since having a screen that is wider than it is tall works for a variety of games, including Bullseye.
Right now, the app works in both portrait and landscape orientations. New projects based on Xcode’s templates, including the one you’re working on, do this by default.
➤ Build and run the app. If you’ve been following the steps in this book up to this point, it should look like this in the simulator:
The simulator defaults to portrait orientation, right side up, since this is the usual way people hold their phones. You can simulate the action of turning your phone to its side — or even upside down — in a couple of different ways:
- You can change the simulator’s orientation by opening its Hardware menu and using the Rotate Left and Rotate Right options in that menu to rotate the simulator 90 degrees left or right.
- You can also use keyboard shortcuts. Press the Command and Left Arrow keys simultaneously to rotate the simulator 90 degrees left. Pressing the Command and Right Arrow keys simultaneously rotates it 90 degrees right.
- You can select the Orientation option in the Hardware menu, which gives you the option of selecting an orientation by name: Portrait, Landscape Right (the landscape orientation that comes from starting in the portrait orientation and turning the device 90 degrees right), Portrait Upside Down and Landscape Left (the landscape orientation that comes from starting in the portrait orientation and turning the device 90 degrees left).
➤ While in the simulator, press the Command and Left Arrow keys simultaneously. You should see this:
One of the advantages that SwiftUI has over the old way of building iOS user interfaces — UIKit — is that it adjusts automatically to changes in orientation without requiring much work from the programmer. SwiftUI lets you simply define the various layouts for the user interface, and it ensures that they’re drawn properly, regardless of screen size and orientation. Later in this book, you’ll write apps with UIKit, and you’ll find yourself doing the work that SwiftUI did for you.
Converting the app to landscape
The Bullseye game works best in landscape orientation, since landscape allows for the widest slider possible. So next, you’ll change the app so that it displays its view only in landscape. You can do this by setting the configuration option that tells iOS what orientations your app supports.
➤ In the Navigator section of Xcode, which is the leftmost column in the Xcode window, make sure that you’ve selected the Project navigator, whose icon looks like a file folder. Click the blue Bullseye project icon at the top of the Project navigator’s list. The Editor and Canvas will disappear and panes that let you change your project’s configuration will replace them.
➤ Make sure that you’ve selected the General tab:
In the Deployment Info section, there are a number of checkboxes in an area marked Device Orientation.
➤ Make sure that you’ve unchecked Portrait and Upside Down, and that you’ve checked Landscape Left and Landscape Right.
➤ Build and run the app. You’ll see that no matter which way you rotate the simulator, the app always stays in landscape orientation:
Adding the other views
You’re going to see the word “view” a lot in this book, so take a moment to quickly go over what “view” means. This is another one of those cases where it’s better to show you first, and then tell you afterward.
Once again, here’s what the Bullseye screen will look like at the end of the chapter, this time with all the apparent views highlighted and labeled:
Some of the views are invisible; you’ll learn more about them soon.
A view is anything that gets drawn on the screen. In the screenshot above, it seems that everything is a view: The text items, the buttons and the slider are all views. In fact, every user interface control is a view.
Some views can act as containers for other views. The biggest view in the screenshot is one of these: It’s the view representing the screen, and it contains all the other views on the screen: The text items, the buttons and the slider.
Different types of views
There are different types of views. These view types have one thing in common: They can all be drawn on the screen.
What makes each type different is a combination of what they look like and what they do. So far, you’ve worked with a few of them:
-
Text: A view that displays one or more lines of read-only text. The “Welcome to my first app!” message in the app you made in Chapter 2, “Getting Started with SwiftUI” is a Text view.
-
Button: A view that performs an action when triggered. In iOS, a user triggers a button when they complete a button press by releasing the button after pressing down on it. “Hit me!” in the app you made in Chapter 2, “Getting Started with SwiftUI” is a Button view.
-
VStack: A view that acts as a container for other views and arranges them into a vertical stack. You used this to arrange the screen so that “Welcome to my first app!” is above “Hit me!”. Unlike the Text and Button views, the VStack view is invisible.
-
View: A view that represents the entire screen and acts as a container for all the other views on the screen. I wouldn’t call this view “invisible”, but it’s something that the user generally doesn’t notice.
Take a look at those Bullseye screen views again, but with the specific types of views called out this time:
You may have noticed a control you haven’t worked with before: The Slider. This control lets a user enter a number by sliding a control, which is called a thumb, along a straight track where one end represents a minimum value and the other end represents a maximum value.
Note: In most apps, you wouldn’t make the user enter a precise number value using a slider. However, for a game like Bullseye, the slider makes the game challenging. After all, we don’t want to make it too easy for the player!
As I mentioned earlier, some of the views that will go on the game screen are invisible. One of them is a VStack, which you’ve already used. You’ll continue to use it to arrange the views into rows. The screenshot below shows the rows that you’ll create using the VStack:
You’ll also need to arrange some rows side by side. To do this, you’ll use a new control, HStack, which acts as a container for other views and arranges them into a horizontal stack (hence the name). You’ll use three HStack views, and you’ll put each one inside a VStack cell, as shown below:
Reviewing what you’ve built so far
Let’s look at the code for the app as it is right now. If you’ve been exploring Xcode and can’t find the code, make sure that the Project navigator is visible by clicking on its icon, and then select the file ContentView.swift. This is the file that contains the code that defines the game’s screen:
Here’s the part of the code that you’ve been working with to define the game screen:
struct ContentView : View {
@State var alertIsVisible = false
var body: some View {
VStack {
Text("Welcome to my first app!")
.fontWeight(.black)
.color(.green)
Button(action: {
print("Button pressed!")
self.alertIsVisible = true
}) {
Text("Hit me!")
}
.presentation($alertIsVisible) {
Alert(title: Text("Hello there!"),
message: Text("This is my first pop-up."),
dismissButton: .default(Text("Awesome!")))
}
}
}
}
We’ve gone over this code before, but here’s a quick review of how this all works:
ContentView is the view representing the screen. The first line of the code begins with struct ContentView : View, and it tells you that ContentView is a View. Remember, any time you see a : character in Swift, you should read it as “is a”.
ContentView is an object, and objects can have properties, which are things that an object knows and methods, which are things that an object does. Right now, ContentView doesn’t have any methods, but it does have two properties. Each of these properties is a var, which means “variable”:
-
alertIsVisible: This property is
trueif the app is currently displaying the alert pop-up, andfalseotherwise. It’sfalseby default, but changes totruewhen the user presses the Hit me! button, and then changes back tofalsewhen the user dismisses the alert pop-up.@Statemarks it as a variable that Swift should watch, and tells Swift that it should be ready to take action if its contents change. -
body: This property defines the content, layout and behavior of the contents of
ContentView. Right now,ContentViewcontains a VStack of two views: The “Welcome to my first app!” Text view and the Hit me! Button view.
Notice that the definition of body starts with the line body: some View. You should read this as “body is a some View.” The grammar of that sentence is a little strange, but it means that the body property can hold either a plain View or some other type of View, such as Text, Button or in this case, a VStack.
The definition of body implies that it can hold only one view at a time. That would normally make for very simple, very boring apps except for the fact that there are some views that can hold other views. VStack, HStack and even View are examples of these. For Bullseye, we’ll fill body with a VStack, and fill that VStack with the other views that make up the game.
Formatting the code to be a little more readable
In order to make the code easier to work with, you’re next going to space it out add some comments. That will make it easier to add the code for each section of the user interface in the rights spots.
➤ Edit the code in ContentView.swift so that it looks like the code shown below. You won’t be deleting anything or changing any existing lines. You’ll be simply be adding lines — some of which are blank, and some of which begin with the // characters:
import SwiftUI
struct ContentView: View {
// Properties
// ==========
// User interface views
@State var alertIsVisible: Bool = false
// User interface content and layout
var body: some View {
VStack {
// Target row
Text("Welcome to my first app!")
.fontWeight(.black)
.foregroundColor(.green)
// Slider row
// TODO: Add views for the slider row here.
// Button row
Button(action: {
print("Button pressed!")
self.alertIsVisible = true
}) {
Text("Hit me!")
}
.alert(isPresented: self.$alertIsVisible) {
Alert(title: Text("Hello there!"),
message: Text("This is my first pop-up."),
dismissButton: .default(Text("Awesome!")))
}
// Score row
// TODO: Add views for the score, rounds, and start and info buttons here.
}
}
// Methods
// =======
}
// Preview
// =======
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
➤ Run the app. You shouldn’t notice any changes.
The changes you made don’t affect the way the program works, and as a result, they won’t make a difference to the user. They will make a difference to you, because they affect the way the code reads. Even with relatively simple apps like Bullseye, it code can quickly get complex. Anything you do to make the code easier to read and understand will help you write better, more error-free code.
The lines beginning with // are comments. Like blank lines, they also don’t perform any action or make any changes. Unlike blank lines, they contain text, but anything after the // characters and up to the end of the line is ignored. Comments are notes that programmers add to code to provide additional information about it. Programmers use comments for a number of purposes, including:
- Indicating who wrote the code and when. That’s what the comments at the start of the ContentView.swift are. Comments like this are often put at the start of the file, and Xcode automatically does this with all its source code files.
- Explaining what sections of code are for. That’s what you’re doing with these comments. They mark the different sections of the code, such as where the properties and methods of the
ContentViewobject go, the individual rows in the user interface, and so on. - Providing a summary of what the code does, especially in cases where it might otherwise be difficult to understand.
- Giving additional background information that’s not made clear in the code.
- Acting as a reminder to either fix something broken in the code or to add something missing to the code. In this sort of comment, many developers add words like TODO or HACK so that they can find these reminders again easily with a “search” function. You added a couple of TODO comments in the
bodyvariable to note that you need to add code to define the slider and score rows in the user interface.
Laying out the target row
Let’s start with the text at the top of Bullseye’s screen (highlighted below), which tells the user the target value they’re aiming for:
This text challenges the user to move the slider to a specific value. You could use a single Text object with both the challenge and the target value, but we’ll go with two: One for the challenge and one for the target value.
You want to lay these two Text objects out side by side, which sounds like an opportunity to use an HStack. Here’s a screenshot with some extra graphics showing how the Text objects and HStack fit together:
There are a couple of ways to create this HStack. One way is to type the necessary code into the Editor. But instead, you’ll do it another way: By using the Canvas. You’ll embed the existing “Welcome to my first app!“ text into an HStack, and then you’ll change its text.
➤ If the Resume button is visible in the upper-right corner of the Canvas, press it.
➤ In the Canvas, command-click on Welcome to my first app!. Select Embed in HStack from the menu that appears:
This embeds the “Welcome to my first app!” view into an HStack. If you look at the Editor, you’ll see that the code in the Target row section has been updated to reflect what you did on the Canvas:
// Target row
HStack {
Text("Welcome to my first app!")
.fontWeight(.black)
.foregroundColor(.green)
}
Next, you’ll do even more with the Canvas. It’s time to change the text.
➤ In the Canvas, command-click on Welcome to my first app!. Select Inspect… from the menu that appears:
➤ Use the inspector to change the text to “Put the bullseye as close as you can to:”:
This changes the Text view, and will also update the code in the editor:
// Target row
HStack {
Text("Put the bullseye as close as you can to:")
.fontWeight(.black)
.foregroundColor(.green)
}
➤ We don’t want the “Put the bullseye as close as you can to:” text to be green and bold, so remove the calls to the Text view’s fontWeight() and foregroundColor() methods so that the code in the Target row section looks like this:
// Target row
HStack {
Text("Put the bullseye as close as you can to:")
}
The next step is to add a new Text view to the HStack, to the right of the “Put the bullseye as close as you can to:” view. Use the library and the editor to do this.
➤ Open the library — remember, you do this by pressing the Library button, which is the + button located near the upper-right corner of the Xcode window:
➤ The Library window will appear. Make sure that you’ve selected the Views tab, which is the leftmost one, with the square-within-a-square icon, then highlight the Text item in the Library list:
➤ Click and start dragging the Text item from the library list and onto the editor as shown below:
➤ As you drag the item onto the editor, a blank line should appear below the Text("Put the bullseye as close as you can to:") line. When this blank line appears, drop the item. A new Text object will appear in the code, which will now look like this:
// Target row
HStack {
Text("Put the bullseye as close as you can to:")
Text("Placeholder")
}
➤ Change the placeholder text (literally “Placeholder”) in the newly-added Text view to 100. The code for the Target row should now look like this:
// Target row
HStack {
Text("Put the bullseye as close as you can to:")
Text("100")
}
➤ Build and run the app. You’ll see that the two Text views have replaced the “Welcome to my first app!” message:
The target value in the second Text view, 100, is a placeholder. You’re using 100 because this text view will eventually contain a random number between 1 and 100. 100 is the largest — and more importantly, widest — text that will go into this view.
Laying out the slider row
Your next task is to lay out the slider and the markings of its minimum value of 1 and maximum value of 100. These can be represented by a Text view, followed by a Slider view, followed by a Text view, all wrapped up in an HStack view:
This time, try setting this up by writing some code. After all, you have some examples of using HStack and Text in code already!
➤ In the Slider row section, replace the // TODO: Add views for the slider row here. line so that code looks like this:
// Slider row
HStack {
Text("1")
Text("100")
}
This defines the 1 and 100 Text views that are on the left and right sides of the slider, respectively. You could also type in the code to create the slider, but first, take a look at one more feature of the library.
➤ Open the library (press the + button near the upper right corner of the Xcode window). Make sure that you’ve selected the Views tab, which is the leftmost one, with the square-within-a-square icon, then type slider into the library’s search text field, which is just to the right of the magnifying glass icon. As you type, the library’s views in the library’s list will disappear until only the slider remains.
➤ Drag the Slider view from the library list onto the editor, and drop it in the empty line in the //Slider row section of the code between the Text("1) line and the Text("100) line:
The code for the Slider row should now look like this:
// Slider row
HStack {
Text("1")
Slider(value: .constant(10))
Text("100")
}
➤ Build and run the app. The Slider row is now visible:
If you tried to move the slider, you probably noticed that it’s stuck on the right side. This has something to do with the .constant(10) that you gave as the Slider’s value: argument.
This is a good time to look at a useful Xcode feature that allows you to find out more about just about anything in the code that has a name.
➤ While holding down the option key, move the cursor over the .constant in the line Slider(value: .constant(10)). The word constant should change color and the cursor’s shape should change to a question mark (?). Click on constant and a pop-up window will appear with more details about it:
The summary text — “Creates a binding with an immutable value” — may sound like meaningless techno-babble to you now, but the words binding and immutable should be hints that it has something to do with state.
You’ll deal with the mystery of the stuck slider soon enough, but you’ll finish setting up the controls first.
Laying out the Button row
Here’s a little gift for you: The Button row’s already done!
Laying out the Score row
The final row is the one at the bottom of the VStack: The Score row, which has a number of views:
These views are:
- A Button view labeled Start over: The user will press this button to start a new game. It will reset the score to 0 and the round to 1.
- Two Text views for the score: One containing the text “Score” and one containing the score value. For now, the score will be set to a placeholder value of 999999.
- Two Round views: One containing the text “Round”, and one containing the number of the current round. For now, this number will be set to a placeholder value of 999.
- A Button view labeled Info: The user will press this button to get more information about the game. It will take the user to another screen, where they’ll see the additional information.
Don’t forget that these should be all in a row, which means that you need an HStack, so start with that.
➤ In the Score row section, replace the // TODO: Add views for the score, rounds, and start and info buttons here. with a blank line.
➤ Open the library (once again, press the + button near the upper right corner of the Xcode window). Make sure that you’ve selected the Views tab (the leftmost one, with the square-within-a-square icon), then find the Horizontal Stack view:
Drag this view onto the editor and drop it into the blank line after // Score row:
The code starting at // Score row should now look like this:
// Score row
HStack {
Text("Placeholder")
}
Notice that Xcode didn’t just give you an HStack; it also included a Text view. You didn’t ask for it, so try removing it by deleting the Text("Placeholder") line.
Here’s what happens:
Xcode is diligent about alerting you to errors in your code, and it turns out that an empty HStack is an error. An HStack must contain at least one view, so Xcode did a little preemptive error prevention by throwing in a “free” Text view when creating the HStack. You’ve probably figured out that the same rule applies to VStack views.
➤ If you got experimental and tried removing the Text view from the HStack, try undoing the change. If that doesn’t work, simply type in code so that the code starting at // Score row looks like this:
// Score row
HStack {
Text("Placeholder")
}
Since Button views contain Text views, Xcode has included a useful feature that takes advantage of this.
➤ Command-click on the Text keyword in the // Score row section of the code. You should see this pop-up menu:
➤ Scroll down the menu and select Embed in Button. Xcode will embed the Text view inside a Button view and the code will now be:
// Score row
HStack {
Button(action: {}) {
Text("Placeholder")
}
}
➤ Copy the Button code and paste it so that the code becomes the following:
// Score row
HStack {
Button(action: {}) {
Text("Placeholder")
}
Button(action: {}) {
Text("Placeholder")
}
}
➤ Change the Text view in each Button view so that the first one becomes the Start over button and the second one becomes the Info button. The code should now look like this:
// Score row
HStack {
Button(action: {}) {
Text("Start over")
}
Button(action: {}) {
Text("Info")
}
}
By now, you’re probably beginning to get the hang of creating user interfaces the SwiftUI way. Next, add the remaining Text views between the Button views.
➤ Add Text views so that the // Score row code becomes this:
// Score row
HStack {
Button(action: {}) {
Text("Start over")
}
Text("Score:")
Text("999999")
Text("Round:")
Text("999")
Button(action: {}) {
Text("Info")
}
}
➤ Build and run the app. The Simulator should display this:
All the controls are there, but the app looks somewhat compressed. In the next section, you’ll fix that.
Introducing spacers
It’s time to bring some Spacer views into your app. As their name implies, these views are designed to fill up space.
When you put a Spacer view into an HStack, it expands to fill up the remaining horizontal space.
Next, see what happens when you use a spacer in the Score row.
➤ Add Spacer views to the Score row code so that it looks like this:
// Score row
HStack {
Button(action: {}) {
Text("Start over")
}
Spacer()
Text("Score:")
Text("999999")
Spacer()
Text("Round:")
Text("999")
Spacer()
Button(action: {}) {
Text("Info")
}
}
➤ Build and run the app. The Score row should look a lot less compressed:
Spacer views also work in VStack views. There, they expand to fill up the remaining vertical space.
Let’s put some Spacer views in your app’s VStack in the following places:
- Above the Target row.
- Between the Target row and the Slider row.
- Between the Slider row and the Button row.
- Between the button row and the Score row.
➤ Change the code for ContentView so that it looks like this:
struct ContentView: View {
// Properties
// ==========
// User interface views
@State var alertIsVisible: Bool = false
// User interface content and layout
var body: some View {
VStack {
Spacer()
// Target row
HStack {
Text("Put the bullseye as close as you can to:")
Text("100")
}
Spacer()
// Slider row
HStack {
Text("1")
Slider(value: /*@START_MENU_TOKEN@*/.constant(10)/*@END_MENU_TOKEN@*/)
Text("100")
}
Spacer()
// Button row
Button(action: {
print("Button pressed!")
self.alertIsVisible = true
}) {
Text("Hit me!")
}
.alert(isPresented: self.$alertIsVisible) {
Alert(title: Text("Hello there!"),
message: Text("This is my first pop-up."),
dismissButton: .default(Text("Awesome!")))
}
Spacer()
// Score row
HStack {
Button(action: {}) {
Text("Start over")
}
Spacer()
Text("Score:")
Text("999999")
Spacer()
Text("Round:")
Text("999")
Spacer()
Button(action: {}) {
Text("Info")
}
}
}
}
// Methods
// =======
}
➤ Run the app. It’s looking a whole lot better!
There’s just one last little change you need to make to the app’s layout: The Score row is a little too close to the bottom. In the next section, you’ll find out how to fix that.
Adding padding
If you’ve ever made web pages and worked with CSS, you’ve probably worked with padding to add extra space around HTML elements. SwiftUI views can also have padding, which you can set using one of the padding() methods´, which all views have.
In this case, we want to add some padding to the bottom of the Score row. You can do this by calling the padding() method for the HStack containing the Score row.
➤ Add the following to the end of the Score row code, so that it ends up looking like this:
// Score row
HStack {
Button(action: {}) {
Text("Start over")
}
Spacer()
Text("Score:")
Text("999999")
Spacer()
Text("Round:")
Text("999")
Spacer()
Button(action: {}) {
Text("Info")
}
}
.padding(.bottom, 20)
The padding() method takes two arguments:
- The set of edges to pad. There are a number of options for this argument including
.bottom(which you just used),.leadingandtrailing(for the leading and trailing edges, respectively),.top,.horizontal(which pads both leading and trailing edges),.vertical(which pads both top and bottom edges) and.all. - The amount of padding. This is a number of screen distance units called points, which you’ll cover a little later on.
➤ Build and run the app. It looks a whole lot better!
Solving the mystery of the stuck slider
Let’s get back to why the slider doesn’t work. As mentioned earlier, it has to do with state.
If you’ve ever gone to a restaurant where the sign said they were open, only to find that they were closed when you tried to enter, you’ve experienced what happens when a user interface, in this case the “Open” sign, doesn’t match the state (the place was actually closed).
This kind of problem happens when the user interface and state aren’t connected. In the restaurant example, keeping the “open/closed” sign in sync with the restaurant’s actual open/closed state means that someone has to make sure that the sign is always providing the right information. If you’ve ever worked in the food service industry, you know that the kind of dedication and reliability needed to make sure that the sign is always right is rare.
You may have seen this sort of thing in software as well. One example is the user interface of an email app that tells you that you have a new message, but when you check, it turns out that you’d already read it. You’ve probably seen other examples of user interfaces that were wrong about their application’s state. As apps grow, their state becomes more complex, and it’s all too easy to forget to update some part of the user interface when some state detail changes.
SwiftUI solves the problem of the mismatch between user interface and application state by creating bindings between them. In a SwiftUI application, when you update some property that’s part of its state, any user interface elements bound to that property automatically update to reflect the change.
You can also choose to make two-way bindings, where if the user changes the value of some user interface element that’s bound to some state property by pressing a button, entering a value into a text field or moving a slider, that property is automatically updated.
This means that in SwiftUI, user interface controls have to be connected to some kind of value. Sometimes, that value is a constant, which is often the case with Text views:
Text("This is a constant value")
The code that currently sets up the slider in Bullseye is similar:
Slider(value: .constant(10))
In this case, the slider is bound to a state property that is set to 10 and can’t be changed; therefore the slider’s position can’t be changed, either.
Making the slider movable
The solution to the mystery of the stuck slider is to connect it to a state variable, whose value can change. So now, declare one. You’ll call it sliderValue and set its initial value to 50.
➤ Add a declaration for the sliderValue variable to the Properties section of ContentView, so that the lines starting with the // User interface views comment look like this:
// User interface views
@State var alertIsVisible: Bool = false
@State var sliderValue: Double = 50.0
Remember, @State marks the variable as part of the application’s state and tells Swift to watch it for changes to its value. The var sliderValue: Double = 50 part is Swift for “The variable sliderValue is a Double, and its value is 50.0”
Note: A Double is a Swift data type that represents numbers with decimal points really, really, really precisely. We’re using it because that’s the kind of value that sliders work with. We’ll talk more about data types soon.
Now that there’s a state variable for the slider, it’s time to connect the two together!
➤ Change the line where the slider is set up to the following:
Slider(value: self.$sliderValue, in: 1...100)
This code creates — or in programmer-speak, instantiates — a Slider view. Here’s what the three parameters do:
-
value: Specifies the binding that connects a state variable to the slider. If you think of
sliderValueas the value of the slider’s current position,$sliderValue(note the$) is the two-way connection between the slider and thesliderValue. Changing the value ofsliderValueaffects the position of the slider’s thumb (the thing on the slider that moves), and moving the thumb on the slider changes the value insliderValue. Since$sliderValueis a property of the object that you’re in, you precede it withself.. -
in: Specifies the range of values that the slider covers.
1...100represents the range of values starting at 1 at its minimum and ending and including 100 at the maximum.
➤ Build and run the app. The initial value of sliderValue is 50.0, which means that the slider’s initial position is midway between the left and right ends. You can also move the slider now!
Reading the slider’s value
In order to for the game to work, we need to know the slider’s current position. Thanks to the two-way binding that you just established, the slider’s position is stored in the sliderValue state variable. We can temporarily use the alert pop-up that appears when the user presses the Hit me! button to display this value.
Here’s the code for the Button row:
// Button row
Button(action: {
print("Button pressed!")
self.alertIsVisible = true
}) {
Text("Hit me!")
}
.alert(isPresented: self.$alertIsVisible) {
Alert(title: Text("Hello there!"),
message: Text("This is my first pop-up."),
dismissButton: .default(Text("Awesome!")))
}
Now, change the argument that you provide to the message: parameter so that it displays the slider’s current value.
➤ Change the call to the Button’s presentation() method to:
.alert(isPresented: self.$alertIsVisible) {
Alert(title: Text("Hello there!"),
message: Text("The slider's value is \(self.sliderValue)."),
dismissButton: .default(Text("Awesome!")))
}
➤ Build and run the app, move the slider anywhere you like, then press the Hit me! button. The alert pop-up will appear, and it’ll give you a painfully precise readout of the slider’s value:
Take a closer look at the Text view that was passed to the alert pop-up’s message: parameter:
Text("The slider's value is \(sliderValue).")
The alert pop-up uses the text before and after \(sliderValue) — “The slider’s value is ” and “.”. On the other hand, \(sliderValue) is replaced by the value in the sliderValue variable. Think of the \( … ) as a placeholder: “The slider’s value is X”, where X will be replaced by the value of the slider.
Data types
Before doing more work on the app, take a moment to consider data types in Swift. These classify the different kinds of data that Swift can work with.
Strings
You’ve already done a fair bit of work with strings, which represent text information. Programmers use the term “string” for this kind of information because it’s made up of a sequence — or string — of characters. Think of characters in a string as being like pearls on a necklace:
Here are some the strings you’ve used so far:
"Welcome to my first app!""Hit me!""Awesome!""The slider's value is \(sliderValue)."
You used the first three when making Text views, and the last one to display the slider’s value in the alert pop-up.
Creating a string is simple in Swift: Just put text between a pair of “double quote” characters ("). Other languages let you create strings by using either “single quote” characters (') or double quotes, but Swift doesn’t. Strings should be delimited — that’s computer-science fancy-talk for “started with and ended with” — by double quotes. And they must be plain double quotes, not typographic “smart quotes”.
To summarize, this is the proper way to make a Swift string…
"I am a good string"
…and these are wrong:
’I should have double quotes’’’Two single quotes do not make a double quote’’“My quotes are too fancy”
Inserting variables’ values into strings
Anything between the characters \( and ) inside a string is special — instead of taking that information literally, Swift evaluates whatever is between those characters and turns the result into a string.
You used this in the alert pop-up to display the value of the slider’s current position, which is stored in the state variable sliderValue. You did it by using this string to create the alert pop-up:
"The slider's value is \(sliderValue)."
If you run the app and press the Hit me! button without moving the slider (which means its value will be 50), the pop-up doesn’t display this text:
The slider’s value is \(sliderValue).
Instead, it says:
The slider’s value is 50.000000.
Inside a string, Swift treats the \( and ) as markers for the beginning and ending of something it should evaluate, converts that thing into a string, and then inserts it into the rest of the string.
This feature isn’t limited to numerical variables — you can also put string variables between \( and ). Suppose a variable named weather contains the value sunny. Swift interprets the string "We expect the weather to be \(weather) today." as We expect the weather to be sunny today.
You can even put calculations between \( and ). Swift interprets the string "Your answer, \(1 + 2), is correct." as Your answer, 3, is correct.
Filling in the blanks this way is a very common way to build strings in Swift and is known as string interpolation.
Numbers
Swift has a number of ways to represent numerical values. The two that you’ll probably use the most are:
- Double: This is short for double-precision floating-point number, which is a fancy computer-science way of saying “painfully precise number.” It’s accurate to 15 or 16 digits, which should be more than enough precision for most calculations. It also has a large range, being able to represent numbers as small as 10-345 and as large as 10308. You’ll use these when you need to store and work with numbers with decimal points.
- Int: This is short for integer, which simply means “whole number”. You’ll use these when you need to store and work with numbers without decimal points.
You’ve already worked with a Double when you created the sliderValue variable to store the position of the slider. The slider reports its position as a Double, so that’s what we use to store its value.
Booleans
You’ll often have to store values of the “yes/no” or “on/off” kind. That’s what Bool variables — short for “Boolean” — are for. They can store only two values: true and false.
Boolean values, which are often referred to simply as Booleans, are often used in programs to make decisions. You’ll soon learn about the if statement, which performs a set of instructions if some condition is true, and another set of instructions (or no instructions) if the condition is false.
Variables
If you’re new to programming, it’s important to remember that programs are really made of just two things:
- Data, which is information that we want to manage, process and perform calculations with.
- Instructions, which tell the computer to how to manage, process and perform calculations with the data.
Programs store their data in variables. So far, Bullseye stores its data in just two variables — one to keep track of the slider’s position, and one to keep track of whether or not the alert pop-up is visible. The previous chapter told you to think of variables as temporary storage containers, each one storing a single piece of data. Just as there are containers of all shapes and sizes, data also comes in all kinds of shapes and sizes, which we call data types. You’ve just looked at a few data types: Strings, Ints, Doubles and Bools.
The idea is to put the right shape in the right container. The container is the variable and its type determines what “shape” fits. The shapes are the possible values that you can put into the variables. You might want to think of variables as being like children’s toy blocks:
You won’t just put stuff in the container and then forget about it. You’ll often replace the contents with a new value. When the thing that your app needs to remember changes, you take the old value out of the box and put in the new value. That’s the whole point behind variables: They can vary.
For example, sliderValue will change every time the user moves the slider, and you’ll change the value of alertIsVisible to true every time the user presses the Hit me! button. The size of the storage container and the sort of values the variable can remember are determined by its data type, or just type.
You specified the Double type sliderValue variable, which means this container can hold very precise numbers. Double is one of the most common data types. There are many others though, and you can even make your own.
The idea is to put the right shape in the right container. The container is the variable and its type determines what “shape” fits. The shapes are the possible values that you can put into the variables.
You can change the contents of each box later, as long as the shape fits. For example, you can take out a blue square from a square box and put in a red square — the only thing you have to make sure of is that both are squares.
But you can’t put a square in a round hole: The data type of the value and the data type of the variable have to match. You can’t put a string value into an integer variable, and you can’t put an integer value into a string variable. The type of the value has to match the variable.
How long do variables last?
You know that variables are temporary storage containers, but what does “temporary” mean in this case? How long does a variable keep its contents?
Unlike meat or vegetables, variables won’t spoil if you keep them for too long. A variable will hold onto its value indefinitely, until you put in a new value or destroy the container altogether.
Each variable has a certain lifetime, also known as its scope, which depends on exactly where in your program you defined that variable. In the case of Bullseye, both alertIsVisible and sliderValue stick around for just as long as their owner, ContentView, does. Their fates are intertwined.
ContentView exists for the duration of the app, and therefore so do alertIsVisible and sliderValue. They don’t get destroyed until the app quits. Soon, you’ll also see variables that are short-lived (also known as local variables).
Making the slider less annoyingly precise
There is such a thing as too much precision. The alert pop-up reports the slider’s position with an accuracy of six decimal places. We want the game to be challenging, but not that challenging! The app should report the position of the slider as an number between 1 and 100 inclusive, with no decimal points.
The Slider reports its position as a Double, which is a very precise number with a decimal point. Right now, the app simply takes this number and displays it in the alert pop-up. We want the pop-up to round the position to the closest whole number and report the position as an Int.
Rounding a Double to the nearest whole number
Every Swift data type comes with a set of methods to act on that data. Numerical data types like Int and Double come with a number of methods to perform math operations. Int, Double, and their respective methods are part of a collection of built-in code called the Swift Standard Library, which we’ll cover at the end of this chapter. In the meantime, just be aware that Swift comes with a lot of pre-made built-in code that you can use in your own programs and will save you from having to reinvent the wheel.
The Double type has methods that are useful for working with numbers with decimal points. One of these is rounded(), which takes the original value and gives back — or as we say in programming, returns — a rounded version of the value. For example, if you call rounded() on a Double variable containing the value 4.2, it will return the value 4.0.
rounded() uses “schoolbook rounding,” which means that if the fractional part of the number is 0.5 or larger, it rounds up to the higher value. It rounds 4.5 up to 5.0, 4.4999 to 4.0, and -4.5 to -5.0.
➤ Update the code for the Button row so that it looks like the following:
// Button row
Button(action: {
print("Button pressed!")
self.alertIsVisible = true
}) {
Text("Hit me!")
}
.alert(isPresented: self.$alertIsVisible) {
Alert(title: Text("Hello there!"),
message: Text("The slider's value is \(self.sliderValue.rounded())."),
dismissButton: .default(Text("Awesome!")))
}
➤ Build and run the app. Move the slider wherever you like, and then press Hit me!. You should see something like this:
This is an improvement, but there’s still the matter of those trailing zeros. There are a couple of ways to remove them:
- One way is to format the number to show only the digits before the decimal point.
- Another way would be to convert the number into an integer (an
Int) and display that value.
Later on in the development of Bullseye, we’re going to generate a target number, which will be a whole number that the user will have to match by positioning the slider. This means that the target number will be an Int. We’ll need to compare the target number to the value of the slider’s position, so the second approach sounds like the better one.
The simplest way to convert a Double value into an Int value is to create a new Int value and use the Double’s value to initialize it.
➤ Update the code for the Button row so that it looks like the following:
// Button row
Button(action: {
print("Button pressed!")
self.alertIsVisible = true
}) {
Text("Hit me!")
}
.alert(isPresented: self.$alertIsVisible) {
Alert(title: Text("Hello there!"),
message: Text("The slider's value is \(Int(sliderValue.rounded()))."),
dismissButton: .default(Text("Awesome!")))
}
➤ Build and run the app. Move the slider wherever you like, then press Hit me!. You should see something like this:
Here’s the part that’s changed:
message: Text("The slider's value is \(Int(sliderValue.rounded()))."),
In this code, sliderValue.rounded() is being fed into Int(). This tells Swift to create a new Int using sliderValue.rounded(), which is the value of the slider rounded to the nearest whole number.
Any time you see a capitalized word followed by parentheses (( and )), braces ({ and }) or both, it usually means that something new is being created — or in programming terms, instantiated — using the things between the parentheses and braces. You’ve already seen many examples of this. Here’s one:
Text("Here is some text.")
This instantiates a new Text view containing the text “Here is some text.”
Here’s a more complex example:
VStack {
Text("Here is some text.")
Text("And here's more text!")
}
This instantiates a new VStack view, and inside it, two Text views are also instantiated.
The Swift Standard Library
You could’ve written a method to round a Double to the nearest whole number, but you didn’t have to. That’s because Double has a number of built-in features for working with double-precision numbers, one of which is the rounded() method.
Double, rounded(), and many other similar goodies are part of a large collection of code that make up the Swift Standard Library. It contains a lot of useful pre-made functionality that you can use in your own apps, including:
- Data types, including the ones you worked with in this chapter:
Int,Double,String, andBool. - Methods that go with each of those data types, which let you do more things with them. You’ve already used one of them:
Double’srounded()method. In the next chapter, you’ll use a couple more Standard Library methods to generate a random number and remove the “minus sign” from a negative number. - Functions, such as
print(). - Other more advanced features, all of which can serve as the building blocks for your apps that you don’t need to write yourself.
It’s pretty much impossible to do any Swift programming without making use of something in the Swift Standard Library — that how useful it is. It does more than just provide useful features. It will also save you so much time because it contains all sorts of things that you’d otherwise have to code yourself. Better yet, all the features it provides have the advantage of having been throroughly tested — and not just by Apple’s quality assurance team, but by the entire Swift developer community who use it regularly (and who complain loudly when it’s not working as they expect).
You’ll use the Swift Standard Library as often as you use Xcode, so it’s worth reviewing it regularly as you learn Swift and iOS programming. It evolves with each version of Swift, so even experienced developers, looking at its online documentation from time to time to see what changed.
To see everything that the Swift Standard Library has to offer, visit the Swift Standard Library home page, located in Apple’s developer documentation site at https://developer.apple.com/documentation/swift/swift\_standard_library.
Key points
So far, you’ve done the following:
- Set up all of Bullseye’s basic user interface elements.
- Made the slider work
- Converted the original alert pop-up to report the slider’s position value.
Along the way, you’ve learned a lot, including:
- What portrait and landscape orientations are and how to and make an app landscape-only.
- Different types of views.
- A little more about state and variables.
- Data types.
In the next chapter, you’ll take the app and turn it into a functioning game!
You can find the project files for the app up to this point under 03 - Slider in the Source Code folder.