Part 3: Swift basics
In the previous topic, we learned about various collection types in Swift.
Here is the link to the last article: https://medium.com/@varunkukade999/part-2-swift-6-0-beta-complete-basics-d4c6dc52b65f
Here are the topics we will learn in this article:
Topics covered:
1. Loops: for in, while, repeat-while
2. Conditional Statements: if else, switch,
3. Control transfer statements: continue, break, fallthrough, return, defer, guard
4. Functions
5. Closures
6. Enumerations
Loops
for-in loop:
let integers = [1, 2, 3, 4]
for integer in integers {
print(integer)
}
Output:
1
2
3
4
loop through dictionary:
let numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
for (animalName, legCount) in numberOfLegs {
print("\(animalName)s have \(legCount) legs")
}
Output:
spiders have 8 legs
ants have 6 legs
cats have 4 legs
use range operators:
for index in 1...5 {
print(index)
}
Output:
1
2
3
4
5
Add your own step up by using stride (from: through: by)
for integer in stride(from: 0, through: 30, by: 5) {
print(integer)
}
Output:
0
5
10
15
20
25
30
Here we started from 0 (from) up to 30 (through) and increased the integer after each iteration by 5 (by).
If you don’t want to include the last integer in the range, you just need to replace through with to.
for integer in stride(from: 0, to: 30, by: 5) {
print(integer)
}
Output:
0
5
10
15
20
25
while loop:
var integer = 0;
while(integer < 5){
print(integer)
integer += 1;
}
Output:
0
1
2
3
4
repeat-while loop:
var integer = 0;
repeat {
print(integer);
integer += 1;
} while(integer < 5)
Output:
0
1
2
3
4
The difference between while and repeat-while is that the repeat-while loop will run at least once irrespective of the condition mentioned.
Conditional Statements
if-else:
let integer = 0;
if integer == 0 {
print("0");
} else if integer == 1 {
print("1")
} else {
print("Other than 0 and 1")
}
You can also store the result of if-else inside some variable: (This will only run successfully on Swift 6.0 beta playground)
let integer = 0;
let char = if integer == 0 {
"Zero"
} else if integer == 1 {
"One"
} else {
"Other than 0 and 1"
}
print(char) //Prints "Zero"
Switch:
let someCharacter: Character = "1"
switch someCharacter {
case "0":
print("Zero")
case "1":
print("One")
default:
print("Other than 0 or 1")
}
Output: One
Some important concepts from Swift Switch:
- Adding a default case in the switch is a must.
- Swift Switch doesn't require a break as it finishes the execution as soon as it matches a certain case. This reduces common mistakes when someone forgets to add a break. Although you can add a break if you want.
- The body of each case should have at least one executable statement and the case body shouldn’t be left empty.
- One value can also be matched with multiple cases but the one that is matched first will be expected and the program will finish execution for the switch statement.
You can also assign the result of the switch to some variable: (This will only run successfully on Swift 6.0 beta playground)
let someCharacter: Character = "1"
let char = switch someCharacter {
case "0":
"Zero"
case "1":
"One"
default:
"Other than 0 or 1"
}
print(char) //Prints "One"
If you want to match a single case to match multiple values, mention values with a comma.
let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a", "b":
print("The letter a/b")
case "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
"n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z":
print("Letter other than a")
default:
print("Not a letter from ato z")
}
Output: The letter a/b
Switch case can also match with certain range:
let integer = 5
switch integer {
case 0...4:
print("Lies in 0-4")
case 4...9:
print("Lies in 4-9")
default:
print("Element not found")
}
Output: Lies in 4-9
Tuples with switch:_
let somePoint = (0, 0)
switch somePoint {
case (0, 0):
print("Matched with 0, 0")
case (_, 0): //This will match if first tuple value can be anything but 2nd tuple value should be 0
print("Matched with _, 0")
case (0, _): //This will match only if first tuple value is 0 and 2nd tuple value can be anything
print("Matched with 0, _")
case (-2...2, -2...2):
print("Matched with -2...2, -2...2")
default:
print("Didn't match with anything")
}
Output: Matched with 0, 0
You can use _ for the tuple value you want to ignore. If you see (0, 0) will match with all the switch cases but it finished execution after the first match.
Switch can also assign the tuple values with temporary constant or variable when any case is matched. These variables can only be used inside the body of the case.
let anotherPoint = (2, 0)
switch anotherPoint {
case (let x, 0):
print("on the x-axis with an x value of \(x)")
case (2, let y):
print("on the y-axis with a y value of \(y)")
case let (x, y):
print("somewhere else at (\(x), \(y))")
}
Output: on the x-axis with an x value of 2
Here
1. For the first case (let x, 0 ): This will be matched first. Value 2 will be assigned to the temporary variable x. You can only access this temporary variable inside the body of that specific case. At this point switch will finish its execution.
2. The second case could have been matched if the first case had not been matched.
3. If the first two cases are somehow not matched, 3rd (last) case will be tried for matching. As we don’t have any specific value to be matched in this case, this case always be matched irrespective of what you pass. Hence, both tuple values are temporarily assigned to x and y.
Hence there is no need to add a default case because we are completely sure that the last case will be matched always.
To extend this further, along with existing conditions for the case you can add additional conditions by using these temporary variables and where clause.
let anotherPoint = (2, 0)
switch anotherPoint {
case (let x, 0) where x == 3:
print("on the x-axis with an x value of \(x)")
case (2, let y):
print("on the y-axis with a y value of \(y)")
case let (x, y):
print("somewhere else at (\(x), \(y))")
}
Output: on the y-axis with a y value of 0
Now if you see the first case, it won’t be matched as we added an extra condition. Here, the second case will be matched and value 0 will be assigned to temporary variable y.
For a while, repeat-while, if-else statements and switch pattern matching statement (with where clause), conditions should resolve to a boolean value (either true or false) for them to work. If the condition resolves to a non-boolean value, swift throws an error.
Control transfer statements
continue: If used inside a loop, the loop will immediately stop the execution of the current iteration and start the execution of the subsequent iteration.
let integers = [1, 2, 3, 4, 5, 6]
for integer in integers {
if(integer % 2 == 0){
continue;
}
print("\(integer) is odd")
}
Output:
1 is odd
3 is odd
5 is odd
break:
break stops the execution of the current ongoing loop and transfers code control after the end of the loop.
let integers = [1, 2, 3, 4, 5, 6]
for integer in integers {
if(integer % 2 == 0){
break;
}
print("\(integer) is odd")
}
//Output: 1 is odd
fallthrough:
A Swift switch statement finishes its execution as soon as it gets matched with a specific case and doesn't go through the remaining cases at all. But if you want that even if a specific case is matched it should go to the next subsequent case, then add the “fallthrough” keyword at the end of that case.
let somePoint = (0, 0)
var finalResult = "";
switch somePoint {
case (0, 0):
finalResult += "(Matched with 0, 0) "
fallthrough;
case (_, 0): //This will match if first tuple value can be anything but 2nd tuple value should be 0
finalResult += "(Matched with _, 0 ) "
fallthrough
case (0, _): //This will match only if first tuple value is 0 and 2nd tuple value can be anything
finalResult += "(Matched with 0, _ ) "
fallthrough
case (-2...2, -2...2):
finalResult += "(Matched with -2...2, -2...2 ) "
default:
print("Didn't match with anything")
}
print(finalResult) //Prints (Matched with 0, 0) (Matched with _, 0 ) (Matched with 0, _ ) (Matched with -2...2, -2...2 )
In this example, input (0,0) is matching with all the cases. We added a fallthrough keyword for the cases and control execution fell to the next one even if the specific case was matched already.
You can label a loop. In the case of nested loops, if you want to use break statements you can mention which loop you want to exit by mentioning the loop name after the break statement.
outerLoop: for i in 1...5 {
print("Outer loop iteration: \(i)")
for j in 1...3 {
print(" Inner loop iteration: \(j)")
if j == 2 {
// Break out of the outer loop
break outerLoop
}
}
// This code will not be executed because of the break statement
print(" This will not be printed")
}
print("Loop ended")
Output:
Outer loop iteration: 1
Inner loop iteration: 1
Inner loop iteration: 2
Loop ended
guard:
A guard statement, like an if statement, executes statements depending on the Boolean value of an expression. You use a guard statement to require that a condition must be true for the code after the guard statement is executed. Unlike an if statement, a guard statement always has an else clause — the code inside the else clause is executed if the condition isn’t true.
func greet(person: [String: String]) {
guard let name = person["name"] else {
return
}
print("Hello \(name)!")
guard let location = person["location"] else {
print("I hope the weather is nice near you.")
return
}
print("I hope the weather is nice in \(location).")
}
greet(person: ["name": "John"])
// Prints "Hello John!"
// Prints "I hope the weather is nice near you."
greet(person: ["name": "Jane", "location": "Cupertino"])
// Prints "Hello Jane!"
// Prints "I hope the weather is nice in Cupertino."
If the guard
statement’s condition is met, code execution continues after the guard
statement’s closing brace. Any variables or constants that were assigned values using an optional binding as part of the condition are available for the rest of the code block that the guard
statement appears in.
If that condition isn’t met, the code inside the else
branch is executed. That branch must transfer control to exit the code block in which the guard
statement appears. It can do this with a control transfer statement such as return
, break
, continue
, or throw
, or it can call a function or method that doesn’t return, such as fatalError(_:file:line:)
.
Using a guard
statement for requirements improves the readability of your code, compared to doing the same check with a if
statement. It lets you write the code that’s typically executed without wrapping it in a else
block, and it lets you keep the code that handles a violated requirement next to the requirement.
defer:
defer keyword helps you to execute the code later. If you write some statements in defer, those statements will be executed just before the exit from the current scope.
var score = 1
if score < 10 {
defer {
print(score)
}
score += 5
}
// Prints "6"
Here defer was executed just before exit from the if statement scope. The code inside of the defer always runs, regardless of how the program exits that scope i.e. breaking out of for loop, early exit from function, etc. If your program stops running — for example, because of a runtime error or a crash — deferred code doesn’t execute.
This behavior makes defer useful for operations where you need to guarantee certain actions happen before the exit of that scope — like manually allocating and freeing memory, etc.
If there is more than 1 defer statement in the same scope, execution works in LIFO type. The one which is put last will be executed first.
if score < 10 {
defer {
print(score)
}
defer {
print("The score is:")
}
score += 5
}
// Prints "The score is:"
// Prints "6"
Functions:
Function definition with return type:
func greet(person: String = "Default Name") -> String {
let greeting = "Hello, " + person + "!"
return greeting
}
Function call:
let result1 = greet(person: "Anna");
let result2 = greet();
print(result1) //Prints Hello, Anna!
print(result2) //Prints Hello, Default Name!
In Swift, values passed while calling functions are called arguments. Values accepted at function definition are called parameters.
Return tuple from function:
func returnMultipleValues() -> (Int, Int) {
return (1, 2)
}
print(returnMultipleValues()) //Prints (1, 2)
You can even return optional from a function. Just append ? to return type. But you should handle them safely while accepting returns from from function call.
If the entire body of the function is only a single expression you don’t need to write a return keyword.
func returnMultipleValues() -> (Int, Int) {
(1, 2)
}
print(returnMultipleValues()) //Prints (1, 2)
While calling any function, you give the argument label, and then in the function definition by default same label is used as a parameter.
func greet(person: String) -> String {
let greeting = "Hello, " + person + "!"
return greeting
}
greet(person: "Anna");
Here if you see the “person” label is passed as an argument label and in the function definition same “person” label is used as the parameter name. Also inside the function same label is used to access the parameter. By default, it should be the same. But if you want to have a different argument label and parameter name you can do so like this:
func greet(person user: String) -> String {
let greeting = "Hello, " + user + "!"
return greeting
}
print(greet(person: "Anna")); //Prints Hello, Anna!
In the function definition, first, write the original label, and after that use the new label. Here “person” is original and “user” is the new one.
Or even if you want you can just skip passing the argument label while calling the function as: (Mostly this is used across the code as this looks simplified and neat).
func greet(_ user: String) -> String {
let greeting = "Hello, " + user + "!"
return greeting
}
print(greet("Anna")); //Prints Hello, Anna!
Just add the “ _” while accepting the argument in the function definition.
Variadic parameters:
The function can be called by passing more than one argument, and at the function definition, those passed arguments can be made available as an array. (Just like the rest parameter in javascript 😉)
func greet(_ persons: String...) {
for person in persons {
print(person)
}
}
greet("Anna", "Josef", "John");
Output:
Anna
Josef
John
Here variadic parameters are declared using three dots(…) after the parameter name. Now “persons” is an array with values “Anna”, “Josef”, and “John”. You can perform any operation that is available for an array in Swift.
In-Out parameters:
We already talked about this at https://medium.com/varunkukade999/part-2-swift-6-0-beta-complete-basics-d4c6dc52b65f.
Function parameters are constants by default. Trying to change the value of a function parameter from within the body of that function results in a compile-time error. This means that you can’t change the value of a parameter by mistake.
Check this example:
func greet(_ user: String) -> String {
user = "Joseph";
}
print(greet("Anna"));
Output: error: cannot assign to value: 'user' is a 'let' constant
If you want a function to modify a parameter’s value, and you want those changes to persist after the function call has ended, define that parameter as an in-out parameter instead.
func SwapValues(value1: inout String, value2: inout String){
value1 = "b";
value2 = "a";
}
var val1: String = "a";
var val2: String = "b";
SwapValues(value1: &val1, value2: &val2);
print(val1, val2) //Prints b a
Explanation of code: Normally when you pass a string as an argument to a function, a new copy is made and is passed. But if you wrote “&” for the argument while calling the function and the “inout” keyword in the function definition while accepting it, the string will be passed by reference. That means the passed string while calling the function and the received string in the function definition as a parameter now point to the same address in the memory. Hence changing the value of any of them will affect the value of other.
In Swift, functions are reference types. This means that when you assign a function to a variable or pass it as an argument, you are passing a reference to the same function, not a copy. Hence new variable also now points to same memory location where prev function was located.
Closures:
Closure is a block of code/functionality that can be used and passed around. Every function in Swift is considered a form of closure. Swift introduced closures to provide additional flexibility and power beyond what functions alone offer. Closures and functions are related, but closures introduce concepts and capabilities that are particularly useful in certain programming scenarios.
Closures take one of the three forms:
- Global functions: All global functions are closures.
func multiply(x: Int, y: Int) -> Int {
return x * y
}
Let’s say multiply is a global function. It’s defined at the global scope (outside of any class, struct, or enum) and can be called from anywhere in your code.
You can assign this function to some other variable/constant and this function can also be passed around the code by passing it as an argument to another function. This depicts that every global function is a form of closure.
2. Nested functions: In Swift, you can define one function inside another function and also return that function.
func makeCumulativeSum() -> (Int) -> Int {
var sum = 0
let addToSum: (Int) -> Int = { number in
sum += number
return sum
}
return addToSum
}
// Create a closure for cumulative sum
let cumulativeSum = makeCumulativeSum()
// Use the closure
print(cumulativeSum(5)) // Output: 5
print(cumulativeSum(10)) // Output: 15
print(cumulativeSum(3)) // Output: 18
// Create a new closure for cumulative sum
let cumulativeSum2 = makeCumulativeSum()
// Use the closure
print(cumulativeSum2(5)) // Output: 5
print(cumulativeSum2(10)) // Output: 15
print(cumulativeSum2(3)) // Output: 18
Here if you see, the addToSum function is defined inside makeCumulativeSum. Here makeCumulativeSum is the outer function and addToSum is the inner function. Here, the outer function returns a closure of type (Int) -> Int.
Capturing values: In this case, the closure (returned inner function) can capture (remember) the values/variables/properties from the scope where it was defined even after it is out of that scope. That means addToSum can remember the reference of the sum variable even if addToSum is returned from the outer function.
Here is the flow: makeCumulativeSum is called. A new variable is created as a sum. Now the addToSum closure is returned. This closure now remembers the reference of the sum variable created earlier. cumulativeSum is now a closure that can be called. When you call cumulativeSum with some number, the addToSum function is executed with the provided number and existing sum variable. Even if you repeatedly call the cumulativeSum function, it will access the same sum variable.
Whenever you call the makeCumulativeSum again, a new sum variable is created and the new closure is returned. That means every new closure stores a different sum reference. That means cumulativeSum and cumulativeSum2 are different closures and refer to different memory locations. Hence both refer to different sum variables.
3. Closure expressions:
Closure expressions are a way to write closure in a more brief, focused syntax. They are useful when you want to write shorter versions of functions/closures without declaring them completely and without naming them.
Let’s take an example:
func callAPI(callback: (String) -> Void) {
let status = "Success";
callback(status) // Call the function with the name
}
func printStatus(status: String) -> Void {
print("API call status is \(status)")
}
callAPI(callback: printStatus)
//Output: API call status is Success
Here I defined the greet function and passed it to the executeFunction as a callback. executeFunction executed the passed greet function and printed a result.
Now the same thing I can write using closure expression syntax as
func callAPI(callback: (String) -> Void) {
let status = "Success";
callback(status) // Call the function with the name
}
callAPI(callback: {
status in
print("API call status is \(status)")
}
)
//Output: API call status is Success
You can see the difference in defining the closure and the way of passing it as a callback. In closure expression syntax, we omitted the closure name, closure explicit declaration, argument types, return type, etc. Although whenever necessary it's good practice to write types in closure expression syntax also. We will see how to do that soon.
There are various forms of closure expression syntaxes you can use. Let’s take one example and understand them.
Swift’s standard library provides a method called sorted. It sorts a given array. It also accepts a callback called sorting closure. The output of sorting closure decides how sorting should be done.
Here’s the syntax without using closure expression.
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
func sortingClosureFunc(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
var reversedNames = names.sorted(by: sortingClosureFunc)
We need to sort names in reverse alphabetical order. sortingClosureFunc is a sorting closure passed. The sorting closure needs to return true if the first value should appear before the second value, and false otherwise. For example, when s1=”Chris” and s2=”Alex”, sorting closure returned true, and hence Chris will appear first before Alex which is what we want.
This can be written in closure expression syntax as follows:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
var reversedNames = names.sorted(by:
{
(s1: String, s2: String) -> Bool in
return s1 > s2
}
)
This is called “Full Closure Syntax”. Here we assigned the closure expression to the “by” directly. Then we mentioned arguments with types and then return type as bool. After that “in” keyword divides the arguments, and return type with the body of the expression. Expression retunes the output bool.
You can even omit the types of arguments and return type in this case. As the sorted function is called with the array of strings and it knows that it accepts the closure which returns a boolean, swift automatically infers these types.
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
var reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
Again to simplify you can even omit the return keyword because the body of the sorting closure expression contains a single expression itself and we directly return that same expression.
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
var reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
Again above can be simplified with the use of Shorthand Syntax.
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
var reversedNames = names.sorted(by: { $0 > $1 } )
How does Shorthand Syntax work?
Implicit Parameter Names: Swift provides implicit names for the closure’s parameters. In the shorthand syntax: $0 represents the first parameter. $1 represents the second parameter, and so on.
No Need for return Keyword: For closures that consist of a single expression, you don’t need to use the return keyword. The result of the expression is automatically returned.
When Not to Use Shorthand Syntax:
Complex Logic: When the closure involves multiple statements or complex logic, using shorthand syntax can reduce readability. In such cases, using the full closure syntax with explicit parameter names and return statements is preferable.
Multiple Parameters: If the closure has more than two parameters or involves complex operations on the parameters, the shorthand syntax might make the code harder to understand.
When to Use Shorthand Syntax:
1. Simple Closures: Shorthand syntax is best suited for closures that have a clear and straightforward purpose, often involving simple expressions. This includes operations like sorting, filtering, or mapping.
2. Single Expression: Use shorthand syntax when the closure contains a single expression or statement. This simplifies the code and removes boilerplate syntax.
3. Readability: While shorthand syntax can make the code more compact, it should be used when it improves readability. For more complex closures, where the logic is more involved, using the full closure syntax might be more appropriate for clarity.
Trailing Closures: You can even write closure expressions using trailing closures. But to do that, functions should accept the closures as one of the final arguments.
This is without trailing closure:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
var reversedNames = names.sorted(by:
{
(s1: String, s2: String) -> Bool in
return s1 > s2
}
)
This is with trailing closure:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
var reversedNames = names.sorted(){
(s1: String, s2: String) -> Bool in
return s1 > s2
}
As you see the difference, you write a trailing closure after the function call’s parentheses, even though the trailing closure is still an argument to the function. When you use the trailing closure syntax, you don’t write the argument label for the first closure as part of the function call.
Another example:
Without trailing closure:
func callAPI(callback: (String) -> Void) {
let status = "Success";
callback(status) // Call the function with the name
}
callAPI(callback: {
status in
print("API call status is \(status)")
}
)
With trailing closure:
func callAPI(callback: (String) -> Void) {
let status = "Success";
callback(status) // Call the function with the name
}
callAPI() {
status in
print("API call status is \(status)")
}
In case of multiple closures: If a function takes multiple closures (should be as last arguments), you omit the argument label for the first trailing closure and you label the remaining trailing closures:
func callAPI(successCallback: (String) -> Void, failureCallback: (String) -> Void) {
let status = "Failure";
if status == "Success" {
successCallback(status)
} else if status == "Failure" {
failureCallback(status)
}
}
callAPI() {
status in
print("API call status is \(status)")
} failureCallback: {
status in
print("API call status is \(status)")
}
Output: API call status is Failure
Here for 2nd closure, we label it as failureCallback.
Functions and Closures are reference types. If you assign one closure/function to any constant/variable, you are setting that constant or variable to be a reference to the function or closure.
Escaping closure:
var completionHandlers: [() -> Void] = [];
func gatherCompletions(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler);
}
gatherCompletions() {
print("Completion Handler function")
}
for eachCompletionHandler in completionHandlers {
eachCompletionHandler()
}
If you see the above code, we passed closure to the gatherCompletions function. We assigned the keyword @escaping to the received closure. We stored this closure in an outside variable completionHandlers. Then we returned from the function. After returning from that function, we looped through completionHandlers, and the closure stored inside was called.
Definition: If we pass a closure to any function as an argument and that closure can also be called even after returning from a function, then that closure is called escaping the closure. Because it escapes the scope where it has been passed originally.
Enumerations:
An enumeration defines a common type for a group of related values and enables you to work with those values in a type-safe way within your code.
In Swift, you can define an enum like this:
enum Direction {
case left
case right
}
print(Direction.left) //Prints "left"
print(Direction.right) //Prints "right"
As direction value can be either “left” or “right”, we defined the enum with those 2 values. Each enum defines some type. Here “Direction” is the name of that type. The values specified in an enumeration (such as left, and right) are its enumeration cases. You use the case keyword to introduce new enumeration cases.
As you see, the implicit value of any case is the provided case name after the case keyword.
let directionValue = Direction.left;
directionValue = .right
If you see the above example, we assigned some case to a variable. At this point, swift knows that the directionValue variable holds the case of type name Direction. Hence we omitted using “Type name” while reassigning it. While reassigning the enum case, you can omit to type name as Swift implicitly infers the type while initializing it the first time.
You can even write all cases on a single line as:
enum Direction {
case left, right
}
On the other side, if you want to check the value of the variable and decide which enum it matches, you can use the switch case.
enum Direction {
case left
case right
}
let directionValue = Direction.left;
switch directionValue {
case .left:
print("User is headed to Left direction")
case .right:
print("User is headed to Left direction"
}
// Prints "User is headed to Left direction"
Exhaustive case handling: You need to mention all cases of enumeration in the switch. If you don’t you will get an error.
enum Direction {
case left
case right
}
let directionValue = Direction.left;
switch directionValue {
case .left:
print("User is headed to Left direction")
}
Output: note: add missing case: '.right'
But if you don’t want to write all cases in the switch, you should use the default case.
enum Direction {
case left
case right
}
let directionValue = Direction.left;
switch directionValue {
case .left:
print("User is headed to Left direction")
default:
print("Default case")
}
// Prints "User is headed to Left direction"
Iterate:
enum Direction: CaseIterable {
case left
case right
}
for eachDirection in Direction.allCases {
print(eachDirection)
}
//Output:
left
right
Just write CaseIterable in front of the type name and while iterating use .allCases as Swift exposes a collection of all the cases as an allCases property of the enumeration type
Raw Values: If you want to give your own values to the enum cases, you can use raw values/default values. You can access raw values using .rawValue
enum Direction: Int {
case left = 0, right
}
print(Direction.left); //Prints "left"
print(Direction.left.rawValue) //Prints 0
Here left will be assigned “rawValue” as 0 and right will be assigned “rawValue” as 1. Here even if we omitted, Swift implicitly assigned “rawValue” to the right case based on the previous “rawValue”. Its perfectly fine if we give separate raw values to each of the cases.
You can also figure out the case by inputting rawValue. But here return value is optional as the given raw value may not be assigned to any case. Hence handle it safely.
print(Direction(rawValue: 1) ?? "NA") //Prints "right"
Associated values: Let’s take one scenario. We have two input values turnDirection string (which holds info where the user turned left or right) and the latitude and longitude of the turn location.
var turnDirection = "left" //can be "left" or "right"
var lattitude = 37.7749
var longitude = -122.4194
Now in this case, if want to store the direction along with latitude and longitude in the enum, we can do that:
enum Direction {
case left(latitude: Double, longitude: Double)
case right(latitude: Double, longitude: Double)
}
The above enum says “User can either turn left or right. If the user turned left, I have extra information about the turn. If the user turned right, I also have extra information about the turn”. Extra information is latitude and longitude in this case.
Extra information is called as “Associated values”. In enum along with each case, you can also store associated values for that case.
Depending on the turn the user took, I can assign the enum as follows:
var turnInfo: Direction;
if turnDirection == "left" {
turnInfo = Direction.left(latitude: lattitude, longitude: longitude)
} else {
turnInfo = Direction.right(latitude: lattitude, longitude: longitude)
}
On the other side if you want to check the turn and decide which enum it matched you can use a switch statement. If you have associated values along with that case, you can access them inside the switch case body.
switch turnInfo {
case .left(let latitude, let longitude):
print("Left Turn Info: \(latitude), \(longitude).")
case .right(let latitude, let longitude):
print("Right Turn Info: \(latitude), \(longitude).")
}
//Prints "Left Turn Info: 37.7749, -122.4194."
Can the above scenario be handled using collection types also?
Yes, why not? Although the above code can also be written by using a dictionary as follows:
var turnDirection = "left" //can be "left" or "right"
var lattitude = 37.7749
var longitude = -122.4194
var turnInfo: [String: Double] = [:]
if turnDirection == "left" {
turnInfo["direction"] = "left"
turnInfo["latitude"] = lattitude
turnInfo["longitude"] = longitude
} else {
turnInfo["direction"] = "right"
turnInfo["latitude"] = lattitude
turnInfo["longitude"] = longitude
}
if let direction = turnInfo["direction"] {
switch direction {
case "left":
let latitude = turnInfo["latitude"]!
let longitude = turnInfo["longitude"]!
print("Left Turn Info: \(latitude), \(longitude).")
case "right":
let latitude = turnInfo["latitude"]!
let longitude = turnInfo["longitude"]!
print("Right Turn Info: \(latitude), \(longitude).")
default:
print("Unknown direction")
}
}
But there are certain advantages to using enum in this specific scenario. We already know there are two turns right or left so it can be easily simplified as 2 cases. This also ensures type safety.
Use Enums when you have a predefined set of distinct cases, need to ensure type safety, and require exhaustive case handling.
Use Dictionaries when you need flexibility, deal with dynamic data, or need a simple key-value mapping without additional constraints.
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
That’s it for this article. See you at the next one. If you have any questions/concerns let me know in the comments.
If you found this tutorial helpful, don’t forget to give this post 50 claps👏 and follow 🚀 if you enjoyed this post and want to see more. Your enthusiasm and support fuel my passion for sharing knowledge in the tech community.
I always learn something new and share it with the community. You can find more of such articles on my profile -> https://medium.com/@varunkukade999