# Swift/iOS CheatSheet
[TOC]
## Swift <img src="https://hackmd.io/_uploads/rJ76dEq-0.png" style="width:25px;"/>
### [Collections](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/collectiontypes)
> In Swift, collections are data structures that allow you to store and manipulate multiple values in a single variable or constant
* **Arrays** - Ordered collections of values of the same type
* **Sets** - Unordered collections of unique values of the same type
* **Dictionaries** - Collections of key-value pairs where each key is unique
* **Tuples** - Fixed-size collections of values that can be of any type
Their differences may seem trivial, but it actually calls for different use cases for each data type.
1. **Arrays:** Storing a list of items in a specific order, such as:
* To-do list items
* User preferences
* Scores in a game
2. **Sets:** Storing a collection of unique items, such as:
* Unique usernames
* Tags or categories associated with an item
* IDs of selected items
3. **Dictionaries:** Storing key-value pairs for quick lookups, such as:
* Mapping student names to their grades
* Storing configuration settings with keys and values
* Representing JSON or property lists as dictionaries
4. **Tuples:** Temporary grouping of values, such as:
* Returning multiple values from a function
* Passing parameters to a function as a single argument
* Storing coordinates (x, y) for a point
```
// Arrays
var toDoList: [String] = ["Buy groceries", "Finish homework", "Call mom"]
toDoList.append("Go for a run") // Insertion
let thirdItem = toDoList[2] // Lookup by index
// Sets
var uniqueUsernames: Set<String> = ["user1", "user2", "user3"]
uniqueUsernames.insert("user4") // Insertion
let containsUser2 = uniqueUsernames.contains("user2") // Lookup by value
// Dictionaries
var studentGrades: [String: Double] = ["Alice": 95.0, "Bob": 87.5, "Charlie": 91.3]
studentGrades["David"] = 88.2 // Insertion
if let gradeForAlice = studentGrades["Alice"] { // Lookup by key
print("Grade for Alice: \(gradeForAlice)")
}
// Tuples
let person: (name: String, age: Int, city: String) = ("Alice", 30, "New York")
let name = person.name // Lookup by label
```
**Things to note**
**Speed**
Sets and dictionaries use hashing techniques to create a unique hash value and store their data in a hash table. Thus, they have faster look up times as compared to tuples and arrays
```
import Foundation
// Generate sample data
let size = 20000
let values = Array(1...size)
let keys = Set(values)
let dict = Dictionary(uniqueKeysWithValues: zip(values, values))
let tuple = (1...size)
// Test lookup timings for arrays
let startTimeArray = CFAbsoluteTimeGetCurrent()
for value in values {
let _ = values.contains(value)
}
let endTimeArray = CFAbsoluteTimeGetCurrent()
print("Array lookup time: \(endTimeArray - startTimeArray)")
// Test lookup timings for sets
let startTimeSet = CFAbsoluteTimeGetCurrent()
for value in values {
let _ = keys.contains(value)
}
let endTimeSet = CFAbsoluteTimeGetCurrent()
print("Set lookup time: \(endTimeSet - startTimeSet)")
// Test lookup timings for dictionaries
let startTimeDict = CFAbsoluteTimeGetCurrent()
for value in values {
let _ = dict[value]
}
let endTimeDict = CFAbsoluteTimeGetCurrent()
print("Dictionary lookup time: \(endTimeDict - startTimeDict)")
// Test lookup timings for tuples (just iterating, as tuples don't have lookup methods)
let startTimeTuple = CFAbsoluteTimeGetCurrent()
for value in tuple {
let _ = value
}
let endTimeTuple = CFAbsoluteTimeGetCurrent()
print("Tuple lookup time: \(endTimeTuple - startTimeTuple)")
```
Results
* Set Lookup Time: 0.002212047576904297 seconds
Sets are the fastest for lookup operations among the tested collection types. This is because sets use hashing techniques for quick membership checks. The lookup time is very low, indicating efficient performance.
* Dictionary Lookup Time: 0.002315998077392578 seconds
Dictionaries are also highly efficient for lookup operations, following closely behind sets. Dictionaries use hashing to quickly locate key-value pairs, resulting in fast lookup times. The slightly higher lookup time compared to sets may be due to the additional step of finding the corresponding value for the given key.
* Tuple Lookup Time: 0.005738019943237305 seconds
Tuples have a higher lookup time compared to sets and dictionaries. This is because tuples don't have built-in lookup methods, so accessing elements typically involves iterating through the tuple's elements sequentially. As a result, the lookup time is higher compared to collections with optimized lookup operations.
* Array Lookup Time: 17.936594009399414 seconds
Arrays have the slowest lookup time among the tested collection types. In arrays, accessing elements by index involves linear search, which becomes inefficient as the size of the array increases. As a result, the lookup time is significantly higher compared to collections with faster lookup methods like sets and dictionaries.
While sets and dictionaries have O(1) lookup times, in the worst case scenario it can be O(n) due to collisions
**Behaviours**
Accessing a value that is not in an array leads to an error, whereas for dictonaries, it simply returns nil. This type of behaviour should be considered when deciding which data type to use. For example, if we want to store student grades, and a student has yet to take the exam, we should use a dictionary that returns nil or even better, a default value when trying to access a student score that is not yet present.
```
let scores = [72, 88]
let students = ["Alice", "Bob", "Charlie"]
let charlieIndex = students.firstIndex(of: "Charlie")
let charlieScore = scores[charlieIndex!] // Index out of range
let studentScores = ["Alice": 72, "Bob": 88]
let charliesScore = studentScores["Charlie"] // nil
let charlieDefaultScore = studentScores["Charlie", default: 0] // 0
```
**Creating empty collections**
```
// Using angle braket syntax
var words = Set<String>()
var scores = Dictionary<String, Int>()
var results = Array<Int>()
// Using parentheses. Only for dictionary and arrays
var teams = [String: String]()
var results = [Int]()
```
Note: We cannot create empty tuples because they are fixed in sized. But we can create tuples with default values first and modify them later.
```
let emptyTuple = (String(), Int())
```
### [Objects](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/classesandstructures)
> Structures and classes are general-purpose, flexible constructs that become the building blocks of your program’s code.
**Structs:**
* Definition: Structs are value types that allow you to encapsulate related properties and behaviors into a single data structure.
* Properties: Structs can have stored properties (variables) and computed properties (derived values).
* Methods: Structs can have methods to perform operations or computations on their properties.
* Initializer: Structs automatically receive a memberwise initializer that initializes all of their properties.
* Immutability: By default, struct instances are immutable. If you need mutability, you can declare properties as var.
* Advantages:
* Lightweight: Structs are generally more lightweight than classes, making them a good choice for simple data types or when performance is a concern.
* Value Semantics: Structs use value semantics, meaning they are copied when passed around in your code. This can prevent unintended side effects and makes them well-suited for small, self-contained pieces of data.
* Thread Safety: Value semantics also make structs inherently thread-safe, as each instance operates independently.
**Classes:**
* Definition: Classes are reference types that encapsulate properties and behaviors into objects.
* Inheritance: Classes support inheritance, allowing one class to inherit properties and methods from another class.
* Identity: Objects of class types have a unique identity, allowing multiple references to point to the same instance.
* Deinitialization: Classes have deinitializers, allowing you to perform cleanup tasks when an instance is deallocated.
* Advantages:
* Inheritance: Classes support subclassing and inheritance hierarchies, enabling code reuse and promoting modular design.
* Reference Semantics: Class instances are reference types, meaning they are passed by reference rather than being copied. This can be advantageous for managing shared state and complex object graphs.
* Dynamic Dispatch: Classes support dynamic method dispatch, allowing for runtime polymorphism and dynamic behavior through method overriding.
**Differences/Advantages:**
* Mutability: Structs are immutable by default, while classes are mutable. This can affect how you design and use them in your code.
* Copy vs Reference Semantics: Structs use value semantics (copying), while classes use reference semantics (sharing). This impacts how changes to instances propagate in your code.
* Performance: Structs are generally more performant for small, simple data types, while classes are more suitable for complex object hierarchies and shared state.
### [Computed vs Stored properties](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties#Stored-Properties-and-Instance-Variables)
> Stored properties store constant and variable values as part of an instance, whereas computed properties calculate (rather than store) a value. Computed properties are provided by classes, structures, and enumerations. Stored properties are provided only by classes and structures.
```
var computed: SomeType {
// some method
}
var stored: SomeType = {
// some method
}()
```
Both computed and stored are both similar looking closures but they behave differently and thus have different use cases.
**Computed**
* Action block is run everytime the property is referenced
* Must be a `var` cannot be a `let` constant
* Cannot explicitly reassign it a value
* Must declare type
* Good if the property has dependencies that might change in value as it ensures we always get the most up to date value of the property
* It is not called by default upon Class initialisation unless explicitly stated. Thus, its dependencies do not usally have to be ready during Class initialisation
**Stored**
* Action block called once upon initialisation.
* Can be a *var* or *let* constant
* If it is a `var`, it can be reassigned a value like a usual `var` can
* Not compulsory to declare type
* Good if the property dependencies do not mutate
* Is called immediately upon Class initialisstion. Thus, dependencies must be ready during Class Initialisation.
```
// Computed property
var totalCPFBalance: Int {
getOABalance() + getSABalance + getMedisaveBalance()
}
// Stored property
// Dependencies must al be ready
let mathScore = 95
let scienceScore = 75
let englishScore = 71
let chineseScore = 70
let psleScore = {
(mathScore + scienceScore + englishScore + chineseScore)*3/4
}()
```
**Important Note**
Take note how you reference closures. putting a `()` behind a closure means you are evaluating it immediately. Without the `()` means you are simply referencing the closure.
```
var someClosure: () -> Int = {
// some work
}
var a = someClosure
var b = someClosure()
let psleScore = {
(mathScore + scienceScore + englishScore + chineseScore)*3/4
}()
let psleScoreCalculator = {
(mathScore + scienceScore + englishScore + chineseScore)*3/4
}
```
* Variable `a` references the closure and thus, is of type `() -> Int`
* Variable `b` evaluates the closure which returns an `Int`, thus, variable `b` is of type `Int`
* `psleScore` is an `Int`
* `psleScoreCalculator` is of type `() -> Int`
### [Property Wrappers](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties/#Property-Wrappers)
> A property wrapper adds a layer of separation between code that manages how a property is stored and the code that defines a property.
When you have multiple variables that need to conform to the same rule, such as `var firstname: String` and `var lastName: String` bothing needing to be uppercased, we could manually call `.uppercased()` whenever we are setting them or using them. To ensure safety and consistency, we could use property observers or computed properties.
```
// Property Observers
var firstName: String {
didSet {
firstName = firstName.uppercased()
}
}
var lastName: String {
didSet {
lastName = lastName.uppercased()
}
}
// Computed Property
private var _firstName: String = ""
var firstName: String {
set {
_firstName = newValue.uppercased()
}
get {
return _firstName
}
}
private var _lastName: String = ""
var lastName: String {
set {
_lastName = newValue.uppercased()
}
get {
return _lastName
}
}
```
Either way, it makes your code long and repetitive. We could wrap the property oberver or computed property logic up in what we call **Property Wrappers**
```
@propertyWrapper // Wrap property observer
struct AllCaps {
var wrappedValue: String {
didSet {
wrappedValue = wrappedValue.uppercased()
}
}
init(wrappedValue: String) {
self.wrappedValue = wrappedValue.uppercased()
}
}
@propertyWrapper // Wrap computed property
struct AllCaps {
private var name: String
var wrappedValue: String {
set {
name = newValue
}
get {
return name.uppercased()
}
}
init(wrappedValue: String) {
self.name = wrappedValue
}
}
```
Now, we can simply use the property wrapper in the folloing manner.
```
@AllCaps var firstName: String
@AllCaps var lastName: String
```
#### Useful Reading
[Property Wrappers in Swift by Evangelist Apps](https://medium.com/@EvangelistApps/property-wrappers-in-swift-51cee87e2c32)
[Property wrappers in Swift by Sundell](https://www.swiftbysundell.com/articles/property-wrappers-in-swift/)
### Closures: Returning closures and capturing variables
Closures can be returned from functions in the following manner.
```
func divePackageCalculator(package: Package) -> (Int, Int) -> Int
```
Having 2 `->` might seem confusing but it literally means the funtion would return a closure that is defined `(Int, Int) -> Int`
Here is an example of how this concept can be used.
```
enum Package {
case roomOnly, halfBoard, fullBoard
}
func divePackageCalculator(package: Package) -> (Int, Int) -> Int {
switch package {
case .fullBoard:
return {
// $100 per day and $50 per dive
100*$0 + 50*$1*$0
}
case .halfBoard:
return {
// $70 per day and $55 per dive
70*$0 + 55*$1*$0
}
case .roomOnly:
return {
// $50 per day and $60 per dive
50*$0 + 60*$1*$0
}
}
}
let numDays = 3
let divesPerday = 1
let package = Package.halfBoard
let costCalculator = divePackageCalculator(package: package)
let cost = costCalculator(numDays, divesPerday)
print(cost)
```
If you use any external values inside your closure, Swift captures them – stores them alongside the closure, so they can be modified even if they don’t exist any more.
```
enum Package {
case roomOnly, halfBoard, fullBoard
}
func divePackageCalculator(package: Package) -> (Int, Int) -> Int {
var counter = 1
switch package {
case .fullBoard:
return {
100*$0 + 50*$1*$0
}
case .halfBoard:
return {
counter += 1
print(counter)
return 70*$0 + 55*$1*$0
}
case .roomOnly:
return {
50*$0 + 60*$1*$0
}
}
}
let costCalculator = divePackageCalculator(package: .halfBoard)
var cost = costCalculator(3,5)
cost = costCalculator(2,1)
cost = costCalculator(2,4)
```
#### Useful reading
[Returning closures from functions - HackingWithSwift](https://www.hackingwithswift.com/sixty/6/10/returning-closures-from-functions#:~:text=In%20the%20same%20way%20that,specify%20your%20closure's%20return%20value.)
[Capturing values - - HackingWithSwift](https://www.hackingwithswift.com/sixty/6/11/capturing-values)
### [Closures: Non-escaping, @escaping and @autoclosure](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures)
> Closures are self-contained blocks of functionality that can be passed around and used in your code.
#### Non-Escaping Closures
A closure that is called inside the function it was supplied into, that is, before it returns, is referred to as a non-escaping closure. This closure never exceeds the function it was supplied into boundaries. The function, when given a closure as a parameter, executes the closure and returns the compiler. The passed closure in this case no longer exists in memory once the closure has been executed and execution has concluded since it has left the scope. During the course of their lifetime, non-escaping closures change into different states.
The @nonescaping closure lifecycle is as follows:
1. When calling the function, pass the closure as a parameter.
2. Do some further work with function.
3. The closure is run by the function.
4. The function gives back the compiler.
```
func doWork(completion: () -> Void) {
// Do some work
completion()
}
```
#### Escaping Closures
A closure that is called after the function it was supplied to returns is referred to as an escaping closure. It outlives the function it was supplied to, in other words. an escaping closure can be thought of as code that outlives the function An error will be thrown if such a closure is not marked with @escaping.
The @escaping closure lifecycle is as follows:
1. When calling the function, pass the closure as a parameter.
2. Put a little extra effort into function.
3. Asynchronously or stored, the function executes the closure.
4. The compiler is returned by the function.
```
class SomeClass {
var someHandler: () -> Void = {}
init(handler: @escaping () -> Void) {
someHandler = handler
}
}
func loadData(completion: @escaping (_ data: Data?) -> Void) {
DispatchQueue.global().async {
let data = try? Data(contentsOf: url)
DispatchQueue.main.async {
completion(data)
}
}
}
```
#### Autoclosures
An autoclosure is a closure that is automatically created to wrap a piece of code. This means that when you call a function that has an `@autoclosure` parameter, you can pass in an expression as if it were a direct argument rather than wrapping it in a closure. This allows for cleaner and more readable code, especially when defining default arguments
```
let age = getAge()
func assertWithClosure(_ condition: () -> Bool, _ message: () -> String = {""}) {
if !condition() {
print(message())
}
}
assertWithClosure({ age >= 0 }, { "Age cannot be negative" })
func assertWithAutoClosure(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = "") {
if !condition() {
print(message())
}
}
assertWithAutoClosure(age >= 4, "Age cannot be negative")
```
You might be wondering why we even need a closure for arguments like `message`. The next segment will cover that.
### Short Circuiting and delayed evaluation
Short-circuiting is a programming concept where the evaluation of expressions stops as soon as the outcome is determined. It is commonly used in logical operations such as `&&` (AND) and `||` (OR).
```
func and(_ lhs: Bool, _ rhs: @autoclosure () -> Bool) -> Bool {
guard lhs else { return false }
return rhs()
}
func or(_ lhs: Bool, _ rhs: @autoclosure () -> Bool) -> Bool {
guard !lhs else { return true }
return rhs()
}
```
In the above examples, if the `lhs` already determines the result, the function will not bother to evaluate the `rhs`.
Now lets take a look back at why the `asset` method uses closures as arguments. (Take note, there is no difference in behaviours whether you use autoclosure or regular closures).
```
func constructString(_ string: String) -> String {
print("Constructing string")
return string
}
let age = 4
func assertNoClosure(_ condition: () -> Bool, _ message: String) {
if !condition() {
print(message)
}
}
assertNoClosure({age >= 0}, constructString("Age cannot be negative")) // Constructing string is printed
func assertWithAutoClosure(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String) {
if !condition() {
print(message())
}
}
assertWithAutoClosure(age >= 4, constructString("Age cannot be negative")) // Constructing string is not printed
```
For the function, `assertNoClosure()` whereby the `message` argument is a `String`, `message` has to be constructed regarless of whether it is being used. Of course Strings are lightweight, but if you are passing in a much more complex custom class, you might not want to construct it unless it is being used. Thus, with closures, we can delay the construction of objects till when we need it, if we need it.
#### Useful Reading
[Escaping and Non-Escaping Closures in Swift - GeekforGeeks](https://www.geeksforgeeks.org/escaping-and-non-escaping-closures-in-swift/)
[How to use @autoclosure in Swift to improve performance - SwiftLee](https://www.avanderlee.com/swift/autoclosure/)
[Using @autoclosure when designing Swift APIs - SwiftbySundell](https://www.swiftbysundell.com/articles/using-autoclosure-when-designing-swift-apis/)
[What is the autoclosure attribute? - HackingWithSwift](https://www.hackingwithswift.com/example-code/language/what-is-the-autoclosure-attribute)
### [Variadic functions](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/functions#Variadic-Parameters)
> Variadic functions in Swift are functions that can accept a variable number of arguments of the same type.
They are particularly useful when you want to create functions that operate on collections of values without restricting the number of elements that can be passed. Variadic arguments are treated as an array within the function.
```
func StudentResult(_ marks:Int...) -> Int{
var sum = 0
for i in marks{
sum += i
}
return sum
}
var summ = StudentResult(23, 45, 66, 77, 88, 66)
```
#### Variadic parameters with other parameters
In function, we can are allowed to use variadic parameters along with other parameters. When we are using a variadic parameter with other parameters then the variadic parameter is placed at the first position then the other parameters are used. It is necessary for the parameter that comes after the first variadic parameter must contain the argument label because it will make parameters unambiguous so that it is easy for the compiler to understand which argument is passed to which parameter either for variadic or the parameter that comes after the variadic parameter.
```
func StudentResult(_ marks:Int..., Sname: String){
var sum = 0
for i in marks{
sum += i
}
print("Name of the student: ", Sname)
print("Total marks of semester 1: ", sum)
}
StudentResult(23, 45, 66, 77, 88, 66, Sname: "Rohan")
```
#### Multiple Variadic parameters
A swift function can contain multiple variadic parameters starting from Swift 5.4. The earlier version of Swift doesn’t support multiple variadic parameters in the function, if you try to use them you will get a compiler error. In multiple variadic parameters, it is necessary that all the variadic parameters must contain the argument label because it will make parameters unambiguous so that it is easy for the compiler to understand which argument is passed to which parameters. If we do not use the argument label at the time of calling the function then the compiler assigned all the values to the first parameter and the remaining parameters remains unassigned.
```
func StudentResult(_ sem1:Int..., sem2: Int..., sem3: Int..., sem4: Int... ){
var sum1 = 0
var sum2 = 0
var sum3 = 0
var sum4 = 0
for i in sem1{
sum1 += i
}
for j in sem2{
sum2 += j
}
for k in sem3{
sum3 += k
}
for l in sem4{
sum4 += l
}
print("Total marks of semester 1: ", sum1)
print("Total marks of semester 2: ", sum2)
print("Total marks of semester 3: ", sum3)
print("Total marks of semester 4: ", sum4)
}
StudentResult(30, 45, 66, 77, 88, 66,
sem2: 45, 67, 34, 67, 89,54,
sem3: 98, 87, 86, 76, 78, 71,
sem4: 90, 80, 76, 71, 61, 52)
```
### [Protocols](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/protocols)
> A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality.
Most modern programming languages, in the hopes of enhanced maintainability and reusability of code, offer some constructs that help the developer keep the definition of behavior and its implementation separate.
Swift takes the idea of interfaces a step further with protocols. With protocols and protocol extensions, Swift allows developers to enforce elaborate conformity rules without compromising the expressiveness of the language.
#### Protocol Design
Swift’s implementation of protocols is definitely one of the most interesting aspects of the language, and the sheer number of ways that they can be defined and used really shows just how powerful they are — especially once we start making full use of features like associated types and protocol extensions.
Because of that, it’s important to not treat every protocol the same way, but to rather design them according to which category that they belong to.
* **Action enablers**, which enable us to perform a given set of actions on each conforming type. They typically have names that end with “able”.
```
protocol Equatable {
static func ==(lhs: Self, rhs: Self) -> Bool
}
protocol Hashable: Equatable {
func hash(into hasher: inout Hasher)
}
```
* **Requirement definitions** enable us to formalize the requirements for being a certain kind of object, for example a Sequence, a Numeric, or a ColorProvider.
```
protocol Sequence {
associatedtype Iterator: IteratorProtocol
func makeIterator() -> Iterator
}
protocol ColorProvider {
var foregroundColor: UIColor { get }
var backgroundColor: UIColor { get }
}
```
* **Type conversion** protocols are used to let various types declare that they can be convertible into another type, or expressible through a raw value or literal — like CustomStringConvertible or ExpressibleByStringLiteral.
```
protocol CustomStringConvertible {
var description: String { get }
}
```
* **Abstract interfaces** act as unified APIs that multiple types can implement, which in turn lets us swap out implementations as we wish, encapsulate third party code, or mock certain objects within our tests.
```
protocol MTLDevice: NSObjectProtocol {
var name: String { get }
var registryID: UInt64 { get }
...
}
```
#### Common protocol conformance
* `Hashable` allows instances of a type to be compared for equality using the == operator.
* `Equatable` is used for types that can be hashed into an integer value.
* `Comparable` allows instances of a type to be compared and sorted using relational operators like <, >, <=, and >=.
* `Codable`combines the Encodable and Decodable protocols, providing a convenient way to serialize and deserialize Swift objects to and from external representations such as JSON or Property Lists.
* `CustomStringConvertible` allows types to provide a custom textual representation.
* `CaseIterable` is designed to make enums iterable like an array.
```
class PSLE: Codable, Hashable, Comparable, Equatable, CustomStringConvertible {
enum MotherTongue: String, CaseIterable, Codable {
case chinese, malay, tamil, hindi, punjabi
}
let mathScore: Int
let scienceScore: Int
let englishScore: Int
let motherTongue: MotherTongue
let motherTongueScore: Int
let score: Int
init(mathScore: Int, scienceScore: Int, englishScore: Int, chineseScore: Int, motherTongue: MotherTongue) {
self.mathScore = mathScore
self.scienceScore = scienceScore
self.englishScore = englishScore
self.motherTongue = motherTongue
self.motherTongueScore = chineseScore // Set motherTongueScore to chineseScore for demonstration
self.score = (mathScore + scienceScore + englishScore + chineseScore) * 3 / 4
}
// MARK: - Codable Conformance
enum CodingKeys: String, CodingKey {
case mathScore, scienceScore, englishScore, motherTongue, motherTongueScore, score
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
mathScore = try container.decode(Int.self, forKey: .mathScore)
scienceScore = try container.decode(Int.self, forKey: .scienceScore)
englishScore = try container.decode(Int.self, forKey: .englishScore)
motherTongue = try container.decode(MotherTongue.self, forKey: .motherTongue)
motherTongueScore = try container.decode(Int.self, forKey: .motherTongueScore)
score = try container.decode(Int.self, forKey: .score)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(mathScore, forKey: .mathScore)
try container.encode(scienceScore, forKey: .scienceScore)
try container.encode(englishScore, forKey: .englishScore)
try container.encode(motherTongue, forKey: .motherTongue)
try container.encode(motherTongueScore, forKey: .motherTongueScore)
try container.encode(score, forKey: .score)
}
// MARK: - Hashable Conformance
func hash(into hasher: inout Hasher) {
hasher.combine(mathScore)
hasher.combine(scienceScore)
hasher.combine(englishScore)
hasher.combine(motherTongue)
hasher.combine(motherTongueScore)
hasher.combine(score)
}
// MARK: - Comparable Conformance
static func < (lhs: PSLE, rhs: PSLE) -> Bool {
return lhs.score < rhs.score
}
// MARK: - Equatable Conformance
static func == (lhs: PSLE, rhs: PSLE) -> Bool {
return lhs.mathScore == rhs.mathScore &&
lhs.scienceScore == rhs.scienceScore &&
lhs.englishScore == rhs.englishScore &&
lhs.motherTongue == rhs.motherTongue &&
lhs.motherTongueScore == rhs.motherTongueScore &&
lhs.score == rhs.score
}
// MARK: - CustomStringConvertible Conformance
var description: String {
return "PSLE Scores: Math(\(mathScore)), Science(\(scienceScore)), English(\(englishScore)), MotherTongue(\(motherTongue.rawValue)), MotherTongueScore(\(motherTongueScore)), TotalScore(\(score))"
}
}
```
#### Protocol use-case
As stated before, can be very useful as it extends limits to how an object can be used. For example, because `PSLE` conforms to `Equatable`, if I wanted to find number of occurances of a specific `PSLE` in an array of `PSLE`, I can do it simply in the following manner.
```
extension Array where Element: Equatable {
func numberOfOccurences(of value: Element) -> Int {
reduce(into: 0) { count, element in
if element == value {
count += 1
}
}
}
}
```
Thus, when struggling to perform simple operations on complex custom objects, it may be helpful to think in terms of protocols as it may hugely simplify your solutions.
### Latest Swift updates: Swift 5.7, 5.8, 5.9 (5.10 coming soon)
[SE-0345](https://github.com/apple/swift-evolution/blob/main/proposals/0345-if-let-shorthand.md) introduces new shorthand syntax for unwrapping optionals into shadowed variables of the same name using if let and guard let.
```
var name: String? = "Linda"
// Previously: if let name = name {
if let name {
print("Hello, \(name)!")
}
```
[SE-0326](https://github.com/apple/swift-evolution/blob/main/proposals/0326-extending-multi-statement-closure-inference.md) dramatically improves Swift’s ability to use parameter and type inference for closures, meaning that many places where we had to specify explicit input and output types can now be removed.
```
// Previously: { score -> String in
let results = scores.map { score in
if score >= 85 {
return "\(score)%: Pass"
} else {
return "\(score)%: Fail"
}
}
```
[SE-0365](https://github.com/apple/swift-evolution/blob/main/proposals/0365-implicit-self-weak-capture.md) allows implicit self for weak self captures, after self is unwrapped
```
class TimerController {
var timer: Timer?
var fireCount = 0
init() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
guard let self else { return }
// Previously: self.fireCount += 1
fireCount += 1
}
}
}
```
[SE-0328](https://github.com/apple/swift-evolution/blob/main/proposals/0380-if-switch-expressions.md) adds the ability for us to use if and switch as expressions in several situations.
```
// Previously: Need to declare a variable first and allocate it a value manually in each case
let simpleResult = if score > 500 { "Pass" } else { "Fail" }
let complexResult = switch score {
case 0...300: "Fail"
case 301...500: "Pass"
case 501...800: "Merit"
default: "Distinction"
}
```
[SE-0366](https://github.com/apple/swift-evolution/blob/main/proposals/0366-move-function.md) introduces consume operator to end the lifetime of a variable binding
```
func createUser() {
let newUser = User(name: "Anonymous")
let userCopy = consume newUser
print(userCopy.name)
}
func consumeUser() {
let newUser = User(name: "Anonymous")
_ = consume newUser
}
func createAndProcessUser() {
let newUser = User(name: "Anonymous")
process(user: consume newUser)
}
```
## iOS <img src="https://hackmd.io/_uploads/S1tdY49bR.png" style="width:25px;"/>
### Concurrency
#### Understanding Concurrecny
**Definations**
* **Concurrency** - The execution of multiple instruction sequences at the same time.
* **Parallelism** - The us eof multiple ocessing elements simultaneously for solving any problem.
* **Thread** - The smallest unit of execution within a process. They enable concurrent execution of different parts of a program
* **Queue** - Data structures that follow the First In, First Out (FIFO) principle, where elements are added at the end (enqueue) and removed from the front (dequeue).

> “Concurrency is about dealing with many things at once, parallelism is about doing many things at once. Concurrency is a way to structure things so you can maybe use parallelism to do a better job.” - Rob Pike on concurrency vs parallelism
**How is it achieved**
iOS relies on time slicing and context switching to manage multiple tasks or threads efficiently.
* **Time Slicing:** Time slicing is a technique where the CPU switches between different threads or tasks rapidly, giving the illusion of parallel execution. Instead of one thread monopolizing the CPU indefinitely, the operating system allocates small time slices to each thread. This ensures fairness and responsiveness among different tasks.
* **Context Switching:** Context switching is the process of saving the current state of a running thread (its execution context, which includes things like the program counter, register values, and stack pointer) and restoring the state of another thread to allow it to run. This switch happens when the operating system's scheduler decides to allocate CPU time to a different thread.
* **Interrupts:** Interrupts play a crucial role in context switching. An interrupt is a signal sent to the CPU from either hardware (like a timer) or software (like a system call) that indicates an event requiring immediate attention. When an interrupt occurs, the CPU pauses its current activities, saves the state of the current thread, and executes an interrupt handler to deal with the event. This can involve initiating a context switch to handle a different task.
* **Scheduling and Latency:** Scheduling algorithms determine which thread or task gets CPU time next. iOS uses various scheduling algorithms (like priority-based scheduling) to ensure that critical tasks are executed promptly and that resources are allocated efficiently. Latency refers to the delay between an event (like an interrupt) and the response to that event, which should be minimized to ensure responsiveness.
* **Time Quantum:** The time quantum (also known as a time slice) is the maximum amount of time a thread is allowed to run before the operating system interrupts it and gives CPU time to another thread. The length of the time quantum can vary based on the scheduling policy and the current system load.
Fo example, we have three processes(P1, P2, P3) with their corresponding burst times(1ms, 4ms, 5ms). A rule of thumb is that 80% of CPU bursts should be smaller than the time quantum. Considering time slice of 2ms.
Here’s how CPU manages it by time slicing.

In iOS, this is all achieved with the following methods:
1. Manual multithreading
2. Grand Central Dispatch
3. Operation Queues
4. Concurrency API
#### Manual multi threading
```
class CustiomThread {
func createThread() {
let thread: Thread = Thread(target: Self.self, selector: #selector(threadSelector), object: nil)
thread.start()
}
@objc func threadSelector() {
print("Thread has began: \(Thread.current)")
}
}
let customThread = CustiomThread()
customThread.createThread()
```
When handling threads, manually, we have a lot of control and customisation power. We can start, stop delay them anytime we want and control the stack size.
However, the onus is on us to:
* Manage threads withtin system conditions
* Deallocate them when they have finished executing
* Maintain the order of execution
Failing to do so may cause memory leaks.
#### Grand Central Dispatch
Execute code concurrently on multicore hardware by submitting work to dispatch queues managed by the system.
[Apple Framework](https://developer.apple.com/documentation/DISPATCH)
Dispatch, also known as Grand Central Dispatch (GCD), contains language features, runtime libraries, and system enhancements that provide systemic, comprehensive improvements to the support for concurrent code execution on multicore hardware in macOS, iOS, watchOS, and tvOS.
GCD decides which thread to use to handle a task. It also manages a collection of dispatch queues, which is executed on a pool of threads. A dispatch queue executes tasks serially or concurrently but always in a FIFO order.
**Order of Execution** - affects the destination queue to which you are dispatching
1. **Concurrent queue**: Multiple Tasks at a time (Will still be dequeued serially)
2. **Serial Queues**: One Task at a time

**Manner of Execution** - affects the current thread from which you are dispatching
1. **Synchronous**: Block execution of other tasks until this one is complete
2. **Asynchronous**: Continue the execution of the current task while allowing the execution of other tasks to tart
To demonstrate this, we have these 3 tasks with 2 types of queues
```
let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)
let serialQueue = DispatchQueue(label: "com.example.serialQueue")
func taskA() {
for i in 1...3 {
print(i)
}
}
func taskB() {
for i in 4...6 {
print(i)
}
}
func taskC() {
for i in 7...9 {
print(i)
}
}
```
Concurrent queue with async execution prints out
147582963 (Can be many different orders)
```
concurrentQueue.async {
taskA()
}
concurrentQueue.async {
taskB()
}
concurrentQueue.async {
taskC()
}
```
Concurrent queue with sync operations prints out
123456789
```
concurrentQueue.sync {
taskA()
}
concurrentQueue.sync {
taskB()
}
concurrentQueue.sync {
taskC()
}
```
Serial queue with async operations prints out
123456789
```
serialQueue.async {
taskA()
}
serialQueue.async {
taskB()
}
serialQueue.async {
taskC()
}
```
Serial queue with sync operations prints out
123456789
```
serialQueue.sync {
taskA()
}
serialQueue.sync {
taskB()
}
serialQueue.sync {
taskC()
}
```
(To be continued...)
#### Operation queues
#### Concurrency API
**Async/Await**
A **synchronous function** is one that executes all its work in a simple, straight line on a single thread. Although the function itself can interact with other threads – it can request that work happens elsewhere, for example – the function itself always runs on a single thread.
An **asynchronous function** is a type of function that allows its execution to be suspended while waiting for certain operations to complete, without blocking the calling thread.
For clarification, concurrency is not achieved by simply making tasks run concurrently. That can be achieved by sending tasks to background threads. What happens when we need to wait for a certain result? We can send API calls into a background thread but we still need to wait for their results to continue. A function by default is synchronous and cannot suspend itself until a result is returned.
```
func concurrentMethod() {
DispatchQueue.main.async {
saveResultsTOServer()
}
// contniue work normally...
}
func updateSomething() {
DispatchQueue.main.async {
let results = fetchResults()
}
updateUI(with: results) // error: cannot access results
}
```
Thus, we have `async` functions. When waiting for prolonged work to be done, an async function can suspend itself without blocking anything on its thread. We are basically achieving the functionality of a linear and concurrent behaviour both at once. We are able to wait for a certain piece of task to be done before we can get its result and work with it. While waiting, we can do other stuff concurrently.
```
func fetchData() async -> [CustomdObject] {
// Some api call
}
func cleanData(_ data: [CustomdObject]) async -> [CustomdObject] {
// Long operations
for object in data {
...
}
}
```
Functions are usually marked `async` when they conduct operations that take a long time or unknowned amount of time to complete. This usually comes in the form of API calls or large loops. This is to allow calling functions to asynchronously run them. Hence, when calling these async functions, the calling functions need to suspend itself asynchronously. That is achieved from whatever we learned earlier - context switching, threading, queueing...
Wait, so that means both the calling function and the long functions must be `async`. So how can we first call these `async` opertions since by default, we start from a sync function?
1. When we daclare our `@main` method as `async`, our programn will launch into an `async` function and can call any other `async` function freely.
2. Frameworks like SwiftUI has in built places that can call `async` functions like `refreshable()` and `task()` modifiers.
3. The most common is the Task API where we can wrap `async` methods in a Task. More on this later.
There are 2 ways to call an `async` method. Either with `awsit` or `async let`.
We use `await` when we need to wait for a result before we can continue This is usally when we need to continue to do work with the result immediately.
```
let data = await fetchData()
let cleanedData = await cleanData(data)
```
We use `async let` when we want to run other tasks asynchronously. But eventually when we need to use the result, we still need the `await` keyword.
```
func getAppData() async -> ([News], [Weather], Bool) {
async let news = getNews()
async let weather = getWeather()
async let hasUpdate = getAppUpdateAvailable()
return await (news, weather, hasUpdate)
}
```
If we do not need the result or coordinate with its completion, we can simple do `async let +`
```
async let _ = updateServer()
```
But of course, many a times, even if we do not need a certain result or know completion, we need to check for its success. `async` functions can also `throw` and when calling them we need to `try`.
```
func fetchFavorites() async throws -> [Int] {
let url = URL(string: "https://hws.dev/user-favorites.json")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Int].self, from: data)
}
if let favorites = try? await fetchFavorites() {
print("Fetched \(favorites.count) favorites.")
} else {
print("Failed to fetch favorites.")
}
```
Prior to swift 5.5, completion handlers and PromiseKit were used for such purposes. However, it resulted in alot of nested code and to get around that, we would need much more changes.
```
// Heavy nesting with completion handlers especially with multiple independent calls
fetchData { data in
cleanData(data) { cleanedData in
fetchData2 { data2 in
cleanData(data2) { cleanedData2 in
sortData(cleanedData, cleanedData2) {
...
}
}
}
}
}
// Workarounds with Promises/completion closures
var data: Data? {
didSet {
if let data {
cleanedData = cleanData()
}
}
}
var cleanedData: Data? {
didSet {
...
}
}
func something() {
fetchDataWithPromiseKit()
.done { data in
// Cannot use data outside of this so need states
self.data = data
}
.catch { error in
print("Error fetching data: \(error)")
}
}
```
Instead of going deep down the completion chain or promise chain, we can turn surface level functions into `async` functions with continuations.
```
// Write an async counterpart to a function that has a completion handler
func fetchData() async -> [CustomObject] {
await withCheckedContinuation { continuation in
fetchData { data in
continuation.resume(returning: data)
}
}
}
let data = await fetchData()
let cleanedData = await cleanedData(data)
```
The code looks much neater now. In fact, we can also throw errors with this.
```
// Converting PromiseKit function to async function with continuations
func fetchDataAsyncWithContinuation() async throws -> Data {
return try await withCheckedThrowingContinuation { continuation in
// Call PromiseKit function and handle completion using continuation
fetchDataWithPromiseKit().done { data in
continuation.resume(returning: data)
}.catch { error in
continuation.resume(throwing: error)
}
}
}
```
**Important note**
Your continuation must be resumed exactly once. Not zero times, and not twice or more times – exactly once. A continuation might leak (not resumed) if a completion is not called.
```
func fetchData(@escaping completion: [CustomObject] -> Void) {
if let data = callData() {
completion(data)
}
}
func fetchData() async -> [CustomObject] {
await withCheckedContinuation { continuation in
fetchData { data in
continuation.resume(returning: data)
}
}
}
```
if callData() fails, the completion will not be called and continuation will not be resumed. Thus, we need to secure our code. However, if you have checked your code carefully and you’re sure it is correct, you can if you want replace the `withCheckedContinuation()` function with a call to `withUnsafeContinuation()`, which works exactly the same way but doesn’t add the runtime cost of checking you’ve used the continuation correctly.
**Sequence and Streams**
**Task and task groups**
**Actors**
### Memory Management
Memory management in iOS refers to the process of allocating and deallocating memory for your app's objects to ensure efficient use of system resources and prevent memory-related issues such as crashes or performance degradation due to memory leaks or excessive memory usage.
#### Handling
iOS uses ARC or Automatic Reference Counting to manage memories. It is a compile time feature for memory management in iOS. ARC increases its retain count by 1 when an object has a strong reference. If there is no strong reference to it means retain count is zero, ARC will deallocate the memory.
#### Memory leaks
Memory leak happens when an object remains in memory even after its lifecycle has finished.
A memory leak is a portion of memory that remains allocated, but never used. You should avoid a strong to strong relationship that creates a retain cycle which generates memory leak.
**Retain Cycles (Strong Reference Cycles)**
A retain cycle occurs when two or more objects hold strong references to each other, preventing their reference counts from reaching zero even when they are no longer needed. This leads to memory leaks because the objects are never deallocated. This mostly happens in classes and closures. Closures live in memory, so when you use “self” which is a reference, you need to make sure that you solve the retain cycle. To avoid retain cycles, use weak or unowned references for relationships that should not keep objects alive indefinitely.
**References**
In the case of referencing an object, we share the same version with whoever passed the object to us. It will increase its retain count by one. When the retain count of an object reaches zero, the object will be deallocated and released from memory.
On the other hand when we copy an object, we will not share the same version of the object, we will simply make a duplicate of that object. A copied object will have a retain count of one just like a newly initialised object.
* **Strong References:** The default reference type in Swift. A strong reference increases the reference count of the referenced object, keeping it alive as long as at least one strong reference exists.
* **Weak References:** A weak reference does not increase the reference count of the referenced object. If the referenced object is deallocated, a weak reference is automatically set to nil to prevent dangling pointers and retain cycles.
* **Unowned References:** Similar to weak references but assumes that the referenced object will not be deallocated as long as the unowned reference exists. Unlike weak references, unowned references are not optional and can lead to runtime crashes if accessed after the referenced object is deallocated.
The below example will showcase circular dependency - When two or more modules depend on each other directly or indirectly to work properly create circular dependency. (These kind of dependencies are also called mutually recursive.) We shall see how this can lead to memory leaks and how we can solve it.
```
class Person {
var name: String
var dog: Dog?
init(name: String) {
self.name = name
}
deinit {
print("\(name) is being deallocated")
}
}
class Dog {
var name: String
var owner: Person?
init(name: String) {
self.name = name
}
deinit {
print("\(name) is being deallocated")
}
}
// Creating a retain cycle
var alice: Person? = Person(name: "Alice")
var fido: Dog? = Dog(name: "Fido")
alice?.dog = fido
fido?.owner = alice
alice = nil
fido = nil
```
In the above:
* We create a Person instance (alice) and a Dog instance (fido).
* alice holds a strong reference to fido (alice?.dog = fido).
* fido holds a strong reference to alice (fido?.owner = alice).
* When we set alice and fido to nil, we expect them to be deallocated, but they are not because of the retain cycle.
To solve this, we use `weak` and `unowned `variables.
```
class Person {
weak var dog: Dog? // Use weak reference to break the retain cycle
}
class Dog {
unowned var owner: Person // Use unowned reference because Dog must have an owner
}
```
In this modified example:
* Person holds a weak reference to Dog (weak var dog: Dog?), which breaks the retain cycle.
* Dog holds an unowned reference to Person (unowned var owner: Person), assuming that every Dog instance must have an owner.
**Closure danger**
Memory leaks happen most commonly in closures.
In order for a closure to execute later, it needs to retain any variables that it needs for it to run. A closure captures references as strong by default like a class. If you assign a closure to a property of a class instance, and the body of that closure captures the instance then strong reference cycle can occur.
The strong reference can occur because the closure’s body accesses a property/method of the instance(for example self.testProperty, self.testMethod(), In both case, these kind of accesses cause the closure to capture self and create a strong reference cycle.)
A strong reference in a closure keeps a strong hold on the referenced object, preventing it from being deallocated as long as the closure exists. This is the default behavior for captured objects in closures.
```
func doWork() {
DispatchQueue.main.async {
print("Person \(self.name) is working") // Strong reference to self
}
}
```
Using a weak reference in a closure avoids creating a strong reference cycle. If the referenced object is deallocated, the weak reference automatically becomes nil.
```
func doWork() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
print("Person \(self.name) is working") // Weak reference to self
}
}
```
An unowned reference in a closure assumes that the referenced object will always exist as long as the closure is executed. If the referenced object is deallocated before the closure is executed, it will result in a runtime crash (accessing a nil reference).
```
func doWork() {
DispatchQueue.main.async { [unowned self] in
print("Person \(self.name) is working") // Unowned reference to self
}
}
```
In summary:
* Use a **strong reference** in a closure when you want to ensure that the referenced object remains alive as long as the closure exists.
* Use a **weak reference** in a closure to avoid creating retain cycles and allow the referenced object to be deallocated when it's no longer needed.
* Use an **unowned reference** in a closure when you can guarantee that the referenced object will exist as long as the closure is executed, and you want to avoid optional unwrapping. However, be cautious as accessing a deallocated object via an unowned reference results in a runtime crash.
#### Xcode Tools
**Memory Report**
While your app is running in Xcode, the memory report available from Xcode’s Debug navigator shows the app’s current memory use, along with the highest value seen. The yellow region of the memory gauge indicates memory use high enough to trigger a warning. The app risks termination by iOS if its memory use enters the red region.

**Memory Graph**
You can generate a memory graph of the objects and allocations in your app by clicking the Debug Memory Graph button in Xcode’s debug area at the bottom of the workspace window.

The memory graph shows the memory regions your app is using and the size of each region. A node in the graph represents an object, a heap allocation, or a memory-mapped file. Connections between nodes, drawn as arrows, show where one memory region refers to another.

The memory graph shows where your app is using memory and how those uses are related. You can augment the graph with allocation stack traces, so that each region is associated with a call stack trace recorded at the point at which the region was allocated.
To turn on allocation stack traces, check the Malloc Stack box in the Diagnostics area of your scheme’s Run settings. With allocation stack traces enabled, the inspector for a node in the memory graph shows the stack trace recorded when that node was allocated. Use this information to associate memory allocations in the memory graph with functions and methods in your app’s source code.
To export memory graphs from Xcode, choose *File > Export* Memory Graph. You can share exported memory graphs with team members or explore using command-line tools, including vmmap and leaks.
### Networking
### Access Control
### Deep Linking
### Embedding Webviews
### Localisation
### Naming Conventions
> Delivering a clear, consistent developer experience when writing Swift code is largely defined by the names and idioms that appear in APIs. These design guidelines explain how to make sure that your code feels like a part of the larger Swift ecosystem. - [Swift API Design Guidelines](https://https://www.swift.org/documentation/api-design-guidelines/#introduction)
The above link contains a concrete set of guidelines for naming conventions. Below is a summary of key points.
### 3rd Party libraries
#### Alamofire
#### Kingfisher
#### Carthage
#### R.swift
#### RxSwift
#### Sourcery
## UIKit <img src="https://hackmd.io/_uploads/SJ6wPEcW0.png" style="width:25px;"/>
### Storyboards and XIBs
### Combine
## SwiftUI <img src="https://hackmd.io/_uploads/HkqHd4qWA.png" style="width:25px;"/>
### Views: First class citizens
### Common Property wrappers
#### ObservableObject
A type of object with a publisher that emits before the object has changed.
```
class Contact: ObservableObject {
@Published var name: String
@Published var age: Int
}
```
* `@State` - lets us manipulate small amounts of value type data locally to a view. This owns its data.
* `@StateObject` - is used to store new instances of reference type data that conforms to the ObservableObject protocol. This owns its data.
* `@ObservedObject` - refers to an instance of an external class that conforms to the ObservableObject protocol. This does not own its data.
* `@EnvironmentObject` - reads a shared object that we placed into the environment. This does not own its data.
* `@Binding` - refers to value type data owned by a different view. Changing the binding locally changes the remote data too. This does not own its data
TBC...
#### Useful Reading
[All SwiftUI property wrappers explained and compared](https://www.hackingwithswift.com/quick-start/swiftui/all-swiftui-property-wrappers-explained-and-compared)
## Design Patterns
### Architecture
### Coordinator
### Repository