This workshop is about learning how to code in Swift, using Xcode Playgrounds.
Topics | Details |
---|---|
Datatypes | declarations + basic datatypes |
Control flow | what is it + what is it used for |
Functions | calling + writing functions |
Playgrounds | how to play around with Swift code |
You can download a Swift Playgrounds version of this workshop here.
This will be a quick introduction into how to write Swift code.
- how to create and modify variables
- how to write control flow
- how to create functions
- Datatypes
- Declarations
- Integers
- Doubles
- Booleans
- Strings
- Arrays
- Sets
- Dictionaries
- Control Flow
- Conditional Statements
- Switch
- Loops
- Functions
First let's open a Playground in Xcode. Playgrounds is a development environment for Swift created by Apple. It allows you to write code and see it in action immediately.
You can open a Playground by opening Xcode and selecting the open Get started with a playground option in the Welcome to Xcode window (if it doesn't show up, press cmd + shift + 1 to bring it up).
In the window that appears, select a blank iOS template, give your playground a name, and then press next.
If you want more information on how to do this, check out this online tutorial: Get Started With Xcode Playgrounds.
You should see a window like this. Delete the text inside the playground, let's start with a clean slate.
Let's write your first line of Swift code.
print("hello world")
Press shift + enter to run this line, or press the Run button in the line numbers or at the bottom left of the code editor.
You should see the result on the right side of the code editor. and at the bottom in the console panel.
Congrats you have now written your first line of Swift code!
Now that we have our playground setup, let's talk about Swift.
Swift, like any other programming language uses datatypes and variables. Swift is a type-safe language, which means that it helps you be clear about which types of values your code works with. This ensures that you don't pass the wrong types of values by mistake, helping you catch errors quicker in the development process.
A datatype is a particular type of data item, defined by:
- the values it can take
- the operations that can be performed by it
A variable is an instance of a datatype, often given a descriptive name.
Datatypes and variables are the tools used by programmers to solve larger problems, and in order to know which tools is best for a given problem, you need to understand the strengths and weaknesses of each datatype.
In this workshop we will only cover a few of the basic datatypes in Swift such as:
Int
(integer)Double
(decimal number)Bool
(true / false)String
(text)Array
(ordered list)Dictionary
(list with key + value pairs)
For more informations read The Basics, from the Swift Organization.
In order to create a datatype we must declare it. If we don't declare it and try to use it, Xcode will complain telling you Use of unresolved identifier 'x' (where x is the variable name).
This — by the way — is called a compilation error.
In order to avoid this error, we have two ways of declaring a variable:
let
(constant)var
(variable)
You can define a constant variable such as pi by typing the following:
let pi = 3.1415926
Let's use pi to compute the Earth's orbital path around the sun (approx.). The average distance between the Earth and the Sun is about 149.6 million km according to Google, so let's use that.
let distance = 149_600_000.0 // 149.6 million km
let circumference = 2 * pi * distance
print("Earth travels about \(circumference) km every year")
You might have noticed two interesting things about Swift in that example:
- You can add
_
to numbers as separators to make it more readable.- e.g.
100_000_000
as opposed to100000000
- e.g.
- You can print variable values using String Interpolation.
- e.g.
"x has the value: \(x)"
- e.g.
Additionally we added the .0
to the distance to make it easier to multiply it with pi when both the same datatype (Double
). If one was a whole number and the other a decimal number, Xcode would have complained. We talk about this later.
Using the let
keyword allows us to create constant variables, but what if we want to change the distance so that instead of the Earth, we wanted to know Mercury's orbit?
Add these two lines below our code to calculate the Earth's orbit:
distance = 57_910_000.0 // 57.91 million km
circumference = 2 * pi * distance
print("Mercury travels about \(circumference) km every year")
Xcode doesn't like that, does it?
See we told Xcode that we would never change the values of distance
and circumference
but here we are trying to change it. We lied to Xcode.
It's ok, Xcode will forgive us; Xcode even offers to fix our problem for us. If you notice on the right side of the lines where we are trying to change the values of distance
and circumference
, there is a message in red: Cannot assign to value: 'distance' is a 'let' constant.
If you click on this message, it will expand with a small message Change 'let' to make it mutable and a Fix button (Mutable means it can change; not a constant). If you press the Fix button Xcode will change the let
declaration to a var
declaration.
Do this for both the distance
and circumference
variables and run your code.
You should see both of the orbital distances travelled by the Earth and Mercury printed to the console.
Optionals are used in Swift where a variable's value may be absent (nil
).
Either:
- there is a value, and you have to unwrap the optional
- or, there is is no value at all
The Int()
initializer returns an Int?
instead of Int
because it might fail to convert the String
to a numeric value.
var twentyThree = Int("23") // -> 23
var five = Int("five") // -> nil
An initializer is used to create an instance of a datatype (otherwise known as constructors in other languages)
In Swift, you can set an optional value to nil, but you cannot do this with non-optional values. By default optional variables are set to nil
twentyThree = nil
var optionalValue: Int? // -> nil
Unwrapping an optional means checking the value inside an optional variable. You can check if an optional has a value using an if
statement (these are explained in Conditional Statements) to check if the value is nil
.
You can then use the !
operator to force unwrap the value contained in the optional. If you use the !
you must be sure that the optional contains a non-nil value, otherwise your program will crash, and you will be unhappy.
optionalValue = Int("20")
if optionalValue != nil {
print("the optional has a value of \(optionalValue!)")
}
// the optional has a value of 20
Another way of checking if an optional has a non-nil value, is to use optional binding:
if let value = optionalValue {
print("the optional still has a value of \(value)")
}
// the optional still has a value of 20
Read more about optionals in The Basics in the Swift Book.
Now that we know how to declare constants and variables, let's get back to datatypes.
In Swift we can represent integers using the Int
datatype. Integers can be positive or negative, but they don't have any decimal values.
You can declare integers in a couple of different ways:
- You can either let Xcode infer that the number is an integer — by the absence of decimals.
- You can explicitly tell Xcode the datatype
- either by writing
:Int
after the variable name. - or by assigning its value to
Int(someNumber)
and convertingsomeNumber
to an integer.
- either by writing
let x = 1 // implicit
let y: Int = -2 // explicit
let z = Int(100.0) // explicit
In Swift we can represent decimal numbers — properly known as floating-point numbers — using the Double
datatype. Floating-point numbers are numbers that have fractional components, colloquially referred to as as decimal values.
let p = 1.0 // implicit
let q: Double = -2 // explicit
let r = Double (99) // explicit
You may have noticed that Xcode does not implicitly converts numbers from type Int
into Double
.
Try this code:
let aDouble = 5.0 // this is a Double
let anInteger: Int = aDouble
// cannot implicitly convert Double to Int
Or the opposite
let someInteger = 5 // this is an Int
let someDouble: Double = someInteger
// cannot implicitly convert Int to Double
Xcode in both cases will complain with something along the lines of: Cannot convert value of type 'Double' to specified type 'Int'.
How do we fix this? The simple solution is to cast the value to the desired datatype using either Int()
or Double()
let myDouble = 5.0
let myInteger: Int = Int(myDouble) // explicit conversion
let yourInteger = 5
let yourDouble: Double = Double(yourInteger)
In Swift we can represent boolean values using the Bool
datatype. A boolean value can only true
or false
.
let schoolIsFun = false
var codingIsFun = true // only when I'm not debugging
let bool: Bool = true
let piGreaterThanFive = pi > 5
if piGreaterThanFive {
print("Physics is broken!?")
}
// Physics is broken!?
You can negate (invert) a boolean value using the negation !
operator.
if !piGreaterThanFive {
print("Physics is fine")
}
// Physics is fine
You can see above that Since a logical operation (pi > 5)
results in a boolean value, you can set a Bool
equal to its result as shown above.
In Swift we can represent text using the String
datatype. A String
is a series of characters — a string of characters.
let aString = "this is a string"
let multiLine = """
this is a multiline string
you can include multiple lines of text
- all in the same string
"""
let aMultiLine = """
this is a string
"""
// aString and aMultiLine are both the same
Sometimes you want to include special characters such as quotations in your strings; this is called escaping, and you do this by using a backslash \
before that character you want to escape. You will have to do this for each character you want to escape.
let escape = "I shouted \"FREEDOM\" as I jumped out the window!"
You can create empty strings — to be filled in later by user input or online data — in two ways (source: Initializing an Empty String):
var emptyString = "" // empty string literal
var anotherEmptyString = String() // initializer syntax
// these two strings are both empty, and are equivalent to each other
And to check if a string is empty you can use the .isEmpty
method.
if emptyString.isEmpty {
print("Nothing to see here")
}
// Nothing to see here
You can add / concatenate strings together which allows you to build longer and more complex strings, such as a greeting.
let firstName = "Elon"
let message = "Hello there " + firstName + "!"
print(message)
// prints "Hello there Elon!"
You can also append to the end of a string using either the .append
method or with the +=
operator.
message.append(" Welcome")
message += "!"
print(message)
// prints "Hello there Elon! Welcome!"
Sometimes, instead of adding strings it makes more sense to insert a value into a string — this value can also be a string, but it could be an integer or another datatype.
let newMessage = "Hello there \(firstName)!"
print(newMessage)
Read more about Strings and Characters.
In Swift we can represent a collection of values using the Array
datatype. An Array
is an ordered list of values. These values must all be of the same type, otherwise Xcode will complain — or worse you'll get a runtime error.
var intArray = [1, 2, 3]
var emptyArray = [Int]() // this is shorthand for Array<Int>()
print("not empty: \(intArray)")
print("empty: \(emptyArray)")
/*
not empty: [1, 2, 3]
empty: []
*/
You can get the number of items in an array using the .count
method.
print("intArray has \(intArray.count) items")
You can append elements to an array using either the .append
method or with the +=
operator, just as with String
.
intArray.append(4)
intArray += [5]
intArray += [6, 7]
print(intArray)
// [1, 2, 3, 4, 5, 6, 7]
You can also access specific elements in an Array
using the subscript []
operator.
print("the first element in the array is \(intArray[0])")
print("the second element in the array is \(intArray[1])")
You can iterate across every element in an array using a for-in
loop:
for item in intArray {
print(item)
}
/*
1
2
3
4
5
6
7
*/
If you need the index, you can also use the .enumerated
method as shown, since that method returns tuples of (index, value).
for (index, value) in intArray.enumerated() {
print("index \(index): \(value)")
}
/*
index 0: 1
index 1: 2
index 2: 3
index 3: 4
index 4: 5
index 5: 6
index 6: 7
*/
Read more about Collection Types in the Swift Book.
Another way of representing a collection of values in Swift is by using a Set
. You can use a Set
when the order of the elements doesn't matter, or when you want to make sure there are no duplicate items in the list.
var intSet: Set<Int> = [1, 2, 3, 2]
intSet.update(with: 7) // adding to the set
print("\n# printing all set elements [unsorted] \n")
for item in intSet {
print("unsorted: \(item)")
}
/*
# printing all set elements [unsorted]
unsorted: 3
unsorted: 1
unsorted: 2
*/
If you want to go over the set elements in a specific order you can use the .sorted
method.
print("\n# printing all set elements [sorted] \n")
for item in intSet.sorted() {
print("sorted: \(item)")
}
/*
# printing all set elements [sorted]
sorted: 1
sorted: 2
sorted: 3
*/
- Think of any array as a bookshelf where each item is next to each other (ordered).
- good for reading
- A set is a bag of items where it doesn't matter where each item is.
- good for inserting
Read more about Collection Types.
In Swift we can use the Dictionary
datatype to organize key-value pairs. It functions like a set, but each value has a key that can be used to retrieve the value.
var students: [Int: String] = [:] // [Key: Value]
// empty dictionary
students[101] = "Jeremy Smith"
students[102] = "Carlos Gutierrez"
students[103] = "Emma Martin"
print("\n# printing all students \n")
for student in students {
print("\(student.key): \(student.value)")
}
/*
# printing all students
103: Emma Martin
101: Jeremy Smith
102: Carlos Gutierrez
*/
Notice that the items in a dictionary are also unordered like in a set. If you want to go over the items in a specific order, use the .sorted
method like with Set
.
print("\n# printing all students [sorted] \n")
for (key, value) in students.sorted(by: <) {
print("\(key): \(value)")
}
You can update dictionary values using the .updateValue
method.
if let oldValue = students.updateValue("Emma Rae Martin", forKey: 103) {
print("The old value for student 103 was \(oldValue).")
print("The new value for student 103 is \(students[103]!).")
}
Read more about Collection Types.
Now that we've learned about different datatypes and how to create and use them, we now need to learn about another fundamental building block of programming: Control Flow.
Control flow allows us to create different actions and combinations of actions based on certain conditions.
You might have already seen some control flow in the examples we've used, but now we're going to go over them in-depth.
"Swift provides a variety of control flow statements. These include while loops to perform a task multiple times; if, guard, and switch statements to execute different branches of code based on certain conditions; and statements such as break and continue to transfer the flow of execution to another point in your code" - Swift Book
- Conditional Statements
- Switches
- Loops
Let's say you want to create a simple program to send a welcome message to every student at the school. We already have our dictionary with student numbers matched to a student name, but let's add a few empty student numbers reserved for future students.
var students = [
101: "Jeremy Smith",
102: "Carlos Gutierrez",
103: "Emma Martin",
104: "",
105: "",
106: "",
]
print("\n# greetings [attempt]\n")
for student in students.sorted(by: <) {
print("Hello, welcome to school, \(student.value)!")
}
/*
# greetings [attempt]
Hello, welcome to school, Jeremy Smith!
Hello, welcome to school, Carlos Gutierrez!
Hello, welcome to school, Emma Martin!
Hello, welcome to school, !
Hello, welcome to school, !
Hello, welcome to school, !
*/
You'll notice that we now have a bunch of greetings with no name. How could we make it so that the greeting is only printed if that student number has a name?
Control flow *cough cough*
The simplest solution to our problem is to check if the string (value) associated with a key is empty, and if it isn't, then we print our greeting message.
We can do this with an if
statement.
print("\n# greetings [correct]\n")
for student in students.sorted(by: <) {
if !student.value.isEmpty { // if the string is not empty
print("Hello, welcome to school, \(student.value)!")
}
}
/*
# greetings [correct]
Hello, welcome to school, Jeremy Smith!
Hello, welcome to school, Carlos Gutierrez!
Hello, welcome to school, Emma Martin!
*/
As you can see, an if
loop allows us to create specific actions based on certain conditions or variables. In this example we used the if
code block at it's most basic: just a simple check for a condition, and some code that is executed if that condition is true.
You can use the let
in the condition of an if
statement to find out if an optional contains a value, or if a conversion is valid (such as from String
to Int
). This is called optional binding.
let randomInput = [ "123", "five" ]
let randomIndex = Int.random(in: 0...1)
var perhapsNumber = randomInput[randomIndex]
if let number = Int(perhapsNumber) {
print("'\(perhapsNumber)' is a valid integer: \(number)")
}
else {
print("'\(perhapsNumber)' is not a valid integer")
}
In this example the if the optional Int?
returned by Int()
is an integer value, that value is used to create a new constant number
.
We can spice up the if
statement with else
and else if
for additional branches of execution.
let temperature = -32
if temperature >= 25 {
print("it's quite hot, make sure to stay hydrated!")
} else if temperature <= -20 {
print("it's quite cold, make sure to bundle up!")
} else {
print("it's nice outside")
}
// it's quite cold, make sure to bundle up!
Using else if
allows us to add a condition that is only checked if the preceding conditions don't match. Since temperature is less than -20, a message to bundle up was printed.
The final else
statement is optional, and can be removed if no default case is needed.
let temperature = 15
if temperature >= 25 {
print("it's quite hot, make sure to stay hydrated!")
} else if temperature <= -20 {
print("it's quite cold, make sure to bundle up!")
}
// prints nothing
In this case, nothing is printed since the temperature is neither hot nor cold enough to trigger either the if
or else if
conditions.
To create more complex conditions we can use the !
(negation), the &&
(AND), ||
(OR) logical operators.
The negation operator will invert the value of a boolean:
- true -> false
- false -> true
The value !allowedEntry
can be read as 'not allowedEntry
', as shown below.
let allowedEntry = false
if !allowedEntry {
print("ACCESS DENIED")
}
// ACCESS DENIED
The logical AND operator is an expression that is true if both boolean values are true. If one is false then the overall expression is false.
In the following example, access is only granted if both enteredDoorCode
and passedRetinaScan
are both true.
let enteredDoorCode = true
let passedRetinaScan = false
if enteredDoorCode && passedRetinaScan {
print("Welcome!")
} else {
print("ACCESS DENIED")
}
// ACCESS DENIED
The logical OR operator is an expression that is true if just one boolean value is true. If one is true then the overall expression is true.
In the example below, if either hasDoorKey
or knowsOverridePassword
is true (or both are true), then the access would be granted.
Only if both hasDoorKey
or knowsOverridePassword
are false, then access would be granted.
let hasDoorKey = false
let knowsOverridePassword = true
if hasDoorKey || knowsOverridePassword {
print("Welcome!")
} else {
print("ACCESS DENIED")
}
// Welcome!
You can find these examples and more information in Basic Operators in the Swift Book.
What if you have a bunch of if
cases for the same variable?
Like this...
let value = "M"
if value == "k" {
print("\(value) is the same as 10^3")
} else if value == "M" {
print("\(value) is the same as 10^6")
} else if value == "G" {
print("\(value) is the same as 10^9")
} else {
print("value doesn't match any case")
}
// M is the same as 10^6
The switch
code block below is identical to the if
code block above, but it is both quicker to write and easier to read.
switch value {
case "k":
print("\(value) is the same as 10^3")
case "M":
print("\(value) is the same as 10^6")
case "G":
print("\(value) is the same as 10^9")
default:
print("value doesn't match any case")
}
// M is the same as 10^6
You can add multiple conditions to one case, for example:
let value = "m"
switch value {
case "k":
print("\(value) is the same as 10^3")
case "M":
print("\(value) is the same as 10^6")
case "G":
print("\(value) is the same as 10^9")
case "c", "m", "u":
print("\(value) is too small")
default:
print("value doesn't match any case")
}
// m is too small
More infomation about Switch Statements.
In addition to if
and switch
statements, we can loop over code using while
, for-in
statements.
for-in
statements are quite useful for iterating over element in a collection, or perform the same task, as we've done several times in this workshop.
var students = [
101: "Jeremy Smith",
102: "Carlos Gutierrez",
103: "Emma Martin",
]
print("\n# regular for-in loop\n")
for student in students {
print("\(student.key): \(student.value)")
}
/*
# regular for-in loop
103: Emma Martin
101: Jeremy Smith
102: Carlos Gutierrez
*/
A different version of this for-in
statement uses a (key, value) tuple:
print("\n# for-in loop using tuple\n")
for (number, name) in students {
print("\(number): \(name)")
}
/*
# for-in loop using tuple
103: Emma Martin
101: Jeremy Smith
102: Carlos Gutierrez
*/
We can also do for-in
with numeric ranges, shown with the three-times table:
for i in 1...5 {
print("3 x \(i) = \(3 * i)")
}
// this is the same as above
// since ..< excludes the final value
for i in 1..<6 {
print("3 x \(i) = \(3 * i)")
}
A while
loop is similar to a for-in
loop, but it will keep executing until a certain condition is not met.
var number = 0
while number != 5 {
print("\(number) is not five")
number = Int.random(in: 0...10)
}
print("\(number) is five!")
Here each time the code is run, it will loop a different amount of times, since only when number is 5 will the condition number != 5
be false
, thus exiting out of the loop.
Using while
and for-in
loops to repeat code execution is super common, especially when you want to perform an action on multiple collection items.
- But what happens when you want to ignore some elements?
- Or if you want to stop looping once you've found a specific item?
In these cases we use either the continue
or break
keywords.
You can use the continue
keyword when you want to finish a loop iteration, but you don't want to stop looping altogether. The code will continue executing at the start of the next loop.
let children = [
"Sam": "bad",
"Peter": "good",
"Joanna": "good",
"Ben": "bad"
]
print("\n# all the good children...\n")
for child in children {
if child.value == "bad" {
continue // ignore naughty children
}
print("\(child.key) is a good child")
}
/*
# all the good children...
Peter is a good child
Joanna is a good child
*/
In this example, if a child was bad, we ignored them but we still kept looping through all the children.
What if you want to stop looping entirely?
You can use the break
keyword to exit the while
or for-in
statement altogether.
let lotteryTickets: Set<String> = [
"10209",
"87123",
"12334",
"98232",
"12353",
]
let winningTicket = "12334"
print("\n# playing the lottery...\n")
for numbers in lotteryTickets {
if numbers == winningTicket {
print("You won the lottery!")
break
}
}
/*
# playing the lottery...
checking 87123...
checking 10209...
checking 98232...
checking 12353...
checking 12334...
You won the lottery!
*/
In this example, as soon as we found the winning lottery, we stop looking and we stop looping altogether.
Functions (methods) allow programmers to encapsulate code and call on it when it's needed instead of copying and pasting it several times across an application. This means that if you find a bug or you need to update the code, you only do so once, and not in every place you use the code.
DRY:
Don't Repeat Yourself
- Functions are self-contained chunks of code that perform a specific task. You give a function a name that identifies what it does, and this name is used to “call” the function to perform its task when needed. — Swift Book:
A function is defined by using these three concepts:
- Parameters: typed input variables (optional)
- Return Type: type of value passed back as output (optional)
- Function Name: name that describes the task the function performs
Function definitions must be unique to a function, otherwise you will get an error that you are redefining an existing function. As long as the parameters or return type is different, then you can have multiple functions with the same name.
When we use a function, we "call" the function by using the function name and passing any input values — arguments — that match the function's parameters.
Below we have an example function that greets a person when given their name. It contains one input parameter called person
defined as a String
. The output return value is also of type String
which will contain the greeting message.
/**
Simple greeting function.
- parameters: person: the person's name to be greeted
*/
func greet(person: String) -> String {
let greeting = "Hello, " + person + "!"
return greeting
}
/*
Hello, Anna!
Hello, Brian!
*/
You might have noticed the markup used before the function defintion. This is called documentation and it is very important when you are working with other developers so that they know what your function is, what it does, and exactly what each parameter is.
You can see your function documentation by clicking on the function name and holding the option key.
We call our greet()
function by writing the function name and passing "Anna"
as the person
parameter. We can store the return value in variable like message so we can then print it later.
Alternatively, since print()
itself is a a function that takes an unnamed String
parameter, we can pass the greet()
function as its argument; this allows us to print the greeting directly without storing it's result in an extra variable.
let message = greet(person: "Anna")
print(message)
print(greet(person: "Brian"))
This function can be simplified, by combining the message creation and the return statement into one line.
func greetAgain(person: String) -> String {
return "Hello again, " + person + "!"
}
print(greetAgain(person: "Anna"))
// Hello again, Anna!
You have a lot of freedom in how you define parameters and return values when using Swift. You can create very simple utility functions or something more complex to suit your needs.
You can define functions that have no input parameters, such as this example:
func sayHelloWorld() -> String {
return "hello world"
}
print(sayHelloWorld())
// hello world
You can also define functions that have many parameters for more complex logic. The first parameter can be left unnamed — by using the underscore '_
' character — but all parameters after that must be named.
Here we are defining a second function named greet()
but here it has two parameters which makes it unique from the earlier greet()
function.
func greet(person: String, alreadyGreeted: Bool) -> String {
if alreadyGreeted {
return greetAgain(person: person)
} // implicit else
return greet(person: person)
}
print(greet(person: "Anna", alreadyGreeted: true))
print(greet(person: "Jenny", alreadyGreeted: false))
/*
Hello again, Brian!
Hello, Jenny!
*/
If you don't need to reuse the result of your function, it might make more sense to not have a return value for your function. This is common with functions that print to the console, or otherwise something to the user.
The return type and the ->
is then optional if the return type is Void
.
func greeting(person: String) -> Void {
print("Greetings, \(person)!")
}
greeting(person: "Dave")
// Greetings, Dave!
Parameters each have a label and a name:
- the label is what you see when you are calling the function
- the name is the variable name inside the function
By default, the parameter name becomes the label, but you can override this by defining your own label.
In this example the person
parameter label and name are the same, but the hometown
parameter is labelled from
.
/**
Slightly more complex greeting function.
- parameters:
- person: the person's name to be greeted
*/
func greet(person: String, from hometown: String) {
print("Hello \(person)! Glad you could visit from \(hometown).")
}
greet(person: "Tim", from: "Cupertino")
// Hello Tim! Glad you could visit from Cupertino.
If you want to learn more about functions in Swift, read the Functions page in the Swift Book — many of these examples come directly from there.
Now that we have a firm basic understanding of how to write Swift code, let's use it to build something cool.
What's a cooler thing to make than a revolutionary app with over a million downloads within it's first week?
Your first tic tac toe app in playgrounds, that's what!
I've already built a skeleton for this tic tac toe app to help you get started, including the labels and buttons. We're just going to add a few functions to finish up the game.
- Add a function that is triggered when you play a move.
- Add a function that checks if a winning move has been played.
- Add a function that updates the game board
The function definitions are shown below. We will implement later.
/**
Action triggered when any game board button is pressed
- parameter sender: A reference to the button that was pressed
*/
func buttonPressed(sender: UIButton!) {}
/**
Checks game board and checks if any winning moves were made.
- returns: True if a winning move was found in `board`
*/
func winner() -> Bool { return false }
/**
Updates `board` with the last move
- parameter button: button ID for button pressed (1...9)
*/
func updateBoard(button: Int) {}
A simple tic tac toe game implemented in Swift Playgrounds.
This bit of code imports tools so we can create our tic tac toe game in Swift Playgrounds.
- UIKit is the UI framework for iOS applications.
- Playground Support allows us to display and interact with live views in playgrounds.
import UIKit
import PlaygroundSupport
These are values that are globally accessible, that means you can use them anywhere in your program. Usually you want to avoid using these since it can create unforeseen problems in a large project and can be quite difficult to debug.
We are using them here since we're just starting out and our program is small.
let background = UIColor(red:0.04, green:0.04, blue:0.03, alpha:1.0)
let lavaRed = UIColor(red:0.78, green:0.06, blue:0.18, alpha:1.0)
let mustard = UIColor(red:1.00, green:0.82, blue:0.32, alpha:1.0)
let navajo = UIColor(red:1.00, green:0.90, blue:0.63, alpha:1.0)
View controllers are a super important concept in iOS development as every single app has at least one view controller, and often more than one. They manage your user interface and it's interactions with the underlying data.
Each view controller manages a top level view which contains smaller subviews / UI element, but it does not necessary manage the subviews — here in our simple example, it does.
Read more about view controllers in Apple's programming guide
class MyViewController : UIViewController {
Class properties are values that are associated with a class. Just like a car has four wheels (each wheel is a property of a car), our tic tac toe game has 9 buttons (one for each position on the board). Our game view also has two text labels: a title and a subtitle. The title will just simply say the name of the game, and the subtitle will give current player's turn or the winner.
/*
Board values
- initial state value: 0
- player x state value: 1
- player o state value: -1
*/
var board = [0,0,0, 0,0,0, 0,0,0]
var turnCount = 0
var turnX = true
var button1: UIButton!
var button2: UIButton!
var button3: UIButton!
var button4: UIButton!
var button5: UIButton!
var button6: UIButton!
var button7: UIButton!
var button8: UIButton!
var button9: UIButton!
var label: UILabel!
var subtitle: UILabel!
You may have noticed that the button variables are all of type UIButton!
which should look similar to optionals where we used ?
after a datatype like UIButton?
.
Using the !
operator instead of ?
still makes the variable an optional, but instead it means we don't have to explicitly unwrap it every time we use it; this is called an implicitly unwrapped optional.
We are using it here because we want to have class-wide access to the buttons, but we only want to create them once the view is loaded. For more info, here is an article explaining implicitly unwrapped optionals: What are implicitly unwrapped optionals?.
This function is what actually loads all of our labels and buttons into the playground live view. Here we create our UIView
and we change its background colour. We setup the labels and buttons, adding them to the view, and then we give the view to the view controller.
setupLabel(with: view)
and setupButtons(with: view)
were created to make this a bit easier; they put all the buttons and labels in the right place and setting them to their initial values, they are defined below in this class.
override func loadView() {
let view = UIView() // default view size: (375.0, 668.0)
view.backgroundColor = background
setupLabels(with: view)
setupButtons(with: view)
self.view = view
}
/**
Creates and adds title and subtitle labels to the view passed.
- parameter view: View created in `loadView()`
*/
@objc func setupLabels(with view: UIView) {
// label (title) //
label = UILabel()
label.frame = CGRect(x: 187.5-60, y: 100, width: 200, height: 60)
label.text = "Tic Tac Toe"
label.font = UIFont(name: "Helvetica", size: 24)
label.textColor = .white
// subtitle //
subtitle = UILabel()
subtitle.frame = CGRect(x: 187.5-30, y: 140, width: 200, height: 60)
subtitle.text = "X's turn"
subtitle.font = UIFont(name: "Helvetica", size: 18)
subtitle.textColor = .gray
view.addSubview(label)
view.addSubview(subtitle)
}
/**
Creates and adds all 9 buttons for tic tac toe game board.
- parameter view: View created in `loadView()`
*/
@objc func setupButtons(with view: UIView) {
button1 = UIButton()
button2 = UIButton()
button3 = UIButton()
button4 = UIButton()
button5 = UIButton()
button6 = UIButton()
button7 = UIButton()
button8 = UIButton()
button9 = UIButton()
button1.tag = 1
button2.tag = 2
button3.tag = 3
button4.tag = 4
button5.tag = 5
button6.tag = 6
button7.tag = 7
button8.tag = 8
button9.tag = 9
button1.setTitle("#", for: .normal)
button2.setTitle("#", for: .normal)
button3.setTitle("#", for: .normal)
button4.setTitle("#", for: .normal)
button5.setTitle("#", for: .normal)
button6.setTitle("#", for: .normal)
button7.setTitle("#", for: .normal)
button8.setTitle("#", for: .normal)
button9.setTitle("#", for: .normal)
button1.setTitleColor(mustard, for: .normal)
button2.setTitleColor(mustard, for: .normal)
button3.setTitleColor(mustard, for: .normal)
button4.setTitleColor(mustard, for: .normal)
button5.setTitleColor(mustard, for: .normal)
button6.setTitleColor(mustard, for: .normal)
button7.setTitleColor(mustard, for: .normal)
button8.setTitleColor(mustard, for: .normal)
button9.setTitleColor(mustard, for: .normal)
button1.setTitleColor(navajo, for: .highlighted)
button2.setTitleColor(navajo, for: .highlighted)
button3.setTitleColor(navajo, for: .highlighted)
button4.setTitleColor(navajo, for: .highlighted)
button5.setTitleColor(navajo, for: .highlighted)
button6.setTitleColor(navajo, for: .highlighted)
button7.setTitleColor(navajo, for: .highlighted)
button8.setTitleColor(navajo, for: .highlighted)
button9.setTitleColor(navajo, for: .highlighted)
button1.frame = CGRect(x: 62.5, y: 300, width: 40, height: 40)
button2.frame = CGRect(x: 167.5, y: 300, width: 40, height: 40)
button3.frame = CGRect(x: 272.5, y: 300, width: 40, height: 40)
button4.frame = CGRect(x: 62.5, y: 405, width: 40, height: 40)
button5.frame = CGRect(x: 167.5, y: 405, width: 40, height: 40)
button6.frame = CGRect(x: 272.5, y: 405, width: 40, height: 40)
button7.frame = CGRect(x: 62.5, y: 510, width: 40, height: 40)
button8.frame = CGRect(x: 167.5, y: 510, width: 40, height: 40)
button9.frame = CGRect(x: 272.5, y: 510, width: 40, height: 40)
button1.addTarget(
self, action: #selector(buttonPressed), for: .touchUpInside
)
button2.addTarget(
self, action: #selector(buttonPressed), for: .touchUpInside
)
button3.addTarget(
self, action: #selector(buttonPressed), for: .touchUpInside
)
button4.addTarget(
self, action: #selector(buttonPressed), for: .touchUpInside
)
button5.addTarget(
self, action: #selector(buttonPressed), for: .touchUpInside
)
button6.addTarget(
self, action: #selector(buttonPressed), for: .touchUpInside
)
button7.addTarget(
self, action: #selector(buttonPressed), for: .touchUpInside
)
button8.addTarget(
self, action: #selector(buttonPressed), for: .touchUpInside
)
button9.addTarget(
self, action: #selector(buttonPressed), for: .touchUpInside
)
view.addSubview(button1)
view.addSubview(button2)
view.addSubview(button3)
view.addSubview(button4)
view.addSubview(button5)
view.addSubview(button6)
view.addSubview(button7)
view.addSubview(button8)
view.addSubview(button9)
}
When a button is pressed we want to do a few different things:
- We want to update that button with that player's move
- but only if that button hasn't already been pressed
- We want to check if that move was a winning move
- and if so we want to stop the game
- We want to show that it's the next player's turn
/**
Action triggered when any game board button is pressed
- parameter sender: A reference to the button that was pressed
*/
@objc func buttonPressed(sender: UIButton!) {
// quit if game is over or button has already been pressed
if winner() || board[sender.tag-1] != 0 {
return
}
let symbol = turnX ? "X" : "O"
let nextSymbol = !turnX ? "X" : "O"
// update button with player symbol
sender.setTitle(symbol, for: .normal)
turnX = !turnX
turnCount += 1
// update board struct with new move
updateBoard(button: sender.tag)
// check for winner
if winner() {
subtitle.text = "\(symbol) wins!"
return
}
// check for draw since no winner was found
if turnCount == 9 {
subtitle.text = "Draw!"
return
}
// update subtitle to say who's turn it is
subtitle.text = "\(nextSymbol)'s turn"
}
There are 8 possible winning combinations for any particular player. We can check for these if there are three game positions with the same value in a line. We can describe these winning combinations by comparing the values as shown below. (rows: a, b, c | col: 1, 2, 3)
The player could get all three in a row:
- a1 == a2 == a3
- b1 == b2 == b3
- c1 == c2 == c3
The player could get all three in a column:
- a1 == b1 == c1
- a2 == b2 == c2
- a3 == b3 == c3
Or the player could get one of the diagonals:
- a1 == b2 == c3
- a3 == b2 == c1
Let's make this function check for each of these 8 cases, and if we find a winning combination we return true, but false if we don't find any.
Once we get that working, let's make it change the colour of the winning positions red as well so we can show the players exactly what the winning moves were.
hint: conditional statements
/**
Checks game board and checks if any winning moves were made.
- returns: True if a winning move was found in `board`
*/
@objc func winner() -> Bool {
if board[0] == board[1]
&& board[0] == board[2]
&& board[0] != 0 {
// row 1 //
button1.setTitleColor(lavaRed, for: .normal)
button2.setTitleColor(lavaRed, for: .normal)
button3.setTitleColor(lavaRed, for: .normal)
return true
} else if board[3] == board[4]
&& board[3] == board[5]
&& board[3] != 0 {
// row 2 //
button4.setTitleColor(lavaRed, for: .normal)
button5.setTitleColor(lavaRed, for: .normal)
button6.setTitleColor(lavaRed, for: .normal)
return true
} else if board[6] == board[7]
&& board[6] == board[8]
&& board[6] != 0 {
// row 3 //
button7.setTitleColor(lavaRed, for: .normal)
button8.setTitleColor(lavaRed, for: .normal)
button9.setTitleColor(lavaRed, for: .normal)
return true
} else if board[0] == board[3]
&& board[0] == board[6]
&& board[0] != 0 {
// col 1 //
button1.setTitleColor(lavaRed, for: .normal)
button4.setTitleColor(lavaRed, for: .normal)
button7.setTitleColor(lavaRed, for: .normal)
return true
} else if board[1] == board[4]
&& board[1] == board[7]
&& board[1] != 0 {
// col 2 //
button2.setTitleColor(lavaRed, for: .normal)
button5.setTitleColor(lavaRed, for: .normal)
button8.setTitleColor(lavaRed, for: .normal)
return true
} else if board[2] == board[5]
&& board[2] == board[8]
&& board[2] != 0 {
// col 1 //
button3.setTitleColor(lavaRed, for: .normal)
button6.setTitleColor(lavaRed, for: .normal)
button9.setTitleColor(lavaRed, for: .normal)
return true
} else if board[0] == board[4]
&& board[0] == board[8]
&& board[0] != 0 {
// diagonal 1 //
button1.setTitleColor(lavaRed, for: .normal)
button5.setTitleColor(lavaRed, for: .normal)
button9.setTitleColor(lavaRed, for: .normal)
return true
} else if board[2] == board[4]
&& board[2] == board[6]
&& board[2] != 0 {
// diagonal 2 //
button3.setTitleColor(lavaRed, for: .normal)
button5.setTitleColor(lavaRed, for: .normal)
button7.setTitleColor(lavaRed, for: .normal)
return true
}
return false
}
We want to update our integer array which stores the states of each button so we don't have to go through each button every time we check for a winner. How can we update the board
values using only the button ID?
hint: ternary operator
/**
Updates `board` with the last move
- parameter button: button ID for button pressed (1...9)
*/
@objc func updateBoard(button: Int) {
board[button-1] = turnX ? 1 : -1
}
}
This line loads and shows the view in playgrounds.
PlaygroundPage.current.liveView = MyViewController()
These are some useful topics that I would suggest reading into, and learning how to use them.
This are some of the resources I used to make this workshop, all of them are worth reading / watching.