100 Days of SwiftUI Checkpoint 4

Course Credit:
This blog post's content is based on the fantastic learning materials from Paul Hudson's 100 Days of SwiftUI course, available for free at Hacking with Swift.
Functions in Swift
Functions are the building blocks that allow you to group related code, give it a name, and reuse it whenever you need. They are essential for writing clean, organized, and efficient code.
Anatomy of a Function
- Defining a function: Use the
func
keyword, followed by a unique name. The function's code is enclosed in curly braces{}
. - Parameters: Functions can accept values, called parameters, to customize their behavior. You list them inside parentheses, giving each a name and a type.
- Return values: Functions can return a value using the
->
syntax, followed by the return type. Thereturn
keyword is used to send the value back to the caller.
Key Concepts
Parameters and Arguments
- Parameters are placeholders for data inside the function's definition.
- Arguments are the actual values passed into the function when it's called.
- You can provide default values for parameters, making them optional to provide when calling the function.
- You can use an external parameter name to make your function calls more readable, or use an underscore
_
to omit the label entirely.
Returning Values
- A function can return a single value of a specified type.
- For a function with a single-line expression, the
return
keyword can be omitted. - To return multiple values, you can use a tuple, a lightweight data structure that groups different values together.
Overloading Functions
- You can have multiple functions with the same name as long as their parameter lists are different. This is called function overloading. The compiler determines which function to call based on the number and type of arguments provided.
Error Handling
- Functions can fail, and Swift's error handling provides a safe way to deal with this.
- You define custom errors using an
enum
that conforms to theError
protocol. - A function that can fail is marked with the
throws
keyword. - When calling a throwing function, you must use
try
within ado-catch
block to handle potential errors. This ensures your code is robust and gracefully handles unexpected issues.
import UIKit
//--- How to reuse code with functions
// Basic functions without parameters or return values
func showWelcome() {
print("Welcome to my app!")
print("By default this prints out a conversion")
print("chart from centimeters to inches, but you")
print("can also set a custom range if you want.")
}
showWelcome()
func evenOdd() {
let number = 139
if number.isMultiple(of: 2) {
print("Even")
} else {
print("Odd")
}
}
evenOdd()
func randomNumber() {
let roll = Int.random(in: 1...20)
print("Today's number of the day is \(roll)")
}
randomNumber()
// Function with parameters
func printTimesTable(number: Int, end: Int) {
for i in 1...end {
print("\(i) * \(number) is \(i * number)")
}
}
printTimesTable(number: 5, end: 20)
// Functions with return values
func roolDice() -> Int {
return Int.random(in: 1...6)
}
let result = roolDice()
print(result)
let root = sqrt(169)
print("The square root of 169 is \(root)")
// --- Two strings that contain the same letters test
// Function to check if two strings are anagrams (same letters)
func letters() -> Bool {
let firstString = "hello"
let secondString = "olleh"
return firstString.sorted() == secondString.sorted()
}
print(letters())
// Function to check anagrams with parameters
func letters2(string1: String, string2: String) -> Bool {
let first = string1.sorted()
let second = string2.sorted()
return first == second
}
print(letters2(string1: "hello", string2: "olleh"))
// --- If you have one line of code you can remove the "return"
func letters3(string1: String, string2: String) -> Bool {
string1.sorted() == string2.sorted()
}
print(letters3(string1: "hello", string2: "olleh"))
print(letters3(string1: "abcd", string2: "dcba"))
print(letters3(string1: "dog", string2: "cat"))
// Functions that return a value
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)
func pythagorasSimplified(a: Double, b: Double) -> Double {
sqrt(a * a + b * b)
}
let cSimple = pythagorasSimplified(a: 3, b: 4)
print(cSimple)
// --- How to return multiple values from functions
// Returning a single boolean value
func upperCase(string: String) -> Bool {
string == string.uppercased()
}
print(upperCase(string: "hello"))
// --- Tuple: A lightweight data structure that groups multiple values of potentially different types into a single unit
func getUser() -> (firstName: String, lastName: String) {
(firstName: "Taylor", lastName: "Swift")
}
let user = getUser()
print("Hello \(user.firstName) \(user.lastName)")
// Tuple without named values
func getUser1() -> (String, String) {
("Linkin", "Park")
}
let user1 = getUser1()
print("Hello \(user1.0) \(user1.1)")
// --- How to customize parameter labels
// A function with both an internal and external parameter name
func printTimesTable(for number: Int, end: Int) {
for i in 1...end {
print("\(i) x \(number) = \(i * number)")
}
}
printTimesTable(for: 3, end: 20)
// A function with multiple parameters and different labels
func rollDice(sides: Int, count: Int) -> [Int] {
var rolls = [Int]()
for _ in 0..<count {
let roll = Int.random(in: 1...sides)
rolls.append(roll)
}
return rolls
}
let rolls = rollDice(sides: 6, count: 5)
print(rolls)
// Overloaded functions (same name, different parameters)
func hireEmployee(name: String) { }
func hireEmployee(title: String) { }
func hireEmployee(location: String) { }
let lyric = "I see a red door and I want it painted black."
print(lyric.hasPrefix("I see"))
// A function that uses an underscore to omit the external parameter name
func isUppercase(_ string: String) -> Bool {
string == string.uppercased()
}
let string1 = "HELLO"
let result1 = isUppercase(string1)
// --- How to provide default values for parameters
func printTimesTable1(for number: Int, end: Int = 12) {
for i in 1...end {
print("\(i) x \(number) = \(i * number)")
}
}
printTimesTable1(for: 3, end: 20) // overrides the default
printTimesTable1(for: 8) // uses the default for 'end'
// --- How to handle errors in functions
enum PasswordError: Error {
case short, obvious
}
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"
}
}
let string9 = "12345"
do {
let result9 = try checkPassword(string9)
print("Password rating: \(result9)")
} 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!")
}
/*------ Checkpoint 4
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:
1. You can’t use Swift’s built-in sqrt() function or similar – you need to find the square root yourself.
2. If the number is less than 1 or greater than 10,000 you should throw an “out of bounds” error.
3. You should only consider integer square roots – don’t worry about the square root of 3 being 1.732, for example.
4. If you can’t find the square root, throw a “no root” error.
As a reminder, if you have number X, the square root of X will be another number that, when multiplied by itself, gives X. So, the square root of 9 is 3, because 3x3 is 9, and the square root of 25 is 5, because 5x5 is 25.
*/
print("Begin code challenge")
enum SqrtError: Error {
case outOfBounds
case noIntRoot
}
func squareRoot(_ number: Int) throws -> Int {
if number < 1 || number > 10000 {
throw SqrtError.outOfBounds
}
for i in 1...100 {
if i * i == number {
return i
}
}
throw SqrtError.noIntRoot
}
let numberToSquareRoot = 81
do {
let result = try squareRoot(numberToSquareRoot)
print("Square root of \(numberToSquareRoot) is: \(result)")
} catch SqrtError.outOfBounds {
print("Error: The number is out of bounds!")
} catch SqrtError.noIntRoot {
print("Error: The number has no integer square root!")
} catch {
print("An unknown error occurred!")
}
/*
Recap of Functions:
- **Reuse code:** Functions let you package code with a name, so you can call it multiple times.
- **Syntax:** They start with the `func` keyword, followed by a name, and have a body inside curly braces `{}`.
- **Parameters:** Functions can accept parameters to customize their behavior. You list them with a colon and their type.
- **Parameter Labels:** You can customize the name of parameters when calling the function (the external label) to make the code more readable, or use `_` to omit the label entirely.
- **Default Values:** Parameters can have default values, so you only need to specify them when you want to use a different value.
- **Return Values:** Functions can return a single value by specifying the return type with `->`.
- **Tuples:** For returning multiple values, the best option is a tuple. Tuples are lightweight data structures with a fixed size and named values.
- **Error Handling:** Functions can throw errors, which you define with an `enum` that conforms to the `Error` protocol. You then use `do`, `try`, and `catch` blocks to handle these errors.
*/
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.
As a reminder, if you have number X, the square root of X will be another number that, when multiplied by itself, gives X. So, the square root of 9 is 3, because 3x3 is 9, and the square root of 25 is 5, because 5x5 is 25.
Here is a commented breakdown of the code challenge.
// This enum defines the two types of errors we need to throw.
// The 'outOfBounds' case is for numbers outside the range of 1 to 10,000.
// The 'noIntRoot' case is for numbers that don't have an integer square root.
enum SqrtError: Error {
case outOfBounds
case noIntRoot
}
// This function calculates the integer square root of a number.
// It can throw one of the errors defined above.
func squareRoot(_ number: Int) throws -> Int {
// This if statement checks if the number is within the valid range.
// If it's not, we immediately throw the outOfBounds error.
if number < 1 || number > 10000 {
throw SqrtError.outOfBounds
}
// Since the maximum number is 10,000, we know the largest possible
// integer square root is 100. We can loop from 1 to 100 to find it.
for i in 1...100 {
// We're checking if the square of our loop variable 'i'
// is exactly equal to the input number.
if i * i == number {
// If we find a match, we've found the integer square root.
// We can return 'i' immediately and stop the function.
return i
}
}
// If the loop finishes without finding a perfect square,
// it means the number doesn't have an integer square root.
// In this case, we throw the noIntRoot error.
throw SqrtError.noIntRoot
}
let numberToSquareRoot = 81
do {
let result = try squareRoot(numberToSquareRoot)
print("Square root of \(numberToSquareRoot) is: \(result)")
} catch SqrtError.outOfBounds {
print("Error: The number is out of bounds!")
} catch SqrtError.noIntRoot {
print("Error: The number has no integer square root!")
} catch {
print("An unknown error occurred!")
}
let noRootNumber = 20
do {
let result = try squareRoot(noRootNumber)
print("Square root of \(noRootNumber) is: \(result)")
} catch SqrtError.outOfBounds {
print("Error: The number is out of bounds!")
} catch SqrtError.noIntRoot {
print("Error: The number has no integer square root!")
} catch {
print("An unknown error occurred!")
}