If you haven’t checked out Day 1 in the journey, you can check that out right here.
Switch/Case
We’ll start this day with another type of flow control called switch/case statement. It’s important to keep in mind that Swift always want to be absolutely certain that there is a result, so in other words, it is exhaustive.
The reason why the switch/case statement above was able to execute is because we made sure it was exhaustive by adding the “default” portion, which takes care of the rest of the possible outcomes.
NOTE: You can also apply closed range operators in your cases before the colon to check for a range of possible values.
There are 2 other keywords to note that you can use, which are:
- “break”: breaks or stops the execution of a function immediately
- “fallthrough”: executes the subsequent case from the result
Although the result of the switch/case statement above is “Prints 1”, there is another result: “Prints 2”. This is because fallthrough makes the subsequent case from the original result execute as well.
Functions
The format of defining a function goes like this: you first use the func keyword, then the name of the function, then open and close parentheses, then a block of code marked by open and close braces.
You later call the function by writing the name of the function followed with an open and close parentheses.
“integer” is the parameter, which is the one value that the function accepts, and it should be of type Int or integer. The function is later called by adding the parameter and the desired value, so in this case “0”.
NOTE: You can make your function accept as many parameters as you want. Also, names of different functions can be the same, but their parameters must be different, or else it will lead to an error of invalid redeclaration.
To increase readability, Swift proposes an alternative approach by specifying one name when the parameter is called and another within the function. In other words, there is an external parameter and an internal parameter.
The external parameter can be an underscore or “_” if you want there to be no external parameter.
Conventionally, programmers will use “in”, “for” and “with” as external parameters to further increase readability.
Swift functions can return a value by inserting a “->” then a desired data type after their parameter list.
The function above needs to return a Boolean (true or false). By adding “return true”, we are fulfilling this.
Optionals
In short, an optional value is a value that might or might not have a value. To make something an optional, you simply add a question mark “?”. If an optional does have a value, that’s great but if it doesn’t have a value, then we call this “no value” as nil.
Nil is often correlated as being a bad idea because it can cause your users to be shown something that wasn’t intended, crash your code, or mess with your app’s logic.
As you probably can already guess, Swift wants to be extra careful, so the language does propose several solutions. The first solution is called optional unwrapping. The benefit of this approach is that it hits 2 birds with 1 stone by checking whether if an optional does indeed have a value and safely unwraps it to a non-optional type then running the block of code.
I safely unwrapped the optional String “String?” that is returned by the example function by writing “if let” then the name of the non-optional value, and equals to the example function or the optional value.
Food for thought: using the “firstIndex(of: value)” to an array will tell you what index that “value” is positioned.
There is another approach of dealing with optionals and that is by using the exclamation mark “!”, or otherwise known as force-unwrapping. If you are confident that the optional does have a value, then you have the choice to force unwrap the optional to gain access to its value.
NOTE: If there is no value when you thought there were, then your code will crash.
The last approach of dealing with optionals is called implicitly unwrapped optionals. This is similarly formatted as the second approach where you force unwrap an optional except. The logic behind an implicitly unwrapped optional is that it might or might not contain a value, but the key is that it does not need to be unwrapped before it is used.
Int! is an example of an implicitly unwrapped optional because it might or might not have a value, and it’s up to you to use it appropriately. You can treat it as if it is a regular optional such as “Int?” except that you have direct access to its value without the need to safely unwrap it.
The main uses of implicitly unwrapped optionals is when dealing with UI elements in UIKit (iOS) or AppKit (macOS), and though these are declared at the beginning or in-advance, they cannot be used until they have been created.
Optional Chaining
Optional chaining only allows you to run your code if the optional does have a value. The concept is like using the “uppercased()” method in the sense that whichever String you attach that method, the String must exist in order for it to be executed.
Optional chaining is visible through a question mark or “?” and it will run only if everything before the question mark has a value. Also, the optional chains can be as long as they want — Swift will just check from left to right until it finds nil (at which point it will stop).
Nil Coalescing Operator
The logic of the nil coalescing operator is as follows: “use choice #1 if you can, but if choice #1 is nil then use choice #2.”
The nil coalescing operator is indicated with two question marks or “??” and the benefit of using this approach is that it safely unwraps the optional while leaving a default value so that your code does not crash or return nil.
Enumerations
Enumerations or “enums” are an approach for you to define a new data type and the possible values that it can contain.
NOTE: Swift uses type inference so instead of having to type out “Enumeration.tiger”, instead you can just type “.tiger” and Swift will infer that you are deriving this from the enum.
You can add a Switch/Case within the function above, but make sure that include all possible scenarios as Swift is exhaustive.
You can use the let keyword to access the value inside a case, then the where keyword to specify the condition or criteria.
The important thing to take away is that the let keyword allows you to get ahold of the value inside the enum by declaring a constant name you can refer to then followed by a subsequent where condition to check.
NOTE: Switch/case evaluates cases starting from top to bottom, so if I were to put the case “.horse” above the case “.horse(let speed) where speed < 5:” then, then the case “.horse” will only be executed.
Structs
Defining a struct is relatively easy as Swift automatically generates what’s called a memberwise initializer, which means that you create the struct by passing in values for the struct’s properties.
If you have created an instance of a struct, you can read its properties by typing the name of the struct, a period, and then the desired property you want to read.
NOTE: Swift utilizes a technique called “copy on write” where it only copies your data if you try to change it.
Forming a copy of a struct does not change the original.
When you see a function inside a struct, we refer to it as a method. However, both functions and methods use the “func” keyword to define them. The only difference is the contexts to which they are located.
Classes
Just like structs, there is another complex data type called classes except there are several notable differences:
- You do not automatically get a memberwise initializer for your classes
- You can define a class based off another class
- An instance of a class is called an object. If you make a copy of that object, both the copy and the original object point to the same class so changing one will result in changing the other
Swift wants to make sure that all properties contain values, and this applies to both structs and classes. There are 3 possible solutions:
- Assign the classes’ properties as optionals
- Provide a default value for the classes’ properties
- Write out your own initializer for the class
The preferable or recommend approach is option 3. When defining an initializer method, do not attach the typical func. Also, if the parameter names are the same as the names of the properties of the class, then you must attach “self.” to make it distinct and clear.
NOTE: Self. just refers to the class itself so in other words, self.coffee refers to the Example class’ coffee property.
The major difference between structs and classes is that the latter can build on each other with the use of class inheritance.
In the screenshot above, the class SubClass is what’s referred to as the subclass and the class SuperClass is what’s referred to as the superclass or parent class. The override keyword indicates that you want to change a method from a parent class in a subclass.
If you want to add a new property to the subclass, then you must make sure to:
- Add the property in your initializer.
- The new initializer needs to call super.init() with any properties that were initialized in the parent class’ initializer. This super.init() needs to be called after all the new properties in the subclass has been added in the initializer.
- You cannot make method calls in the initializer until all properties have been given values in the initializer.
Alternatively, we can also do this:
If you want to work with Objective-C code, you can do so by using the “@objc” or “@objcMembers” keywords. Effectively, these attributes mark the methods as being available for older Objective-C code to run.
The difference between @objc and @objcMembers is that @objc are attached before individual methods whereas @objcMembers are attached before your class to automatically make all of its methods available to Objective-C.
Structs are referred to as “value types” because they point to a value whereas classes are referred to as “reference types” because objects are shared references to the real value.
As a basic rule, you should always be using structs over classes unless if you have a specific reason to be using classes.
Properties
Structs and classes are bundled as complex data types or “types”, and their variables and constants are referred to as properties.
When you use a property within a method, it will automatically use the values that belong to the same object, so for a struct’s case, a copy of an instance would use the copy’s defined values of its properties whereas for classes, it would be the same even for a copy because classes are “reference types”.
There are 2 types of property observers called willSet and didSet:
- willSet has access to a special value called “newValue”.
- didSet has access to a special value called “oldValue”.
newValue simply refers to what the new property value is going to be, whereas the oldValue refers to the old property value or previous value.
The logic behind computed properties is like using the uppercased() method or capitalized() method, in the sense that if these methods were attached to a String, for example, the uppercased or capitalized version of the String wouldn’t be stored except that it would be calculated.
To make a computed property, simply start an open brace after your property and use either the get or set to make an action happen.
NOTE: If you intend to use computed properties only for reading data, then you can just remove the get part entirely.
Static properties are known as shared properties, and you can simply create one by using the static keyword.
As static properties belong to the struct and not the instance of the struct, you can’t use it to access any non-static properties from the struct.
Access Control
Access controls are important to define because it specifies the level of access for data within structs and classes to be exposed to the outside world.
There are in total 5 to choose from in the case of property (starting from the least to most restrictive):
- Open: can only be attached to parent classes or subclasses and the access level is equivalent to public. Open allows others to subclass and override the property.
- Public: everyone can read and write the property
- Internal: all the files within the same folder can read and write the property
- File Private: only the specific single file can read and write the property
- Private: only the specific declaration can read and write the property
NOTE: If you want to use the “file private” access control, type it out as one word “fileprivate”.