Optionals
As we stated before, Swift is a type-safe language. Apple also created Swift with the intention of keeping as many potential errors and bugs in the compilation state of development as opposed to runtime. Though Xcode has some great debugging tools, from the use of breaks, logging, and the LLDB debugger, runtime errors, particularly in games can be tough to spot, thus bringing the development process to a halt. To keep everything type-safe and as bug-free as possible during compilation, Swift deals with the concept of optionals.
Optionals, in short, are objects that potentially can be or start as nil. Nil, of course, is an object that has no reference.
In Objective-C, we could declare the following string variable for a game:
NSString *playerStatus = @"Poisoned"; playerStatus = nil;
In Swift, we would write this in the same way, but we'd find out very quickly that Xcode would give us a compiler error in doing so:
var playerStatus = "Poisoned" playerStatus = nil //error!
Even more confusing for anyone new to Swift, we'd also get an error if we did something as simple as this:
var playerStatus : String //error
Creating empty/undeclared objects in our games makes sense and is something we'd often want to do at the start of our classes. We want that flexibility to assign a value later on based on the events of our game. Swift seems to be making such a basic concept impossible to do! No worries; Xcode will inform you in most cases to suffix a question mark, ?,
at the end of these nil
objects. This is how you declare an object as an optional.
So, if we want to plan our game's properties and objects in Swift, we can do the following:
var playerStatus : String? //optional String var stageBoss : Boss? //optional Boss object
Unwrapping optionals
Let's imagine that we want to display what caused a player to lose in the game.
var causedGameOver:String? = whatKilledPlayer(enemy.recentAttack)
let text = "Player Lost Because: "
let gameOverMessage = text + causedGameOver //error
Because the string causedGameOver
is optional, Xcode will give us a compile error because we didn't unwrap the optional. To unwrap the value in an optional, we suffix an exclamation point !
at the end of the optional.
Here's our Game Over
message code, now fixed using the unwrapped optional:
var causedGameOver:String? = whatKilledPlayer(enemy.recentAttack)
let text = "Player Lost Because: "
let gameOverMessage = text + causedGameOver! //code now compiles!
We can also force unwrap optionals early at declaration to allow any potential errors to be taken care of at runtime instead of when compiling. This happens often with @IBOutlets
and @IBActions
(objects and functions linked to various storyboards and other tools that are based on menu/view tools).
@IBOutlet var titleLabel: UILabel! //label from a Storyboard var someUnwrappedOptional : GameObject! //our own unwrapped optional
Note
If possible, though it's recommended to use the basic wrapped optional ?
as much as possible to allow the compiler to find any potential errors. Using what's known as optional binding and chaining, we can do some great early logic checks on optionals that in prior languages would have involved various if
statements / control flow statements to simply check for nil objects.
Keeping code clean, safe, and easy to read is what Swift aims to do and why Swift goes out of its way sometimes to force many of these rules with optionals.
Optional binding and chaining
Optional binding is checking whether an optional has a value or not. This is done using the very handy if-let or if-var statements. Let's look back at our earlier code:
var causedGameOver:String? = whatKilledPlayer(enemy.recentAttack) let text = "Player Lost Because: " if let gotCauseOfDeath = causedGameOver { let gameOverMessage = text + gotCauseOfDeath }
The code block, if let gotCauseOfDeath = causedGameOver{…}
, does two things. First, using the key words, if let
, it automatically creates a constant named gotCauseOfDeath
and then binds it to the optional causedGameOver
. This simultaneously checks whether causedGameOver
is nil
or has a value. If it's not nil, then the if
statement's code block will run; in this case, creating the constant gameOverMessage
that combines the text
constant with gotCauseOfDeath
.
We can use if-var to simplify this even further:
let text = "Player Lost Because: " if var causedGameOver = whatKilledPlayer(enemy.recentAttack) { let message = text + causedGameOver }
The if-var statement creates a temporary variable using our previously used optional causedGameOver
and does a Boolean logic check based on the result of whatKilledPlayer(enemy.recentAttack)
. The statement is true if there's a non-nil value returned. Note how we don't have to use either wrapped (?
) or forced unwrapping (!
) of the optional in such a case.
Optional chaining is when we query down into the properties of an object using the dot operator while also doing a nil/value check as we did with optional binding. For example, let's say that we have a game where certain Enemy types can cause a player to lose instantly via an Enemy instance named currentEnemy
. In this example, currentEnemy.type
would be a string that returns the name of the kind of enemy that hit the player. Optional chaining uses the custom dot modifier ?.
while accessing a potentially nil check on a property. Here's the code to get a better idea of how this works:
if let enemyType = currentEnemy?.type { if enemyType == "OneHitKill" { player.loseLife() //run the player's lost l } }
Chances are that we'd probably not make an enemy without a designated type, but for the sake of understanding optional chaining, observe how this checks for the possible nil object that'd be returned by currentEnemy.type
using currentEnemy?.type
. Like how the dot operator functions where you can drill down the properties and properties of properties, the same can be done with the recurring ?.per
property that is drilled down. In this code, we do a Boolean comparison with ==
to see if enemyType
is the string OneHitKill
.
Don't worry if the syntax of the if
statement syntax is a bit of a mystery; next, we discuss how Swift uses if
statements, loops, and other ways we can control various object data and their functions.
Control flow in Swift
Control flow in any program is simply the order of instructions and logic in your code. Swift, like any other programming language, uses various statements and blocks of code to loop, change, and/or iterate through your objects and data. This includes blocks of code such as if
statements, for-loops, do-while loops and Switch statements. These are contained within functions, which make up larger structures like classes.
If statements
Before we move on to how Swift handles one of the main topics of OOP, functions and classes, let's quickly run through if-else statements. An if
statement checks whether a Boolean statement is true
or false
. We have the example as follows:
if player.health <= 0{ gameOver() }
This checks whether or not the player's health is less than or equal to 0
, designated by the <=
operator. Note that Swift is OK with there not being parenthesis, but we can use this if we wish or if the statement gets more complicated, as in this example:
if (player.health <=0) && (player.lives <=0){ //&& = "and" gameOver() }
Here, we check not just whether the player has lost all of their health, but also if all of their lives are gone with the and (&&
) operator. In Swift, like in other languages, we separate out the individual Boolean checks with parentheses, and like other languages, we do a logic-or check with two bar keys (||
).
Here are some more ways to write if
statements in Swift with the added key words, else-if and else, as well as how Swift can check if-not a certain statement:
//(a) if !didPlayerWin { stageLost() } //(b) if didPlayerWin { stageWon() } else { stageLost() } //(c) if (enemy == Enemy.angelType){enemy.aura = angelEffects} else if(enemy == Enemy.demonType){enemy.aura = demonEffects} else{ enemy.aura = normalEffects } //(d) if let onlinePlayerID = onlineConnection()?.packetID?.playerID { print("Connected PlayerID: /(onlinePlayerID)" } //(e) if let attack = player.attackType, power = player.power where power != 0 { hitEnemy(attack, power) } //(f) let playerPower = basePower + (isPoweredUp ? 250 : 50)
Let's look at what we put in the code:
(a)
: This checks the not / reverse of a statement with the exclamation point,!
, via!statement
.(b)
: This checks whether the player has won or not. Otherwise, thestageLost()
function is called, using the key wordelse
.(c)
: This checks if an enemy is an angel and sets its aura effect accordingly. If this is not, then it will check if it's a demon using else-if, and if that's not the case, then we catch all other instances with theelse
statement. We could have a number of else-if statements one after another, but if we start to stack too many, then using for-loops and Switch statements would be a better approach.(d)
: Using optional chaining, we create anonlineID
constant based onif
; we are able to get a non-nilplayerID
property using if-let.(e)
: This uses if-let, where optional binding became a feature in Swift 1.2. Instead of having nested if-lets and other logic checks, akin to how SQL queries are done in backend web development, we can create very compact, powerful early logic checking. In the case of example(e)
, we have an enemy receive an attack based on what type of attack it is and the power of the player.(f)
: This is an example of combining the creation of a constant with the keywordlet
and doing a shorthand version of anif
statement. We shorthen anif
statement in Swift with the question mark?
and colon:
. Here is the format for short handing anif
statement:bool ? trueResult : falseResult
. IfisPoweredUp
istrue
, thenplayerPower
will equalbasepower + 250
; iffalse
, then it'sbasepower + 50
.
For loops
We touched on for-in loops before dealing with collections. Here again is a for-in loop in Swift that will iterate through a collection object:
for itemName in inventory.values { print("Item name: \(itemName)") }
For some of us programmers who are used to the older way of using for-loops, don't worry, Swift lets us write for-loops in the C-style, which many of us are probably used to:
for var index = 0; index < 3; ++index { print("index is \(index)") }
Here's another way of using a for-loop without using an index variable, noted with the underscore character _
but of course using a Range<Int>
object type to determine how many times the for-loop iterates:
let limit = 10 var someNumber = 1 for _ in 1...limit { someNumber *= 2 }
Note the …
between the 1
and limit
. This means that this for-in loop will iterate from 1-10. If we wanted it to iterate from 0
to limit-1
(similar to iterating between the bounds of an array's index), we could have instead typed 0..<limit
where limit
is equal to the array's .count
property.
Do-while loops
Another very common iteration loop in programming is the do-while loop. Many times we can just utilize the while portion of this logic, so let's look into how and why we might use a while loop:
let score = player.score var scoreCountNum = 0 while scoreCountNum < score { HUD.scoreText = String(scoreCountNum) scoreCountNum = scoreCountNum * 2 }
In game development, one use of the while loop (though executed differently in a game app, this accommodates iterating once per frame) is for displaying the counting up of a player's score from 0 to the score the player reached—a common esthetic of many games at the end of a stage. This while loop will iterate until it reaches the player's score, displaying on HUD object showing the intermediate values up until that score.
A do-while loop is practically the same as the while-loop with the extra caveat of iterating through the code block at least once. The end-stage score count example can also illustrate why we would need such a loop. For example, let's imagine that the player did really bad and got no score when the stage ended. In the while loop given, a score of zero won't let us enter the block of code in the while loop since it doesn't fulfill the logic check of scoreCountNum < score
. In the while loop, we also have code that displays the score text. Though maybe embarrassing to the player, we would want to count up to the score and more importantly, still display a score. Here's the same code done with a do-while loop:
let score = player.score var scoreCountNum = 0 do { HUD.scoreText = String(scoreCountNum) scoreCountNum = scoreCountNum * 2 } while scoreCountNum < score
Now score text will display even if the player scored nothing.
Switch statements
Switch statements are useful when we wish to check many different conditions of an object in a fully encompassing and neat way without having a wall of else-if statements. Here's a code snippet from the game PikiPop that uses a Switch statement from the game, PikiPop, that sets the percentage a GameCenter
achievement (in this case, a 6x combo) based on the number of times the combo was achieved by the player. Don't worry too much about the GameCenter
code (used with the GCHelper
singleton object); that's something we will go over in future chapters when we make games in SpriteKit and SceneKit.
switch (comboX6_counter) { case 2: GCHelper.sharedInstance.reportAchievementIdentifier("Piki_ComboX6", percent: 25) break case 5: GCHelper.sharedInstance.reportAchievementIdentifier("Piki_ComboX6", percent: 50) break case 10: GCHelper.sharedInstance.reportAchievementIdentifier("Piki_ComboX6", percent: 100) default: break }
The switch statement here takes the variable used to count how many times the player hit a 6X combo, comboX6_counter
, and performs different tasks based on the value of comboX6_counter
. For example, when the player has done a 6X Combo twice, the Piki_ComboX6 achievement gets 25% fulfilled. The player gets the achievement (when at 100%) when the counter hits 10. The purpose of the keyword break
is to tell the loop to exit at that point; otherwise, the next case block will iterate. Sometimes, this might be desired by your game's logic, but keep in mind that Swift, like many other languages, will continue through the switch statement without break
. The keyword default
is the catch-all block and is called when the value of the item checked by the switch statement is anything but the various cases. It can be thought of as an equivalent to the else{}
block, while all of the cases are similar to else if(){}
. The difference though is that Swift requires all cases of the switch be handled. So, though we can suffice with an if
without an else
, we have to have a default case for a switch statement. Again, this is done to keep Swift code safe and clean earlier in the coding process.
Functions and classes
Up until this point, we have kept from discussing probably the most important aspects of Swift or any OOP languages for that matter—how the language handles functions on objects and how it organizes these objects, object properties, and functions and performs various object-oriented design concepts, such as polymorphism and inheritance with classes, Structs, enums, protocols, and other data structures. There is much more to discuss about how Swift utilizes these concepts, more than we can fit in this chapter but throughout the course of this book, especially as we get into how to use Apple's game-centric SpriteKit and SceneKit frameworks, we will flesh out more on these topics.
Functions
In Objective-C, functions are written the following way:
-(int) getPlayerHealth() { return player.health; }
This is a simple function that returns the player's health as an integer—the Int
equivalent in Objective-C.
The structure of the function/method is as follows in Objective-C:
- (return_type) method_name:( argumentType1 )argumentName1 joiningArgument2:( argumentType2 )argumentName2 ... joiningArgumentN:( argumentTypeN )argumentNameN { function body }
Here's the same function in Swift:
func getPlayerHealth() -> Int { return player.health } //How we'd use the function var currentHealth : Int = 0 currentHealth = getPlayerHealth()
This is how a function is structured in Swift:
func function_name(argumentName1 : argumentType1, argumentName2 : argumentType2, argumentNameN : argumentTypeN) -> return_type { function body }
Note how we use the keyword func
to create a function and how the argument/parameter names are first with the types second, separated by the colon (:
) and within parenthesis.
Here's what a typical void function looks like in Swift. A void-type function is a function that doesn't return a value.
//with a Player type as a parameter func displayPlayerName (player:Player){ print(player.name) } //without any parameters; using a class property func displayPlayerName(){ print(currentPlayer.name) }
In a void function, there's no need to write ->returnType
, but even if there are no parameters, we do have to put in the ()
parenthesis at the end of the function name.