Welcome to the "Introduction to Swift" repository! This repository aims to take you through the basics of Swift, Apple's programming language for iOS, macOS, watchOS, and tvOS development.
Whether you're new to programming or an experienced developer, this guide will help you understand and get started with Swift.
- Want to Contribute?
- Constants and Variables
- Strings
- Storing Whole Numbers
- Arithmetic Operators
- Storing Decimal Numbers
- Booleans
- Store Ordered Data in Arrays
- Storing and Finding Data in Dictionaries
- Sets
- Creating and Using Enums
- Using Type Annotations
- if...else
- switch...case
- Using Ternary Conditional Operator
- Using a For Loop
- Using a While Loop
- Skipping Loop Item with Break and Continue
- Functions
- Tuples
- Customizing Parameter Labels
- Providing Default Values for Parameters
- Handling Errors
- Creating and Using Closures
- Function Types
- Passing Functions into Functions
- Passing a Closure into a function
- Trailing Closure Syntax and Shorthand Syntax
- Accepting Functions as Parameters
- Creating Structs
- Compute Property Values Dynamically
- Taking Action when a Property Changes
- Creating a Custom Initializer
- Access Control
- Static Properties and Methods
- Creating Classes
- Inheriting from a Class
- Adding Initializers for Classes
- Copying Classes
- De-initializing a Class
- Working with Variables Inside Classes
- Protocols and Extensions
- Protocol Extensions
- Protocol-Oriented Programming
- Optionals
- Nil Coalescing
- Optional Chaining
- Optional try
- Failable Initializers
- Typecasting
- Acknowledgement
-
Variable: It's value can vary
-
It's good to give a variable a descriptive name
var greeting = "Jambo"
-
One can change a variable's value over time
greeting = "Bonjour"
greeting = "Hello"
- Constant: Use a constant when the value assigned will not change
let pi = 3.142
-
Strings: characters placed between two double quotes (Lee is a String)
let name = "Lee"
-
Double-quote in a String: place a \ before the double quote
let sentence = "This is a \"Stop\" sign"
-
Multi-line Strings: Use three sets of double quotes to create a multi-line string
var sentence = """
A day in
the life of
a software dev
"""
- Read String Length: .count
let nameLength = name.count
print(nameLength)
- Convert String to Uppercase: .uppercased()
let upperCasedName = name.uppercased()
print(upperCasedName)
- Check Prefix and Suffix
print(sentence.hasPrefix("A day"))
print(sentence.hasSuffix("Dev"))
Swift is case sensitive; the first statement will print 'true' while the second will print 'false'
'dev' is not same as 'Dev'
- Joinig Strings Together: using '+' operator
let fName = "Lee"
let lName = "Sangoroh"
let fullName = fName + "Leonard" + lName
- Joining Strings Together: Using String interpolation
It's a better way since it's more efficient and you can pull in integers, decimals etc.
let name = "Leonard Sangoroh"
let age = 23
let message = "Hello I'm \(name) and I'm \(age) years old"
print(message) // Hello I'm Leonard Sangoroh and I'm 23 years old
Perform calculations insider string interpolation
print("5 x 5 is \(5*5)")
- Integers: Can be positive or negative, and are whole numbers
//constant
let DOB = 2001
//variable
var age = 23
- Break up numbers: Use underscores to break numbers up
let bigHardToReadNumber = 1000000000
let bigReadableNumber = 1_000_000_000
- Check whether and integer is a multiple of another integer
let number = 36
print(number.isMultiple(of: 3))
let score = 100
let lowerScore = 100 - 10
let higherScore = 100 + 10
// addition assignment operator OR shorthand operator
let highestScore += 20
let doubledScore = score * 2
let halfScore = score / 2
let pi = 3.142
- Swift considers decimals and integers as different data types
- This means one can't add a decimal and an integer directly
- Store either true or false
let goodDogs = true
let badDogs = false
- Not operator (!): flips a boolean value from true to false, or false to true
var isAuthenticated = true
isAuthenticated = !isAuthenticated
print(isAuthenticated) //prints false
- Toggle functionality: if you call toggle() on a boolean, it will flip a true value to false, and vice versa
var gameOver = false
print(gameOver) //prints false
gameOver.toggle()
print(gameOver) //prints true
Your goal is to write a Swift playground that:
- Creates a constant holding any temperature in Celsius.
- Converts it to Fahrenheit by multiplying by 9, dividing by 5, then adding 32.
- Prints the result for the user, showing both the Celsius and Fahrenheit values.
var tempCelsius = 24
var tempFahrenheit = ((tempCelsius * 9) / 5) + 32
print(tempFahrenheit)
Arrays can only store one data type at a time
var carModels = ["BMW", "Mercedez", "Toyota", "Mazda"]
var numbers = [1,2,3,4,5,6,7,8,9]
var temp = [22.3, 25.6, 12.4]
- Reading Values From Array
var fastestCar = carModels[0]
- Add Items To Array: Only if it was declared as a variable
carModels.append("Mobius")
- Declaring an Empty Array
var albums = Array<Stringt>()
albums.append("African Giant")
var numbers = [Int]()
numbers.append(4)
- Count Number of Items in Array
var carCount = carModels.count
- Remove Items From Array
//remove specific item
carModels.remove(at: 2)
// remove all items
carModels.removeAll()
- Check whether Array Contains an Item
if carModels.contains("Toyota") {
print("Japan Made It")
}
- Sort Items in an Array: Ascending Order
NB: Returns a new array, while the original array remains unchanged
let cities = ["Nairobi", "Tanzania", "Kigali", "Cape Town"]
print(cities.sorted())
- Reverse Items in an Array
print(cities.reversed())
Dictionaries don't store items according to their position like arrays do, but instead let us decide where items should be stored.
Dictionaries stores associations between keys of the same type and values of the same type in a collection with no defined ordering
When reading from a dictionary, there's a chance that the key does not exist, or the key has no value, hence it always returns an optional.
When reading from a dictionary, you can provide a default value to use if the key does not exist
let employee = [
"name" : "Leonard Sangoroh",
"job" : "Software Engineer",
"location" : "Nairobi, Kenya"
]
print(employee["name", default: "Unknown"])
let olympics = [
2012 : "London",
2016 : "Rio de Janeiro",
2020 : "Tokyo"
]
print(olympics[2012, default: "Unknown"])
//create an empty dictionary
let heroes = [String : String]
heroes["Kenya"] = "Dedan Kimathi"
The .count and removeAll() functions both exist for dictionaries
They are similar to arrays, except that they don't store duplicates and don't store items in any particular order
let people = Set(["Lee", "Natty"])
- Declaring an Empty Set
let emptySet = Set<String>()
emptySet.insert("Filling Set")
Alongside .contains(), sets also have the .count and .sorted() functions
An enum is a set of named values that can be created and used in your code
Enums let us define a new data type with a handful of specific values that it can have
enum Weekday {
case monday
case tuesday
case wednesday
case thursday
case friday
}
var dayOfTheWeek = Weekday.monday
Once you assign a value to a variable or constant, its data type becomes fixed
For enums this means once can skip the enum name after the first assignment
var day = Weekday.monday
day = .tuesday
day = .friday
Type annotation let's us be specific about the data type we want
let name: String = "Lee"
let age: Int = 23
let height: Double = 1.76
let sharp: Bool = true
let schoolsLearnt: [String] = ["MEC", "Mang'u", "Strathmore"]
let details: [String: String] = ["id": "134327"]
let booksRead: Set<String> = Set(["The Subtle Art...", "How to Make Friends and Influence People"])
Declaring an empty array
var countries: [String] = [String]()
var towns = [String]()
var cities: [String] = []
This time the challenge is to create an array of strings, then write some code that prints the number of items in the array and also the number of unique items in the array.
var carModels = ["BMW", "Mercedez", "Toyota", "Mazda"]
print(carModels.count)
var uniqueCarModels = Set(carModels)
print(uniqueCarModels.count)
Programs very often make choices, and Swift handles this with if statements
if statements lets us check whether a condition is true
let score = 85
if score > 80 {
print("Great job!")
}
// comparison operators also works with strings
// they are compared alphabetically
let ourName = "Dave Lister"
let friendName = "Arnold Rimmer"
if ourName < friendName {
print("It's \(ourName) vs \(friendName)")
}
if ourName > friendName {
print("It's \(friendName) vs \(ourName)")
}
//equality operator
let country = "Kenya"
if country == "Kenya" {
print("Naipenda nchi yangu \(country)")
}
// not equal operator
if country != "Kenya" {
print("I haven't been there before")
}
//check if a string is empty
if country.isEmtpy == true {
print("Empty")
}
if country.isEmpty {
print("Empty")
}
// using else block for mutually exclusive conditions
let age = 17
if age >= 18 {
print("You can vote")
} else {
print("Try again next election")
}
// using else if, where you can run a new check if the first one fails
// there can only be one 'else'
// 'else' means; if all other conditions have been false
let a = false
let b = true
if a {
print("Code to run if a is true")
} else if b {
print("Code to run if a is false but b is true")
} else {
print("Code to run if both a and b are false")
}
- Making more advanced conditions
// if today's temperature is over 20 degrees but under 30 degrees, print a message
let temp = 25
if temp > 20 {
if temp < 30 {
print("Favorable weather conditions")
}
}
// logical 'and'
if temp > 20 andand temp < 30 {
print("Favorable weather conditions")
}
// logical 'or'
let userAge = 14
let hasParentalConsent = true
if userAge >= 18 || hasParentalConsent == true {
print("You can buy the game")
}
//shorthand
if userAge >= 18 || hasParentalConsent {
print("You can buy the game")
}
- Checking multiple conditions, using enums, and applying logical 'OR'
enum TransportOption {
case airplane, helicopter, bicycle, car, scooter
}
let transport = TransportOption.airplane
if transport == .airplane || transport == .helicopter {
print("Let's fly!")
} else if transport == .bicycle {
print("I hope there's a bike path…")
} else if transport == .car {
print("Time to get stuck in traffic.")
} else {
print("I'm going to hire a scooter now!")
}
Switch cases work well with enums since it knows all possible cases the enum can have, Swift will help out on ensuring that all cases are checked and there is no duplicate checks
All switch...cases must be exhaustive; meaning they must all possible values
Swift will execute the first case that matches the condition and go no further
enum Weather { case sun, rain, wind, snow, unkown}
let forecast = Weather.sun
switch forecase {
case .sun:
print("It should be a nice day")
case .rain:
print("Pack an umbrella")
case .wind:
print("Wear something warm")
case .snow:
print("School is cancelled.")
case .unknown:
print("Our forecast generator is broken")
}
- Switch...case can have a 'default' value when there is ambiguity
For example, when switching between Strings, you can't check all possible strings so a default value would be required
The default case will run if all cases have failed to match
let place = "Metropolis"
switch place {
case "Gotham":
print("You're Batman!")
case "Mega-City One":
print("You're Judge Dredd!")
case "Wakanda":
print("You're Black Panther!")
default:
print("Who are you?")
}
Remember: Swift checks its cases in order and runs the first one that matches. If you explicitly want Swift to carry on executing subsequent cases, use 'fallthrough' just before ending the case code block
The ternary operator lets us check a condition and return one of two values
//create a constant called 'age' and another called 'canVote' that will store whether you can vote
// WTF(What, True, False) syntax
let age = 23
let canVote = age >= 18 ? "Yes" : "No"
// another example
let temp = 24
let generalWeather = temp < 16 ? "It's freezing" : "No need for a heavy jacket"
//another example
enum Theme {
case light, dark
}
// if Theme is dark, assign "black" to theme otherwise assign "white" to theme
let theme = Theme.dark
let background = theme == .dark ? "black" : "white"
print(background)
- Print each Item in an Array
// loop body - code inside the braces
// loop iteration - one cycle through the loop body
// loop variable - exists only inside the loop body and will change to a new value in the next loop iteration
let platforms = ["iOS", "macOS", "tvOS", "wathcOS", "visionOS"]
for os in platforms {
print("Swift works on \(os))
}
- Loop over fixed range of numbers
// prints 1 to 12 (including 12)
for i in 1...12 {
print(i)
}
- Put loops inside loops (nested loops)
for i in 1...12 {
for j in 1...12 {
print("\(i) x \(j) is \(i*j)")
}
}
- Loop over fixed range of numbers
// prints 1 to 11 (excluding 12)
for i in 1..<12 {
print(i)
}
- Loop over without need for a variable
// prints 1 to 4 (including 4)
for _ in 1...4 {
print("Help")
}
A while loop continuously executes a loop until the condition is false
- Basic while loop
var countdown = 10
while countdown > 0 {
print("\(countdown)")
countdown -= 1
}
print("blast off!")
While loops are useful when the iterations of a loop can't be determined at the start
For example; when rolling a virtual dice and ending the loop when 6 is rolled
var roll = 0
while roll != 6 {
roll = Int.random(in: 1...6)
print("I rolled a \(roll)")
}
- Continue: skips the current loop iteration
- Break: exits the loop skips all remaining iterations
// continue
// if filename does not have ".jpg" as suffix, skip it and move to the next iteration
let filenames = ["me.jpg", "work.txt", "sophie.jpg", "logo.psd"]
for filename in filenames {
if filename.hasSuffix(".jpg") == false {
continue
}
print("Found picture: \(filename)")
}
/* break
checking multiples for two numbers
if i is a multiple of both 4 and 14, append it to the integer array
once the 10 multiples are appended, call break to exit the loop
*/
let number1 = 4
let number2 = 14
var multiples = [Int]()
for i in 1...100_000 {
if i.isMultiple(of: number1) andand i.isMultiple(of: number2) {
multiples.append(i)
if multiples.count == 10 {
break
}
}
}
print(multiples)
Your goal is to loop from 1 through 100, and for each number:
- If it’s a multiple of 3, print “Fizz”
- If it’s a multiple of 5, print “Buzz”
- If it’s a multiple of 3 and 5, print “FizzBuzz”
- Otherwise, just print the number.
for i in 1...100 {
if i.isMultiple(of: 3) andand i.isMultiple(of: 5) {
print("FizzBuzz")
} else if i.isMultiple(of: 3) {
print("Fizz")
} else if i.isMultiple(of: 5) {
print("Buzz")
} else {
print("\(i)")
}
}
Functions enable the reuse of code
// create function
func showWelcome(){
print("Welcome to my application!")
}
// call a function
showWelcome()
// parameters in functions
// function that prints the multiplication table
func printTimesTable(number: Int, end: Int){
for i in 1...end {
print("\(i) x \(number) is \(i * number)")
}
}
// 5 and 20 are arguments
printTimesTable(number: 5, end: 20)
func rollDice() -> Int {
return Int.random(in: 1...6)
}
let result = rollDice()
// example two
func areLettersIdentical(stringOne: String, stringTwo: String) -> Bool {
let first = stringOne.sorted()
let second = stringTwo.sorted()
return first == second
}
// when a function has only one line of code and has promised to return a value,
// there's no need to write the 'return' keyword
func areLettersIdentical(string1: String, string2: String) -> Bool {
string1.sorted() == string2.sorted()
}
func pythagoras(a: Double, b: Double) -> Double {
let input = a * a + b * b
let root = sqrt(input)
return root
}
let c = pythagoras(a: 3, b: 4)
print(c)
// shorten pythagoras()
func pythagoras(a: Double, b: Double) -> Double {
sqrt(a * a + b * b)
}
- Return multiple values using an array
func getUser() -> [String] {
["Lee", "Sangoroh"]
}
let user = getUser()
print("Name: \(user[0]) \(user[1])")
- Return multiple values using a dictionary
func getUser() -> [String: String] {
[
"fName" : "Lee",
"lName" : "Sangoroh"
]
}
let user = getUser()
print("Name: \(user["fName"], default: "Anonymous") \(user["lName"], default: "Anonymous")")
Like arrays, dictionaries, and sets, tuples lets us store multiple values in a single variable.
Unlike the rest, tuples have a fixed size and can have a variety of data types
func getUser() -> (firstName: String, lastName: String) {
(firstName: "Lee", lastName: "Sangoroh")
}
let user = getUser()
print("Name: \(user.firstName) \(user.lastName)")
/*
when returning a tuple from a function, Swift already knows the names you're giving each item in the
tuple, so there's no need to repeat them when returning
*/
func getUser() -> (firstName: String, lastName: String) {
("Lee", "Sangoroh")
}
/*
some given tuples may have elements without names
you will access them using numerical indices
*/
func getUser() -> (String, String) {
("Lee", "Sangoroh")
}
let user = getUser()
print("Name: \(user.0) \(user.1)")
/*
if a function returns a tuple you can pull the tuple apart into individual values
*/
func getUser() -> (firstName: String, lastName: String) {
(firstName: "Taylor", lastName: "Swift")
}
let (firstName, lastName) = getUser()
print("\(firstName) \(lastName)")
// if you don't need all values from the tuple
let (firstName, _) = getUser()
print(firstName)
- Return type is (firstName: String, lastName: String), which is a tuple containing two strings
- The key advantage for tuples over dictionaries is that we can specify exactly which values will exist and what types they have, whereas dictionaries may or may not contain the values we're asking for
When defining parameters for a function we can add two names; one for use where the function is called and one for use inside the function
func isUppercase(_ string: String) -> Bool {
string == string.uppercased()
}
let string = "HELLO, WORLD"
let result = isUppercase(string)
// a name for calling the function and for using inside the function block
func printTimesTables(for number: Int) {
for i in 1...12 {
print("\(i) x \(number) is \(i * number)")
}
}
printTimesTables(for: 5)
func printTimesTable(for number: Int, end: Int = 12) {
for i in 1...end {
print("\(i) x \(number) is \(i * number)")
}
}
printTimesTable(for: 5, end: 12)
printTimesTable(for: 3)
Handling errors involves three steps;
- Informing Swift of the possible errors that can happen
- Writing a function that can flag up errors if they happen
- Calling the function, and handling any errors that might happen
// define possible errors that might happen i.e. make a new enum that builds on Swift's existing Error type
enum PasswordError: Error {
case short
case obvious
}
// the enum does not define what the errors mean, it only brings existence
// write a function that will trigger one of the errors
func checkPassword(_ password: String) throws -> String {
if password.count < 5 {
throw PasswordError.short
}
if password == "12345" {
throw PasswordError.obvious
}
if password.count < 8 {
return "OK"
} else if password.count < 10 {
return "Good"
} else {
return "Excellent"
}
}
/*
if a function can throw error(s) without handling them itself, make the function as 'throws' before the return type
if marked with 'throws', the function can but must not throw an error
*/
// run the function and handle any errors that might happen
/*
1. Start a block of work that might throw errors using 'do'
2. Call one or more throwing functions using 'try'
3. Handling any thrown error using catch
*/
let string = "12345"
do {
let result = try checkPassword(string)
print("Rating: \(result)")
} catch {
print("There was an error")
}
/*
try - must be written before all functions that can throw an error
one must be inside a 'do' block when using 'try, and one must have a 'catch' block(s)
*/
/* when it comes to catching errors, one must have a catch block (that's able to handle every kind of error)
you can also catch specific errors
*/
let string = "12345"
do {
let result = try checkPassword(string)
print("Password rating: \(result)")
} catch PasswordError.short {
print("Please use a longer password.")
} catch PasswordError.obvious {
print("I have the same combination on my luggage!")
} catch {
print("There was an error.")
}
The challenge is this: write a function that accepts an integer from 1 through 10,000, and returns the integer square root of that number. That sounds easy, but there are some catches:
- You can’t use Swift’s built-in sqrt() function or similar – you need to find the square root yourself.
- If the number is less than 1 or greater than 10,000 you should throw an “out of bounds” error.
- You should only consider integer square roots – don’t worry about the square root of 3 being 1.732, for example.
- If you can’t find the square root, throw a “no root” error.
enum Errors: Error {
case outOfBounds
case noRoot
}
func squareRoot(number: Int) throws -> Int {
if number > 1 andand number <= 10_000 {
for i in 1...100 {
if i*i == number {
return i
} else {
throw Errors.noRoot
}
}
}
throw Errors.outOfBounds
}
do {
let result = try squareRoot(number: 35)
} catch Errors.outOfBounds{
print("the number entered should be between 1 and 10_000")
} catch Errors.noRoot {
print("the number entered has not square root")
} catch {
print("There is an error")
}
// create closure
let sayHello = {
print("Hi there!")
}
sayHello()
// parameter-accepting closure(s)
let sayHi = { (name: String) -> String in
// must not use 'return' keyword since its a single line
"Hi \(name)"
}
/*
no parameter name is required since when the closure is assigned to a variable,
it acquires the type (String)-> String
and not type (name: String) -> String
that is just how it is :)
this works the same way as creating a copy of a function
it assumes a different data type and hence no need to mention the parameter name while passing the argument
*/
sayHi("Lee")
func greetUser() {
print("Hello")
}
var greetCopy = greetUser
//type annotation for greetCopy
/*
- empty parentheses shows the function takes no arguments
- the arrow prompts the declaration of a return type
- Void means 'nothing'; meaning the function returns nothing
*/
var greet: () -> Void = greetUser
// every function's type depends on the data it receives and returns
func getUserData(for id: Int) -> String {
if id == 1989 {
return "Taylor Swift"
} else {
return "Anonymous"
}
}
let data: (Int) -> String = getUserData
let user = data(1989)
print(user)
let fullName = ["Leonard", "Lee", "Sangoroh"]
//will sort alphabetically
let sortedName = fullName.sorted()
print(sortedName)
// sorted allows us to pass in a custom sorting function
//provided it takes in two strings, and returns a boolean
// if true first string will be sorted before second
//if false, second string will be sorted before first
//sort function to put firstName as the first in the array
func firstNameFirstSorted(name1: String, name2: String) -> Bool {
if name1 == "Lee" {
return true
} else if name2 == "Lee" {
return false
}
return name1 < name2
}
let firstNameFirst = fullName.sorted(by: firstNameFirstSorted)
print(firstNameFirst)
/*
instead of passing a function, you can just write the logic directly into the sorted()
using a closure
*/
/*
instead of passing a function, you can just write the logic directly into the sorted()
using a closure
*/
let firstNameFirst = fullName.sorted(by: { (nameOne: String, nameTwo: String) -> Bool in
if nameOne == "Lee" {
return true
} esle if nameTwo == "Lee" {
return false
}
return nameOne < nameTwo
})
- We will practically do this by using the section above ('Passing a Closure into a Function') for reference
/*
the function passed into .sorted() must have two string parameters and return a boolean
so there's no need to specify the types of the two parameters because they must be strings
and no need to specify the return type since it must be boolean
//code change
*/
let firstNameFirst = fullName.sorted(by: { (nameOne, nameTwo) in
if nameOne == "Lee" {
return true
} esle if nameTwo == "Lee" {
return false
}
return nameOne < nameTwo
})
// trailing closure syntax
// allowed when one function accepts another
let firstNameFirst = fullName.sorted { (nameOne, nameTwo) in
if nameOne == "Lee" {
return true
} esle if nameTwo == "Lee" {
return false
}
return nameOne < nameTwo
}
// shorthand syntax
// swift can automatically provide names for our parameters
let firstNameFirst = fullName.sorted {
if $0 == "Lee" {
return true
} esle if $1 == "Lee" {
return false
}
return $1 < $2
}
- The filter() function lets us run some code on every item in the array, and will send back a new array containing every item that returns true for the function. So, we could find all team players whose name begins with T like this:
let allNames = ["Lee", "Leonard", "Sangoroh"]
let lOnly = allNames.filter { $0.hasPrefix("L")}
- The map() function lets us transform every item in the array using some code of our choice, and sends back a new array of all the transformed items
let uppercasedName = allNames.map { $0.uppercased() }
print(uppercasedName)
// function accepting a function/closure
func makeArray (size: Int, using generator: () -> Int) -> [Int] {
var numbers = [Int]()
for _ in 0..<size {
let newNumber = generator()
numbers.append(newNumber)
}
return numbers
}
// calling the function and passing a closure using trailing syntax
let rolls = makeArray(size: 50) {
Int.random(in: 1...20)
}
print(rolls)
// calling a function and passing a function
func generateNumber() -> Int {
Int.random(in: 1...20)
}
let newRolls = makeArray(size: 50, using: generateNumber)
print(newRolls)
func doImportantWork(first: () -> Void, second: () -> Void, third: () -> Void) {
print("About to start first work")
first()
print("About to start second work")
second()
print("About to start third work")
third()
print("Done!")
}
// calling the function
doImportantWork {
print("This is the first work")
} second: {
print("This is the second work")
} third: {
print("This is the third work")
}
Your job is to:
- Filter out any numbers that are even
- Sort the array in ascending order
- Map them to strings in the format “7 is a lucky number”
- Print the resulting array, one item per line
let luckyNumbers = [7,4,38,21,16,15,12,33,31,49]
func mixedFunction(luckyNumbers: [Int]) -> [String] {
let oddNumbers = luckyNumbers.filter { !$0.isMultiple(of: 2)}
let sortedNumbers = oddNumbers.sorted()
let mappedToString = sortedNumbers.map { "\($0) is a lucky number" }
return mappedToString
}
var mappedToString = mixedFunction(luckyNumbers: luckyNumbers)
for string in mappedToString {
print(string)
}
// chaining methods without using temporary variables
let luckyNumbers = [7,4,38,21,16,15,12,33,31,49]
func mixedFunction(luckyNumbers: [Int]) -> [String] {
let mappedToString = luckyNumbers.filter{ !$0.isMultiple(of: 2)}.sorted().map{ "\($0) is a lucky number"}
return mappedToString
}
var mappedToString = mixedFunction(luckyNumbers: luckyNumbers)
for string in mappedToString {
print(string)
}
- Swift's structs let us create our own custom, complex data types, complete with their own variables, and functions
- Variables and constants that belong to structs are called properties
- Functions that belong to structs are called methods
- An instance of a struct is created when a constant or variable is created from the struct
// create new type called album with two string constants plus an integer constant and a function
struct Album {
let title: String
let artist: String
let year: Int
func printSummary() {
print("\(title) \(year) by \(artist)")
}
}
let red = Album(title: "Red", artist: "TS", year: 2012)
let wings = Album(title: "Wings", artist: "BTS", year: 2016)
print(red.title)
print(wings.artist)
red.printSummary()
// when having changing values in a struct, the variable created from the instance must be a 'var'
// in case a function is changing a variable, it must start with the keyword 'mutating'
struct Employee {
let name: String
var vacationRemaining: Int = 14
mutating func takeVacation(days: Int) {
if vacationRemaining > days {
vacationRemaining -= days
print("I'm going on vacation!")
print("Days remaining: \(vacationRemaining)")
} else {
print("Oops! There aren't enough days remaining.")
}
}
}
var archer = Employee(name: "Lee Archer", vacationRemider: 14)
var kane = Employee(name: "Harry Kane")
archer.takeVacation(days: 4)
print(archer.vacationRemaining)
A struct can have two types of variables;
- stored property - variable that holds a piece of data inside an instance of a struct
- computed property - calculates the value of the property dynamically every time its accessed
struct Employee {
let name: String
var vacationAllocated = 14
var vacationTaken = 0
// vacationRemaining is calculated by subtracting how much vacation they have taken from how much
// vacation they were allocated
// it is not assigned but rather calculated
var vacationRemaining: Int {
vacationAllocated - vacationTaken
}
}
var archer = Employee(name: "Sterling Archer", vacationAllocated: 14)
archer.vacationTaken += 4
// reading from vacationRemaining
print(archer.vacationRemaining)
archer.vacationTaken += 4
// reading from vacationRemaining
print(archer.vacationRemaining)
// reading from and writing to computed properties
var vacationRemaining: Int {
get {
vacationAllocated - vacationTaken
}
set {
// newValue is automatically provided by Swift, and stores whatever value the user
// was trying to assign to the property
vacationAllocated = vacationTaken + newValue
}
}
var archer = Employee(name: "Sterling Archer", vacationAllocated: 14)
archer.vacationTaken += 4
archer.vacationRemaining = 5
print(archer.vacationAllocated)
- Swift enables us to set property observers, which are special code blocks that run when properties change
- There are of two forms; didSet and willSet
- didSet; runs when the property just changed
- willSet; runs before the property changes
struct Game {
var score = 0 {
didSet {
print("Score changed to \(score)")
}
}
}
var game = Game()
game.score += 10
game.score -= 3
game.score += 1
struct App {
var contacts = [String]() {
willSet {
print("Current value is: \(contacts)")
print("New value will be: \(newValue)")
}
didSet {
print("There are now \(contacts.count) contacts.")
print("Old value was \(oldValue)")
}
}
}
var app = App()
app.contacts.append("Adrian E")
app.contacts.append("Allen W")
app.contacts.append("Ish S")
- Initializers are specialized methods designed to prepare a new struct instance to be used
- We can create our own struct as long as we ensure all properties have a value by the time the initializer ends
// our own initializer
// once you create your custom initializer, Swift assumes that the default one
// is no longer needed
struct Player {
let name: String
let number: Int
init (name: String, number: Int) {
self.name = name
self.number = number
}
}
- By default, Swift's structs let us access their properties and methods freely
- Sometimes, you want to hide some data from external access
struct BackAccount {
// make funds variable inaccessible outside the struct to prevent people
// from adjusting their real balance
private var funds = 0
mutating func deposit(amount: Int) {
funds += amount
}
mutating func withdraw(amount: Int) -> Bool {
if funds >= amount {
funds -= amount
return true
} else {
return false
}
}
var account = BankAccount()
account.deposit(amount: 100)
let success = account.withdraw(amount: 200)
if success {
print("Withdrew money successfully")
} else {
print("Failed to get the money")
}
}
Swift provides us with a couple of access controls;
- private: Don't let anything outside the struct use this
- fileprivate: Don't let anything outside the current file use this
- public: Let anyone, anywhere use this
- private(set): let anyone read this property, but only let methods inside the struct write to it
-
Each instance of a struct has its own unique copy of properties and methods
-
Sometimes, you want to add a property or method to the struct directly and not its instance, which enables you to use them directly
-
Uses of static properties and methods include;
a. creating example data
b. storing fixed data -
No instance of School is created, its properties and methods are accessed directly
-
'mutating' is also not needed; it's only needed with a regular struct function when an instance of struct was created as a constant
struct School {
static var studentCount = 0
static func add(student: String) {
print("\(student) joined the school")
studentCount += 1
}
}
School.add(student: "Lee Sangoroh")
print(School.studentCount)
- Rule One: You can't refer non-static properties and methods from static properties or methods
- Rule Two: To access static code from non-static code, use your type's name e.g. School.studentCount, or use Self to refer to the current type
Difference btw self and Self
- self: refers to the current value of the struct
- Self: refers to the current type
// where structs are used
// storing shared constant values
struct AppData {
static let version = "1.0.0 beta"
static let saveFileName = "settings.json"
static let homeURL = "https://www.mumamaler.com"
}
// generating examples
struct Employee {
let username: String
let password: String
static let example = Employee(username: "cfederighi", password: "hairforceone")
}
Create a struct to store information about a car, including its model, number of seats, and current gear, then add a method to change gears up or down. Have a think about variables and access control: what data should be a variable rather than a constant, and what data should be exposed publicly? Should the gear-changing method validate its input somehow?
struct Car {
let carModel: String
let numberOfSeats: Int
var currentGear: Int
mutating func gearUp(){
if currentGear < 10 {
currentGear += 1
}
}
mutating func gearDown(){
if currentGear > 1 {
currentGear -= 1
}
}
}
var toyota = Car(carModel: "Toyota", numberOfSeats: 5, currentGear: 3)
print(toyota.currentGear)
toyota.gearUp()
print(toyota.currentGear)
Apart from structs, Classes are also another way of creating custom data types
Differences between classes and structs
- You can make one class build upon functionality in another class, gaining all its properties and methods as a starting point. You can also selectively override some methods.
- There are no automatically generated memberwise initializers for classes. Either write you own, or assign default values
- When you copy an instance of a class, both copies share the same data - if you change one copy, the other one also changes
Why? In Swift, class are reference types; meaning when you create an instance of a class and then create a copy of that instance, both the original instance and the copy actually reference the same underlying object in memory. They both point to the same memory location. Structs on the other hand are value types; meaning each instance of a struct has its own unique copy of the data. When you assign a struct instance to another variable, or pass it to a function, Swift creates a copy of that instance. - When the final copy of a class instance is destroyed, Swift can optionally run a special function called 'deinitializer'
- Even if you make a class constant, you can still change its properties as long as they are variables
class Game {
var score = 0 {
didSet {
print("Score is now \(score)")
}
}
}
var newGame = Game()
newGame.score += 10
When a class inherits functionality from another, Swift gives the child class access to the properties and methods of the parent class, allowing us to make small changes to customize the way the new class behaves
class Employee {
let hours: Int
init (hours: Int) {
self.hours = hours
}
func printSummary() {
print("I work \(hours) hours a day.")
}
}
class Developer: Employee {
func work() {
print("I'm writing code for \(hours) hours")
}
}
class Manager: Employee {
func work() {
print("I'm going to meetings for \(hours) hours")
}
// changing a method you inherit (using override)
override func printSummary() {
print("I am the boss, I decide how long I will work for")
}
}
// the child classes can refer directly to 'hours'
let lee = Developer(hours: 18)
lee.work() // I'm writing code for 18 hours
let sangoroh = Manager(hours: 6)
sangoroh.work() // I'm going to meetins for 6 hours
sangoroh.printSummary() // I work 6 hours a day
- If you want your class not to support any inheritance, mark it using 'final' keyword
- If a child class has any custom initializers, it must always call the parent's initializer after it has finished setting up its own properties, if it has any
class Vehicle {
let isElectric: Bool
init(isElectric: Bool) {
self.isElectric = isElectric
}
}
class Car: Vehicle {
let isConvertible: Bool
init(isConvertible: Bool, isElectric: Bool) {
self.isConvertible = isConvertible
super.init(isElectric: isElectric)
}
}
let tesla = Car(isConvertible: true, isConvertible: false)
NB: If a subclass doesn't have any of its own initializers, it automatically inherits the initializers of its parents class.
- In Swift, all copies of a class instance share the same data
class User {
var username = "leonardsangoroh"
}
var userOne = User() //leonardsangoroh
var userTwo = userOne // leonardsangoroh
userTwo = "leonard"
// both userOne and userTwo change to 'leonard'
print(user1.username)
print(user2.username)
- If you want to create a unique copy of a class instance (deep copy), you need to handle creating a new instance and copy acros all your data safely
class User {
var username = "Anonymous"
func copy() -> User {
let user = User()
user.username = username
return user
}
}
// now we can safely call copy() to get an object with the same sarting data, but any future changes won't impact the original
var anonymous = User(username: "lee")
var lee = anonymous.copy
Swift's classes can optionally be given a de-initializer
- Deinitializers don't use the func keyword
- They can't take parameters or return data
- They are automatically called when the final copy of a class instance is destroyed
- They are automatically handled by the system
- Structs don't have deinitializers, because you can't copy them
class User {
let id: Int
init(id: Int) {
self.id = id
print("User \(id): I'm alive!")
}
deinit {
print("User \(id): I'm dead!")
}
}
// creating and destroying instances
// if we create a User instance inside a loop, it will be destroyes when each loop iteration finishes
for i in 1...3 {
let user = User(id: i)
print("User \(user.id): I'm in control!")
}
Four options for variables and instance of a class;
- Constant instance, constant property – a signpost that always points to the same user, who always has the same name.
- Constant instance, variable property – a signpost that always points to the same user, but their name can change.
- Variable instance, constant property – a signpost that can point to different users, but their names never change.
- Variable instance, variable property – a signpost that can point to different users, and those users can also change their names.
- Make a class hierarchy for animals, starting with Animal at the top, then Dog and Cat as subclasses, then Corgi and Poodle as subclasses of Dog, and Persian and Lion as subclasses of Cat.
But there’s more:
- The Animal class should have a legs integer property that tracks how many legs the animal has.
- The Dog class should have a speak() method that prints a generic dog barking string, but each of the subclasses should print something slightly different.
- The Cat class should have a matching speak() method, again with each subclass printing something different.
- The Cat class should have an isTame Boolean property, provided using an initializer.
class Animal {
let legs: Int
init(legs: Int) {
self.legs = legs
}
}
class Dog: Animal {
//intrinsically inherits parent class' init method
func speak() {
print("Wooof!")
}
}
class Cat: Animal {
let isTame: Bool
init(isTame: Bool, legs: Int) {
self.isTame = isTame
super.init(legs: legs)
}
func speak() {
print("Meeeeow!")
}
}
class Corgi: Dog {
override func speak() {
print("Corgi Wooof!")
}
}
class Poodle: Dog {
override func speak() {
print("Poodle Wooof!")
}
}
class Persian: Cat {
override func speak() {
print("Persian Meeoooow!")
}
}
class Lion: Cat {
override func speak() {
print("Lion Meeoooow!")
}
}
Protocols are like agreements in Swift; they let us define what kinds of functionality we expect a data type to support, and Swift ensures that the rest of our code follows those rules
Protocols let us define properties and methods that we want to use without implementing those properties and methods.
Protocols act as blueprints
// this is a new type
protocol Vehicle {
// if need be, you can mark methods as throwing or mutating
func estimateTime(for distance: Int) -> Int
func travel(distance: Int)
}
// we can create structs, classes, or enums that conform to the protocol
// car struct that conforms to Vehicle Protocol
struct Car: Vehicle {
func estimateTime(for distance: Int) -> Int {
distance/50
}
func travel(distance: Int) {
print("I'm driving \(distance)km")
}
// not needed to conform to Vehicle protocol
func openSunroof() {
print("It's a nice day!")
}
}
/*
Swift knows that any type conforming to Vehicle must implement both estimateTime() and travel() methods,
and so it lets us use Vehicle as the type of our parameter rather than car
Now the function can be called with any type of data, as long as that type
conforms to Vehicle protocol e.g. Car, Bus, Train, Plane etc.
*/
//func commute(distance: Int, using vehicle: Car) {
func commute(distance: Int, using vehicle: Vehicle) {
if vehicle.estimateTime(for: distance) > 100 {
print("That's too slow!")
} else {
vehicle.travel(distance: distance)
}
}
let car = Car()
commute(distance: 100, using: car)
struct Bicycle: Vehicle {
func estimateTime(for distance: Int) -> Int {
distance / 10
}
func travel(distance: Int) {
print("I'm cycling \(distance)km.")
}
}
let bike = Bicycle()
commute(distance: 50, using: bike)
- Type annotation is required for properties in protocol(s), since we can't provide a default value in a protocol, just as protocols can't provide implementations for methods.
protocol Vehicle {
// a string called name which must be readable
// Meaning it could be a constant of a computed property with a getter
var name: String { get }
// an integer called currentPassengers that must be read-write
// could be a variable, or a computed property with a getter and setter
var currentPassengers: Int { get set }
}
- So now our protocol requires two methods and two properties, meaning that all conforming types must implement those four things in order for our code to work. This in turn means Swift knows for sure that functionality is present, so we can write code relying on it.
func getTravelEstimates(using vehicles: [Vehicle], distance: Int) {
for vehicle in vehicles {
let estimate = vehicle.estimateTime(for: distance)
print("\(vehicle.name): \(estimate) hours to travel \(distance)km")
}
}
getTravelEstimates(using: [car, bike], distance: 150)
- One protocol can inherit from multiple protocols.
- Unlike classes, you can inherit from multiple protocols at the same time before you add your own customization.
protocol Payable {
func calculateWages() -> Int
}
protocol NeedsTraining {
func study()
}
protocol HasVacation {
func takeVacation(days: Int)
}
// employee protocol brings together all the three protocols
// we can make new types conform to the single protocol rather than the threes
protocol Employee: Payable, NeedsTraining, HasVacation { }
- Extensions allow us to add methods(functionality) to existing types
// add extension to Int type, add a squared() methods to return current number squared
extension Int {
func squared() -> Int {
return self * self
}
}
let number = 23
number.squared()
- Swift does not allow adding of stored properties in extensions, necessitating the use of computed properties
extension Int {
var isEven: Bool {
return self % 2 == 0
}
}
- Protocols: let us describe what properties and methods something should have, but don't provide implementation
- Extensions: let us provide implementation of methods, but only affect one data type i.e. one can't add the method to lots of types at the same time
- Protocol Extensions; are like regular extensions, but instead of extending a specific type like Int, we extend a whole protocol so that all conforming types get the changes
// both arrays and sets conform to Swift's collection protocol
let sangorohs = ["Lee", "Rachael", "Roy"]
let operations = Set(["addition", "subtraction"])
// extend collection instead of arrays or sets
extension Collection {
func summarize() {
print("There are \(count) of us: ")
for name in self {
print(name)
}
}
}
//both array and set will have the method
sangorohs.summarizee()
operations.summarize()
- Protocol extensions can provide default implementations for our own protocol methods, hence making it easy for a type to conform to a protocol
- We can extend a protocol to add method implementations, meaning any type conforming to that protocol get those methods.
//protocol
protocol Identifiable {
var id: String { get set }
func identify()
}
// you can make all conforming types implement identity() individually
// but protocol extensions allow you to provide a default
extension Identifiable {
func identify() {
print("My ID is \(id).")
}
}
// creating a type that conforms to Identifiable
struct User: Identifiable {
var id: String
}
let lee = User(id: "Lee")
lee.identify()
- Used to add functionality directly to protocols, meaning there's no need to copy that functionality to all structs and classes conforming to the protocol
Make a protocol that describes a building, adding various properties and methods, then create two structs, House and Office, that conform to it.
Your protocol should require the following:
- A property storing how many rooms it has.
- A property storing the cost as an integer (e.g. 500,000 for a building costing $500,000.)
- A property storing the name of the estate agent responsible for selling the building.
- A method for printing the sales summary of the building, describing what it is along with its other properties.
protocol Building {
var numberOfRooms: Int { get }
var cost: Int { get set }
var agentName: String { get }
func printSalesSummary() -> Void
}
extension Building {
func printSalesSummary() -> Void {
print("""
\(numberOfRooms)
\(cost)
\(agentName)
""")
}
}
struct House: Building {
var numberOfRooms: Int
var cost: Int
var agentName: String
}
struct Office: Building {
var numberOfRooms: Int
var cost: Int
var agentName: String
}
let houseOne = House(numberOfRooms: 5, cost: 1_000_000, agentName: "Lee Sangoroh")
print(houseOne.printSalesSummary())
- An optional variable might have a value or might not; it might literally be missing, which is 'nil' in Swift
//make a type optional by adding ? after it
var age: Int? = nil
// assign a value
age = 23
// if...let
if let unwrapped = age {
print("I am \(age)years old")
} else {
print("Missing name.")
}
// unwrapping with guard
// guard let will unwrap an optional
// but if it finds nil inside it expects you to exit the function loop, or condition you used it in
// guard let lets your unwrapped variable be used after the guard code
func greet (_ name: String?) {
guard let unwrapped = name else {
print("No name provided")
return
}
print("Hello, \(unwrapped)!")
}
- Force unwrapping converts an optional to a non-optional
- e.g. Converting a String to an Integer leads to the destination variable being an optional because the converted string could be a word hence not convertinble to an Integer
- However, if you're sure it is an integer, you can force unwrap
- If forcefully unwrapped and found to be nil, the entire code crashes
let str = "7"
//optional
let num = Int(str)
// non-optional
let num = Int(str)!
- Regular optional, just that they don't need to be unwrapped to be used
- They exist because sometimes a variable will start life as nil, but will always have a value before you need to use it
let age: Int! = nil
- It ensures that a variable has a value
- The nil coalescing operator unwraps an optional and returns the value inside if there's one. If the optional was nil, a default value is used instead
func username(for id: Int) -> String? {
if id == 1 {
return "Lee Sangoroh"
} else {
return nil
}
}
// nil coalescing
let mainUser = username(for: 5) ?? "Anonymous"
print(mainUser) //prints Anonymous
- Swift gives a shortcut to access things like a.b.c and b is an optional.
- You can write a question mark after it to enable optional chaining (a.b?.c)
- Swift checks whether b has a value, if it's nil, the rest of the line is ignored but if it has a value it will be unwrapped and execution will continue
let names = ["Lee", "Leonard", "Sangoroh"]
// if first item in array exists, execution continues and fName will contain its uppercased version
// if first item is not there, execution ends and fName is assigned nil
let fName = names.first?.uppercased()
- Previous do...try...catch
enum PasswordError: Error {
case obvious
}
func checkPassword(_ password: String) throws -> Bool {
if password == "password" {
throw PasswordError.obvious
}
return true
}
do {
try checkPassword("password")
print("That password is good!")
} catch {
print("You can't use that password.")
}
- There are two alternatives to try; try? and try!
- The first is try?, changes throwing functions into functions that return an optional.
- If the function throws an error you'll be sent nil as the result, otherwise you'll get the return value wrapped as an optional
if let result = try? checkPassword("password") {
print("Result has \(result)")
} else {
print("D'oh.")
}
- The second is try!, which is used when you know the function will not fail. If the function throws an error then all the code crashes
try! checkPassword("Leonard")
print("Okay")
-
In Swift, a failable initializer is an initializer that can return nil, indicating that the initialization of the object has failed. This is useful when you need to perform some validation or checks during the initialization process, and there's a possibility that the initialization might not succeed.
-
You define a failable initializer by placing a question mark after the init keyword: init?. If the initialization fails, you return nil.
struct Person {
var name: String
var age: Int
init?(name: String, age: Int) {
if age < 0 {
// If the age is negative, return nil to indicate initialization failure
return nil
}
self.name = name
self.age = age
}
}
if let person = Person(name: "John Doe", age: 30) {
print("Person was created successfully: \(person.name), \(person.age) years old")
} else {
print("Failed to create person")
}
if let person = Person(name: "Jane Doe", age: -5) {
print("Person was created successfully: \(person.name), \(person.age) years old")
} else {
print("Failed to create person")
}
-
In this example, the Person struct has a failable initializer. It checks if the age is less than 0, and if so, it returns nil, indicating that the initialization has failed. When we try to create a Person with a negative age, the initializer fails, and we get nil.
-
Failable initializers are useful when dealing with potentially invalid data or when initializing objects that depend on certain conditions being met. They provide a way to safely handle initialization failures without causing runtime errors.
- Used for filtering sometimes
class Animal {
}
class Fish: Animal {
}
class Dog: Animal {
func makeNoise() {
print("Woof!")
}
}
//array of fish and dogs
let pets = [Fish(), Dog(), Fish(), Dog()]
//both Fish and Dog inherit from Animal class, hence type inference is used to make pets an array of Animal
// if we want to loop over pets and ask all dogs to bark
// we need to perform a typecase: Swift will check whether each pet is a Dog object
// if it is we can call makeNoise()
// we will need the keyword as?, which returns an optional; it will be nil if the typecast fails, or a converted type otherwise
for pet in pets {
if let dog = pet as? Dog {
dog.makeNoise()
}
}
Contributions are welcome! If you have any improvements or suggestions, feel free to open an issue or submit a pull request to the staging branch of the repository.
Improvements or suggestions may include but are not limited to;
- Grammatical errors
- Wrong Swift syntax
- Inclusion of a Swift Concept
- Suggestion to better explain a concept
- Better organization of the .md file
- The list is endless!
Give a ⭐️ if you would like to revisit this content!
Buy me Coffee
- Pual Hudson - this document is made in reference to Paul's free online course: 100 days of Swift