4.
Swift Basics
Written by Joey deVilla
So by now, you’ve built the user interface for Bullseye, you’ve made the slider work and you know how to find its current position. That already knocks quite a few items off your to-do list. In this chapter, you’ll take care of a few more items on that list. Here’s what this chapter will cover:
- Generating and displaying the target value: Select the random number that the player will try to match using the slider and display it onscreen.
- Calculating the points scored: Determine how many points to award to the player based on how close they came to positioning the slider at the target value.
- Writing methods: You’ve used some built-in methods so far, but built-in methods can’t cover everything. It’s time to write your own!
- Improving the code: Make the code more readable so that it’s easier to maintain and improve and less error-prone.
- Key points: A quick review of what you learned in this chapter.
Generating and displaying the target value
First, you need to come up with the random number that the user will try to match using the slider. Where can you get a random number for each game’s target value?
Generating (sort of) random numbers
Random numbers come up a lot when you’re making games because games need to have an element of unpredictability. You can’t get a computer to generate numbers that are truly random and unpredictable, but you can employ a pseudo-random number generator to spit out numbers that at least appear to be random.
To make random numbers, pseudo-random generators typically start with a seed value, a number derived from an event that isn’t easy to predict. Some examples include the number of milliseconds the system has been running, the user’s most recent keyboard input, mouse clicks and other events. They feed this seed value into a mathematical formula that creates a list of wildly different numbers that appear random.
If you were to run a pseudo-random number generator that always started with the same seed value, it would always generate the same set of numbers in the same order. But it’s pretty unlikely that the events Swift uses for its seed values will be exactly the same each time, so the random number generators built into Swift are good enough for everyday applications like games.
For the curious: macOS and iOS constantly update a file named /dev/random with hard-to-predict values from the system’s device drivers, and you can use that file as a source of seed values for pseudo-random number generators. You can see what it contains by opening the Terminal app and entering
od -d /dev/randomon the command line. You’ll see a stream of numbers, which you can stop by typing control+c.
Generating a random target number
Swift’s data types for numbers, which include Int and Double numeric types, have a method that lets you generate random numbers in a given range.
➤ Add the following line to the start of ContentView.swift:
@State var target: Int = Int.random(in: 1...100)
The set of @State variables should now look like this:
@State var alertIsVisible: Bool = false
@State var sliderValue: Double = 50.0
@State var target: Int = Int.random(in: 1...100)
Take a closer look at the line you just added.
The first half of the new line, @State var target: Int, doesn’t include anything that you haven’t already seen. It says that you’re declaring a variable named target that holds Int (i.e., integer, or whole number) values, and that target makes up part of the state for ContentView. As a state variable, Swift will watch target for changes to its value, then take any necessary action when that value changes.
The second half of the line, = Int.random(in: 1...100), might be new to you. It assigns an initial value to target, and that value is a random number between 1 and 100 inclusive. The random number comes from the random() function built into the Int data type to get a pseudo-random integer between 1 and 100. The 1...100 part is a closed range, which you should read as “all the numbers between 1 and 100, including 1 and 100.” The ... part indicates that you want the range to include the last number (100) as part of the range.
You could also use a half-open range, which you specify with these characters: ..<. 1..<100 is an example of a half-open range, and you should read it as “all numbers between 1 and 100, including 1, excluding 100.” If you wanted to specify a range of numbers from 1 to 100 inclusive using a half-open range, you’d do it with 1..<101.
With this single line, the app now generates a new random target value every time it starts. Your next step is to make that target value visible to the user.
Displaying the target value
➤ Scroll down to the part of the body variable that begins with the comment line Target row:
// Target row
HStack {
Text("Put the bullseye as close as you can to:")
Text("100")
}
This code defines the text near the top of the screen, which tells the user what the target value is:
Right now, the text that displays the score value holds the placeholder text “100”:
Text("100")
➤ Change it so that it displays the value inside target, the state variable you just created:
Text("\(self.target)")
You’ve just replaced the 100 with \(self.target). As you learned in the previous chapter, the characters \( and ) have a special meaning when used inside a string. They mark the beginning and end of something that Swift should evaluate, convert into a string and then insert into the rest of the string.
In this case, the object between \( and ) that Swift needs to evaluate is self.target. Remember that any time you see code in the form of object.feature, you should read it as code that makes use of an object’s feature. In this case, the object is self, which is Swift for “the object that this code lives in.” In this case, self refers to ContentView. The feature is target, which is one of ContentView’s variables… the one you just created.
Since target is a state variable (because you marked it with the keyword @State), the Text object will always display the current value of target, even when target changes. You’ll see this in action in the next chapter, when you incorporate multiple rounds into the game.
➤ Build and run the app. There’s only a 1 in 100 chance that your target will be to put the bullseye as close as possible to 100:
➤ Stop the app and run it again, then do that again a few more times. 99% of the time when you restart the app, the target value will be different from the previous one.
Calculating and displaying the points scored
Now that you have both the target value and a way to read the slider’s position, as you learned from the previous chapter, you can calculate how many points the player scored.
How close is the slider to the target?
The closer the slider is to the target when the player presses the Hit me!, the more points they should receive. To calculate the score for each round, you look at how far the slider’s value is from the target:
A simple approach to finding the distance between the target and the slider is to subtract sliderValue from target.
Unfortunately, that gives a negative value if the slider is to the right of the target because now sliderValue is greater than target.
You need some way to turn that negative value into a positive value — or you end up subtracting points from the player’s score (unfair!).
Doing the subtraction the other way around — sliderValue minus target — won’t always solve things either because, then, the difference will be negative if the slider is to the left of the target instead of the right.
Hmm, it looks like you’re in trouble here…
Exercise: How would you frame the solution to this problem if you wanted to solve it in natural language? Don’t worry about how to express it in code for now. Just think it through in plain language.
I came up with something like this:
-
If the slider’s value is greater than the target value, then the difference is: Slider value minus the target value.
-
However, if the target value is greater than the slider value, then the difference is: Target value minus the slider value.
-
Otherwise, both values must be equal, and the difference is zero.
If you prefer to think in pictures, here’s the solution in flowchart form:
This will always lead to a difference that is a positive number, because you always subtract the smaller number from the larger one. Do the math to test it out:
- If the slider is at position 60 and the target value is 40, then the slider is to the right of the target and has a larger value. The difference is 60 – 40 = 20.
- However, if the slider is at position 10 and the target is 30, then the slider is to the left of the target and has a smaller value. The difference is 30 – 10 = 20.
Calculating the points scored
The number of points the player receives should depend on the difference between the slider value and the target value:
- When the slider is right on top of the target, the difference between the slider value and the target value is 0. In this case, the player should receive the maximum number of points for getting a bullseye.
- When the slider is as far as possible from the target, it means that the slider is at one end and the target is at the opposite end. In this case, the player should receive the minimum number of points for being way off.
For now, we’ll use a simple formula:
points = 100 - difference between slider value and target value
With this formula, the player earns 100 points for placing the slider right at the target value. In the case where the slider is at one end and the slider is at the opposite end, the player scores one point, just for showing up.
Algorithms
In coming up with a way to calculate the score, you’ve come up with an algorithm. That’s a fancy term for a process or series of steps to follow to perform a calculation or solve a problem. This algorithm is very simple, but it’s an algorithm nonetheless.
There are many algorithms that you can adapt for use in your own programs. As you gain more experience programming, you might run into well-known ones such as quicksort, for sorting a list of items, and binary search, for quickly searching a sorted list. The academic field of computer science centers around studying algorithms and finding better ones. You can find these algorithms in books or online and you can use them in your own programs, saving you from having to reinvent the wheel.
Although there are many published algorithms, you’ll still have to come up with your own algorithms to fit the specific needs of the program you’re writing. Some will be as simple as the one above; others will be complex and might cause you to throw up your hands in despair. That’s part of the fun of programming.
You can describe any algorithm using plain language or diagrams — use whatever method works better with the way you think. Remember that an algorithm, no matter how complex it is or how fancy a result it produces, is just a set of steps to follow.
If you ever get stuck and you don’t know how to make your program calculate something, step away from the computer and think the steps through. Take out a piece of paper — still one of the best software engineering tools out there — and try to write or draw out the steps. Ask yourself how would you perform the calculation or solve the problem by hand. Once you know how to do that, converting the algorithm to code should be a piece of cake.
Writing your own methods
Back near the start of Chapter 2, you read about the concept of functional decomposition, which is the process of tackling a large project by breaking it into sub-projects, and possibly breaking the sub-projects into even smaller sub-projects until they are manageable.
Whenever you find yourself thinking something along the lines of, “At this point in the app, I need to tackle this sub-project,” that’s a sign that you need to create a method to perform that task. Once you have a method, you can simply activate it by calling it by name.
You’ve already made use of a pre-defined method: rounded(), which comes built-in with the Double data type. You used it to round the slider’s current value to the nearest whole number:
// Button row
Button(action: {
print("Button pressed!")
self.alertIsVisible = true
}) {
Text("Hit me!")
}
.presentation(self.$alertIsVisible) {
Alert(title: Text("Hello there!"),
message: Text("The slider's value is \(Int(self.sliderValue.rounded()))."),
dismissButton: .default(Text("Awesome!")))
}
There couldn’t possibly be a built-in method for every purpose, but that’s not a problem because you can write your own. Start by writing a method to calculate how many points the player should receive.
Implementing a basic method for calculating the points to award the player
In building a method to calculate how many points to award to the player, you’re going to use an approach called stepwise refinement. This means starting by building the simplest thing that could possibly work and then refining it over a number of steps until you get the desired result.
The first version of this method will simply award 100 points to the player, no matter where the player positioned the slider. At this point, you want it to simply report a number.
➤ Add the following to the end of ContentView, after the Methods comments and before ContentView’s closing brace (}):
func pointsForCurrentRound() -> Int {
return 100
}
Now, take a look at this new method, starting with the first line:
func pointsForCurrentRound() -> Int {
This line is the method’s signature, which specifies three things:
- The name of the method.
- The information that the method must receive.
- The information that the method provides as a result.
Just like structs and vars, methods start with a keyword that specifies what kind of thing you’re defining. For methods, this keyword might surprise you: it’s func, which is short for function. You use this keyword because methods are a kind of function, which is a general term for a block of code that you can call by name and which may or may not return a value at the end.
The name of the method follows the func keyword. In this case, it’s pointsForCurrentRound().
You’ve probably noticed that method names end with parentheses (the () characters). That’s a convention borrowed from the way you write mathematical functions. That’s how you can tell whether something’s a variable or a function: Function names end with parentheses, while variable names don’t.
Some methods require additional information before they can perform their task. You’ve already used such a method, rounded(), which requires a rounded number. If you were defining a method that required additional information, you would put that information within the parentheses. Since pointsForCurrentRound() doesn’t require additional information, you don’t have to put anything between the parentheses.
After the parentheses comes this symbol: ->. It doesn’t mean “minus” followed by “greater than.” Rather, you should interpret it as an arrow pointing rightward, and should read it as “returns a value of the following data type.” This is immediately followed by Int. This means that when pointsForCurrentRound() has completed its task, it should give back — or as we say in programming, return — an integer value.
After the method signature comes the body of the method, which goes between braces (the { and } characters). The body of the method specifies what the method does.
The body of the pointsForCurrentRound() method is a single line:
return 100
The return keyword defines the result that the method provides. return 100 makes the method provide a result of 100 when called.
So you’ve completed your first step, reporting a number that you’ve set! Now that you have a basic method for calculating the points to award the player, you can see it in action.
Calling the method and viewing its result
To see how it works, use print to show what pointsForCurrentRound() returns.
➤ Change the print statement in the Button row section so that it displays the results of pointsForCurrentRound(). The result should look like this:
// Button row
Button(action: {
print("Points awarded: \(self.pointsForCurrentRound())")
self.alertIsVisible = true
}) {
Text("Hit me!")
}
.alert(isPresented: self.$alertIsVisible) {
Alert(title: Text("Hello there!"),
message: Text("The slider's value is \(Int(self.sliderValue.rounded()))."),
dismissButton: .default(Text("Awesome!")))
}
➤ Build and run the app and press the Hit me! button a few times, keeping an eye on the Xcode’s debug console. You should see a line that says Points awarded: 100 for each press of the Hit me! button:
pointsForCurrentRound() returns the number that you set, but that’s not the correct one. Next, you’ll fix that so the game awards the right number of points.
Making pointsForCurrentRound() actually calculate points
Now that pointsForCurrentRound() returns a value and the alert pop-up displays that value, it’s time to change the value to the actual number of points that the player should receive.
➤ Replace the code in pointsForCurrentRound() so that the method looks like this:
func pointsForCurrentRound() -> Int {
var difference: Int
if self.sliderValue.rounded() > self.target {
difference = self.sliderValue.rounded() - self.target
} else if self.target > self.sliderValue.rounded() {
difference = self.target - self.sliderValue.rounded()
} else {
difference = 0
}
return 100 - difference
}
You’ve now replaced the code that simply awards the player 100 points no matter how well they played with code that implements the algorithm you created earlier.
The code may look right, but Xcode will take exception:
The error messages show what the problem is: You’re attempting to compare and subtract two different kinds of things:
-
sliderValue.rounded(), which is aDouble. -
targetwhich is anInt.
To us humans, both Double and Int values are just numbers, and differentiating between them seems like nitpicking. However, computers represent Double and Int values very differently, and the compiler treats them as very different things. A computer can’t make sense of it.
To compare sliderValue and target and perform arithmetic on them, you’ll need to convert one of them to the same type as the other. Since you’re only using a Double because sliders use that data type to report their values, we’ll convert sliderValue.rounded() into an Int.
➤ Update pointsForCurrentRound() so it looks like this:
func pointsForCurrentRound() -> Int {
var difference: Int
if Int(self.sliderValue.rounded()) > self.target {
difference = Int(self.sliderValue.rounded()) - self.target
} else if self.target > Int(self.sliderValue.rounded()) {
difference = self.target - Int(self.sliderValue.rounded())
} else {
difference = 0
}
return 100 - difference
}
The error messages should be gone now. Before running the app, take a moment to review this new code.
The first line should be familiar:
var difference: Int
This declares a new variable, difference, which you’ll need to store the difference between the slider’s current position and the target value. Since the difference will be a whole number, it’s an Int.
Note that you haven’t assigned a value to difference, you’ve simply declared it’s an Int. That’s because you’ll assign a value to it in the lines of code that follow.
What follows the first line is new:
if Int(self.sliderValue.rounded()) > self.target {
difference = Int(self.sliderValue.rounded()) - self.target
} else if self.target > Int(self.sliderValue.rounded()) {
difference = self.target - Int(self.sliderValue.rounded())
} else {
difference = 0
}
The if construct allows your code to make decisions, and it works much as you expect:
if something is true {
then do this
} else if something else is true {
then do that instead
} else {
do something when neither of the above are true
}
Basically, you put a logical condition after the if keyword. If that condition turns out to be true, like if sliderValue is greater than target, then the code in the block between the { } brackets executes.
However, if the condition isn’t true, then the computer looks at the else if condition and evaluates that instead. There may be more than one else if, and code execution moves one by one from top to bottom until one condition proves to be true.
If none of the conditions are found to be valid, then the code in the final else block executes.
In the implementation of this little algorithm, you compare sliderValue.rounded() against the target. Remember that the slider (and therefore sliderValue) is precise to about 6 decimal places, so we’re rounding its value to the nearest whole number.
First, you determine if sliderValue.rounded() is greater than target:
if self.sliderValue.rounded() > self.target {
The > is the greater-than operator. The condition self.sliderValue.rounded() > self.target is true if the value stored in sliderValue is at least one higher than the value stored in target. In that case, the following line of code executes:
difference = self.sliderValue.rounded() - self.target
Here, you subtract the smaller value, target, from the larger one, sliderValue.rounded(), and store the result in difference.
Notice how the variable names clearly describe what type of data they contain. Often, you’ll see code that’s harder to understand, like this:
a = b - c
It’s not immediately clear what’s happening here, except that some arithmetic is taking place. The variable names a, b and c don’t give any clues as to their purpose or what kind of data they contain. That makes it harder to maintain your code in the future.
Now, go back to the if statement. If sliderValue is equal to or less than target, the condition is untrue (or false in computer-speak) and execution will move on to the next condition:
} else if self.target > self.sliderValue.rounded() {
The same thing happens here as before, except now you’ve reversed the roles of target and sliderValue. The computer will only execute the following line when target is the greater of the two values:
difference = self.target - self.sliderValue.rounded()
This time, you subtract sliderValue.rounded() from target and store the result in the difference variable.
There is only one situation you haven’t handled yet: When sliderValue.rounded() and target are equal. If this happens, the player has put the slider exactly at the position of the target random number, a perfect score. In that case, the difference is 0:
} else {
difference = 0
}
By now, you’ve already determined that one value is not greater than the other, nor is it smaller. You can only draw one conclusion: The numbers must be equal!
Once you know the difference between the slider and the target values, calculating the number of points to award to the player is simple. It’s 100 minus the difference, and the method returns that value:
return 100 - difference
Now that you’ve reviewed everything, you’re probably eager to see the method in action!
➤ Build and run the app and play a few rounds: Move the slider, press Hit me!, and look at Xcode’s debug console to see how you scored each time:
Displaying the points
Now that pointsForCurrentRound() properly calculates the points the player earned, it’s time to display them. So next, you’ll make a change to the alert pop-up so that it displays the results of pointsForCurrentRound().
➤ Scroll up to the Button row section and change the message: parameter of the Alert so that the section looks like this:
// Button row
Button(action: {
print("Points awarded: \(self.pointsForCurrentRound())")
self.alertIsVisible = true
}) {
Text("Hit me!")
}
.alert(isPresented: self.$alertIsVisible) {
Alert(title: Text("Hello there!"),
message: Text("The slider's value is \(Int(self.sliderValue.rounded())).\n" +
"The target value is \(self.target).\n" +
"You scored \(self.pointsForCurrentRound()) points this round."),
dismissButton: .default(Text("Awesome!")))
}
Take a look at the message: parameter for the Alert view:
message: Text("The slider's value is \(Int(self.sliderValue.rounded())).\n" +
"The target value is \(self.target).\n" +
"You scored \(pointsForCurrentRound()) points this round."),
The first thing that you should notice is that you made a longer string by “adding” strings together with the + sign. Programmers call this string concatenation, and you’ll see it quite often in programming.
The next thing to note is that the first two lines end with \n, but they don’t seem to appear in the pop-up. That’s because inside a string, \n represents the newline character, which ends the current line and starts a new one. You’re using it to break a run-on string into three easy-to-read lines.
Note: In Swift, as in many other programming languages, the appearance of the
\character inside a string marks the start of an escape sequence, which is a sequence of characters that Swift should translate into another character or a sequence of characters.You use escape sequences to represent things that would be hard or impossible to represent directly inside a string.
The final thing you should notice is that each of the strings that you concatenate uses the \( and ) characters to include values from a property or method:
"The slider's value is \(Int(self.sliderValue.rounded())).\n"
"The target value is \(self.target).\n"
"You scored \(pointsForCurrentRound()) points this round."
Remember that you use the \( and ) characters for string interpolation, and you now know that the \ marks the beginning of an escape sequence. As an escape sequence, this code is not interpreted literally; it’s a signal to Swift that anything between \( and ) is first evaluated, then converted into a string, and then included along with the rest of the string.
➤ Build and run the app. Move the slider and press Hit me!, and you’ll see the new and improved alert pop-up:
Improving the code
In programming, you’ll often make changes to the code that have no effects that the user can see. These are changes to the app’s internal structure, and they’re visible only to the programmer — that is, you.
But that doesn’t mean that they aren’t worth doing. There are many reasons for making these changes, such as making the code easier to read, understand and maintain, and making it less likely that you’ll introduce bugs into the code. Programmers call the process of making these kinds of changes refactoring. That’s what you’re going to do in this section.
Using a constant to DRY your code
Take a look at the if statement inside pointsForCurrentRound():
if Int(self.sliderValue.rounded()) > self.target {
difference = Int(self.sliderValue.rounded()) - self.target
} else if self.target > Int(self.sliderValue.rounded()) {
difference = self.target - Int(self.sliderValue.rounded())
} else {
difference = 0
}
The repeated use of Int(self.sliderValue.rounded()) makes it more difficult to read. It’s also a calculation that runs four times, even though the result is the same each time. That sort of repetition is nothing to a computer, but the programmer who has to read it — and that’s probably you — has to take a moment to figure out what that code is trying to do… four times.
In case you’ve forgotten, the code takes the slider’s current value, rounds it to the nearest whole number, and then uses that result to create a new Int value.
You can improve this code by performing the calculation once and storing its result for later use. You can even give the storage place a meaningful name that makes the code easier to read. To do this, create a new variable, sliderValueRounded, to hold the result of Int(self.sliderValue.rounded()). You can then use it to make the if statement much easier to read.
➤ Make changes to pointsForCurrentRound() so that its code reads like this:
func pointsForCurrentRound() -> Int {
var sliderValueRounded = Int(self.sliderValue.rounded())
var difference: Int
if sliderValueRounded > self.target {
difference = sliderValueRounded - self.target
} else if self.target > sliderValueRounded {
difference = self.target - sliderValueRounded
} else {
difference = 0
}
return 100 - difference
}
The if statement is a lot easier to read now.
You’ve just changed code that performed a calculation that gave the same answer four times into code that performs the same calculation only once, stores its answer, and uses that answer when needed.
This is an approach that programmers call “Don’t Repeat Yourself,” or DRY.
The rationale behind DRY is that it makes code easier to read and to change, and makes it less prone to errors by reducing unnecessary redundancy. You just saw how much easier the code is to follow with the changes you made. If you decided later that you wanted to change the way that you convert the slider’s value, you’d have to do it only once with your new DRY code, instead of four times with the previous code.
➤ Build and run the app to confirm that it still works and that the changes you made aren’t visible to the player.
You may have noticed that even though the program works, Xcode has a suggestion:
“Variable ‘sliderValueRounded’ was never mutated?” Why would anyone want to mutate a variable?
Introducing let and constants
If you’re into science fiction, you probably read “mutated” and thought of it as meaning “exposed to radiation or chemicals and turned into a horrible monster.” However, in programming, “mutated” simply means “changed”.
Take a look at the if statement in pointsForCurrentRound(), where you use the variable sliderValueRounded:
if sliderValueRounded > self.target {
difference = sliderValueRounded - self.target
} else if self.target > sliderValueRounded {
difference = self.target - sliderValueRounded
} else {
difference = 0
}
In this code, sliderValueRounded is compared to self.target, subtracted from self.target, or has self.target subtracted from it. At no point does the value of sliderValueRounded ever change once it’s set. It’s not really a variable because it doesn’t vary.
Xcode is suggesting that since sliderValueRounded never changes, you should change it from a variable into a constant by using the let keyword instead of var. You use let to declare constants, which are like variables except that their values can be set only once; after that, they can’t be changed. If you try to change the value of a constant after its value is set, it causes an error.
Take Xcode’s suggestion but this time, instead of manually changing the code, make Xcode do the work for you.
➤ Click on the icon beside the suggestion, which Xcode calls “warnings”. The warning pop-up will expand and you’ll see a suggested fix: Replace var with let.
➤ Click on the Fix button to let Xcode take this action and make the fix itself. The code will now look like this:
func pointsForCurrentRound() -> Int {
let sliderValueRounded = Int(self.sliderValue.rounded())
var difference: Int
if sliderValueRounded > self.target {
difference = sliderValueRounded - self.target
} else if self.target > sliderValueRounded {
difference = self.target - sliderValueRounded
} else {
difference = 0
}
return 100 - difference
}
➤ Build and run the app again. You’ll see that it still works just like before and that no changes are noticeable to the player.
Since changing sliderValueRounded from a variable to a constant had no noticeable effect on the app, you might be asking “What was the point? Why should Xcode care if a variable never changes and suggest that I use a constant instead?”
The answer is that using a constant here helps prevent bugs. Many coding errors happen because the programmer thinks a variable holds a certain value, only to discover that some other code has changed that value.
This is true in the simple sort of programming you’re doing right now, where you only have to worry about one thing happening at a time. In parallel programming, where you have many pieces of code running at the same time, or asynchronous programming, where you have many pieces of code running independently at unpredictable times and all communicating with each other, it’s even easier for code to change data that it shouldn’t be changing.
This is why there’s a general rule that says whenever possible, use a constant instead of a variable. If you need to store the result of a calculation for later use, you’ll often find that the result never changes so you can store it as a constant.
Xcode is good at spotting opportunities to turn variables into constants, but it’s not perfect. Take another look at the complete code for pointsForCurrentRound(), paying particular attention to how difference is set:
func pointsForCurrentRound() -> Int {
let sliderValueRounded = Int(self.sliderValue.rounded())
var difference: Int
if sliderValueRounded > self.target {
difference = sliderValueRounded - self.target
} else if self.target > sliderValueRounded {
difference = self.target - sliderValueRounded
} else {
difference = 0
}
return 100 - difference
}
The computer science term for the different actions taken as the result of things like the if statement is branches. In the code above, there are three branches and in each one, difference is set to a different value. However, no matter which branch the code takes, the value of difference never changes after it’s set, so it should be turned into a constant. Remember that a constant is like a variable, except that its value can be set only once.
➤ Change the line:
var difference: Int
to:
let difference: Int
➤ Build and run the app. Once again, you’ll see that it still works, with no noticeable changes.
Another attempt to DRY some code
Since you’ve defined the sliderValueRounded constant in pointsForCurrentRound(), try using it in ContentView’s body to make it more DRY. Scroll up to the Button row section and pay particular attention to message::
// 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(self.sliderValue.rounded())).\n" +
"The target value is \(self.target).\n" +
"You scored \(pointsForCurrentRound()) points this round."),
dismissButton: .default(Text("Awesome!")))
}
That unwieldy Int(self.sliderValue.rounded()) that you got rid of in pointsForCurrentRound() is in the string that displays the slider value. Since you’ve already defined sliderValueRounded in pointsForCurrentRound(), try using it there.
➤ Change the code that defines the alert pop-up so that it looks like this:
alert(isPresented: self.$alertIsVisible) {
Alert(title: Text("Hello there!"),
message: Text("The slider's value is \(sliderValueRounded).\n" +
"The target value is \(self.target).\n" +
"You scored \(pointsForCurrentRound()) points this round."),
dismissButton: .default(Text("Awesome!")))
}
While it seems as if this change would make your code more readable, Xcode has issues with it:
“Use of unresolved identifier ‘sliderValueRounded’” is Xcode’s robotic way of saying “I have no idea of what sliderValueRounded is.”
To figure out why you are getting this warning, you’ll change your code back and then you’ll look at why you got an error.
➤ Change the code that defines the alert pop-up back to this:
alert(isPresented: self.$alertIsVisible) {
Alert(title: Text("Hello there!"),
message: Text("The slider's value is \(Int(self.sliderValue.rounded())).\n" +
"The target value is \(self.target).\n" +
"You scored \(pointsForCurrentRound()) points this round."),
dismissButton: .default(Text("Awesome!")))
}
The lives and times of variables and constants
If you look at the code in ContentView, you’ll see that there are variables and constants in two places:
- Inside
ContentViewbut outside everything else inContentView. - Inside
pointsForCurrentRound().
Take a look at the code for pointsForCurrentRound() again:
func pointsForCurrentRound() -> Int {
let sliderValueRounded = Int(self.sliderValue.rounded())
let difference: Int
if sliderValueRounded > self.target {
difference = sliderValueRounded - self.target
} else if self.target > sliderValueRounded {
difference = self.target - sliderValueRounded
} else {
difference = 0
}
return 100 - difference
}
pointsForCurrentRound() has two constants: sliderValueRounded and difference. They come into being the moment they are declared within the method (which happens at the let statement), and they vanish from existence at the end of the method. You can only refer to them after they’ve been declared, and they exist only inside pointsForCurrentRound(). This restriction would also apply if they were variables.
When you declare constants and variables inside a method, you can only refer to them within that method. This is why programmers call them local constants and variables.
Now look at the start of ContentView:
struct ContentView : View {
@State var alertIsVisible: Bool = false
@State var sliderValue: Double = 50.0
@State var target: Int = Int.random(in: 1...100)
var body: some View {
...
When the app starts, it creates an instance of ContentView. You’ll learn more about what happens when an app starts later on in this book. Upon the creation of ContentView, its variables: alertIsVisible, sliderValue, target and body also come into being. Since these variables belong to the ContentView instance, programmers call them instance variables.
Instance variables are accessible from anywhere within the object they belong to. That’s why ContentView’s methods (pointsForCurrentRound()), references (sliderValue and target) and variables (other instance variables) can reference them.
When a variable exists and you can reference it, it’s in scope. A good general rule to follow is that a variable is in scope only inside the braces where you declared it. For example:
{
var a = 30 // a is in scope here
// (More code goes here)
{
var b = a + 1 // Both a and b are in scope here
// (More code goes here)
}
// b is no longer in scope.
print("The value of a is \(a).") // a is still in scope
}
// Both a and b are no longer in scope.
In case you’d forgotten: the
//characters mark the start of a comment, which the compiler ignores. You can read about them in more detail below.
Comments
You’ve probably noticed the green text that begins with // a few times now. As I explained earlier, these are comments. You can write any text you want after the // symbol, and the compiler will ignore any text from the // to the end of the line.
// I am a comment! You can type anything here.
The best use for comment lines is to explain how your code works. You should try to write your code in a self-explanatory way, but sometimes a little extra explanation can go a long way.
Unless you have the memory of an elephant, you’ll probably have forgotten exactly how your code works when you look at it six months later; this is where comments are useful. As I’ve said before, you want to write code so that the next programmer can easily understand it, because that next programmer might be you!
There’s another style of comment that covers more than one line. Anything between the /* and */ markers is a comment:
/*
I am also a comment!
I can span multiple lines.
*/
The /* */ comments are good for longer comments. They also have another common use: temporarily disable whole sections of source code, which is helpful when you’re trying to hunt down a pesky bug.
Remember, the compiler ignores comments, so you can make it ignore one or more lines of code by putting them into a comment. This practice is known as commenting out.
Xcode makes it simple to comment out one or more lines of code. Use the Command-/ keyboard shortcut to comment/uncomment any currently-selected lines, or if you have nothing selected, the current line.
A second attempt at DRYing some code
Right now, two different places in ContentView make use of the same calculation to get the value of the slider: Round it to the nearest whole number and convert it into an Int.
You use that calculation in the part of the body instance variable that defines the alert pop-up:
Alert(title: Text("Hello there!"),
message: Text("The slider's value is \(Int(self.sliderValue.rounded())).\n" +
"The target value is \(self.target).\n" +
"You scored \(pointsForCurrentRound()) points this round."),
dismissButton: .default(Text("Awesome!")))
}
It’s also used in the pointsForCurrentRound() method:
func pointsForCurrentRound() -> Int {
let sliderValueRounded = Int(self.sliderValue.rounded())
let difference: Int
if sliderValueRounded > self.target {
difference = sliderValueRounded - self.target
} else if self.target > sliderValueRounded {
difference = self.target - sliderValueRounded
} else {
difference = 0
}
Ideally, you’d like to have a “single source of truth” for the slider’s current position as an integer value, and you’d like to make it accessible from anywhere inside ContentView.
There are a couple of ways you could make this happen. One way would be to define a new method. It would look like this (don’t type this one in; just read it):
func sliderValueRounded() -> Int {
return Int(sliderValue.rounded())
}
This new method would make a rounded, whole-number value for the slider position available from anywhere within ContentView. If a method were the only way you could do this, you’d use it.
However, there’s another way to get this value: a computed property, which is a property that acts like a method. In cases where you need a simple calculation based on a property, it’s often better to use a computed property instead of a method. We explain the differences between property types in Chapter 26 in Section 3 of the book.
➤ Add the following to the end of the User interface views part of ContentView’s Properties section, immediately after the line where you declare target:
var sliderValueRounded: Int {
Int(self.sliderValue.rounded())
}
Declaring a computed property is like declaring an ordinary property in that it begins with var or let followed by the property name and the property’s data type. The difference is that computed properties have a body which defines the value in the property. In the case of the sliderValueRounded property, the body contains Int(self.sliderValue.rounded()).
Now that you have the computed property, you can use it.
➤ Change the code that defines the alert pop-up to the following:
Alert(title: Text("Hello there!"),
message: Text("The slider's value is \(self.sliderValueRounded).\n" +
"The target value is \(self.target).\n" +
"You scored \(self.pointsForCurrentRound()) points this round."),
dismissButton: .default(Text("Awesome!")))
➤ Change pointsForCurrentRound() to the following. Since ContentView now has a sliderValueRounded property, there’s no longer a need for a sliderValueRounded constant within the method:
func pointsForCurrentRound() -> Int {
let difference: Int
if self.sliderValueRounded > self.target {
difference = self.sliderValueRounded - self.target
} else if self.target > self.sliderValueRounded {
difference = self.target - self.sliderValueRounded
} else {
difference = 0
}
return 100 - difference
}
➤ Build and run the app. It still works in the same way, but underneath, the code is now easier to read and to maintain.
Simplifying the Alert code
ContentView’s body defines the user interface; as a result, it’s big and can be unwieldy. For example, consider the code in body that generates the alert pop-up:
Alert(title: Text("Hello there!"),
message: Text("The slider's value is \(self.sliderValueRounded()).\n" +
"The target value is \(self.target).\n" +
"You scored \(pointsForCurrentRound()) points this round."),
dismissButton: .default(Text("Awesome!")))
The message: parameter has a big chunk of text in it. This is a good place to make use of a method to simplify things.
➤ Add the following method in ContentView’s Methods section, just below the pointsForCurrentRound() method:
func scoringMessage() -> String {
return "The slider's value is \(self.sliderValueRounded).\n" +
"The target value is \(self.target).\n" +
"You scored \(self.pointsForCurrentRound()) points this round."
}
You should note a couple of things about this method:
- Because this method returns a result, it has a name that describes that result.
- This method returns a
Stringinstead of anInt. Methods aren’t limited to only returning numbers; they can return all kinds of data, as you’ll see in the exercises in this book.
➤ Incorporate the new scoringMessage() method into the alert pop-uo code:
Alert(title: Text("Hello there!"),
message: Text(self.scoringMessage()),
dismissButton: .default(Text("Awesome!")))
That’s a lot cleaner.
➤ Once again, build and run the app. Again, you won’t see any noticeable changes, but you’ll feel the warm glow of satisfaction that you’ve done a good job refactoring the code.
Removing redundancies
Take a look at the Properties section of ContentView, particularly the part marked User interface views:
@State var alertIsVisible: Bool = false
@State var sliderValue: Double = 50.0
@State var target: Int = Int.random(in: 1...100)
var sliderValueRounded: Int {
Int(self.sliderValue.rounded())
}
Remove the data types from the first three properties so that the code looks like this:
@State var alertIsVisible = false
@State var sliderValue = 50.0
@State var target = Int.random(in: 1...100)
var sliderValueRounded: Int {
Int(self.sliderValue.rounded())
}
Note that Xcode doesn’t display any error messages after this change.
➤ Build and run the app to confirm that it still works, even though you’ve removed the information about those properties’ data types.
Why does the code still work? It’s because Swift is smart enough to deduce, or as we say in programming, infer, the type of a variable or constant based the value you assign to it.
Consider the first property:
@State var alertIsVisible = false
By assigning the value false to alertIsVisible, Swift infers that alertIsVisible’s data type is Bool.
Here’s the next property:
@State var sliderValue = 50.0
Assigning sliderValue with the value 50.0 — a number with a decimal point — causes Swift to infer that the variable’s data type is Double, the preferred data type for numbers with decimal points.
And then comes this property:
@State var target = Int.random(in: 1...100)
Int.random(in: 1...100) is a method that returns a random integer between 1 and 100 inclusive. By assigning it to target, Swift infers that target is an Int property.
Whenever possible, you should let Swift infer the type of variables and constants. There are times when Swift can’t do that, though. Here’s an example, taken straight from pointsForCurrentRound()
let difference: Int
if self.sliderValueRounded > self.target {
difference = self.sliderValueRounded - self.target
} else if self.target > self.sliderValueRounded {
difference = self.target - self.sliderValueRounded
} else {
difference = 0
}
return 100 - difference
difference isn’t assigned a value until after it’s declared, which means that Swift doesn’t have anything to use to infer difference’s type. In circumstances like this, Swift has to be told what data type difference is. Swift also has to be told the data type of computed properties. If you try to change the declaration of sliderValueRounded to the following…
var sliderValueRounded: {
Int(self.sliderValue.rounded())
}
…Xcode will complain quickly by throwing a very short error message: “Expected type”. This is where computed properties are more like methods; they can get so complex that Swift can’t infer their data type.
Key points
In this chapter, you added the following features to your app:
- The game now displays the target value at the top of the screen.
- It also calculates the points that the player earned based on the difference between the slider and the target values, then displays those points.
You also learned about:
- Generating pseudo-random numbers
- Algorithms
- Writing your own methods
- Making decisions with the
ifstatement - Adding strings using concatenation and representing special characters with escape sequences.
- Refactoring
- The
letstatement and constants - The scope of variables and constants
- Computed properties
You have the basic elements of a working game, but it can only play a single round right now. In the next chapter, you’ll make the game fully functional with rounds and scorekeeping. You can find the project files for the app up to this point under 04 - Outlets in the Source Code folder.