Access Control

1.Open Access

Used primarily in frameworks to allow subclassing and overriding outside the module

open class OpenClass {
    open func greet() {
        print("Hello from Swift!")
    }
}

2.Public Access

Allows usage outside the module but does not allow subclassing or overriding outside the module.

public class PublicClass {
    public func greet() {
        print("Hello from PublicClass!")
    }
}

3.Internal Access

Accessible anywhere within the same module.

internal class InternalClass {
    internal func greet() {
        print("Hello from InternalClass!")
    }
}

4.Fileprivate Access

Limits access to the enclosing declaration or extensions within the same file

class PrivateClass {
    private func greet() {
        print("Hello from PrivateClass!")
    }
}

Open vs Public

Open allows external subclassing and overriding:

Public restricts subclassing and overriding outside the module

open class Animal {
    open func sound() {
        print("Sound")
    }
}

public class Dog: Animal {
    override open func sound() {
        print("Bark!")
    }
}
public class Animal {
    public func sound() {
        print("Sound")
    }
}

public class Dog: Animal {
    override open func sound() {
        print("Bark!")  // Error public can't be overridden outside module
    }
}

Automatic Reference Counting

Swift uses Automatic Reference Counting (ARC) to manage the memory of instances of classes. ARC automatically keeps track of references to class instances and deallocates them when they are no longer needed, freeing up memory.

class Person {
    var name: String

    init(name: String) {
        self.name = name
        print("\(name) is initialized.")
    }

    deinit {
        print("\(name) is deinitialized.")
    }
}

var person1: Person? = Person(name: "Bishal")  // Reference count: 1
var person2 = person1                          // Reference count: 2

person1 = nil                                  // Reference count: 1
person2 = nil                                  // Reference count: 0
// Output: Bishal is deinitialized.

Retail Cycle and Memory Leak

A retain cycle occurs when two or more class instances hold strong references to each other, preventing ARC from deallocating them. This leads to a memory leak.

class Person {
    var name: String
    var apartment: Apartment?

    init(name: String) {
        self.name = name
    }

    deinit {
        print("\(name) is deinitialized.")
    }
}

class Apartment {
    var unit: String
    var tenant: Person?

    init(unit: String) {
        self.unit = unit
    }

    deinit {
        print("Apartment \(unit) is deinitialized.")
    }
}

var john: Person? = Person(name: "John")
var unit4A: Apartment? = Apartment(unit: "4A")

john?.apartment = unit4A
unit4A?.tenant = john

// Breaking references
john = nil  // Retain cycle: Apartment is not deallocated.
unit4A = nil  // Retain cycle: Person is not deallocated.

Solving Retail Cycle

  1. Weak References
    • A weak reference does not increase the reference count of an instance.
    • Declared using the weak keyword.
    • Must always be an optional (nil if the instance is deallocated).
class Apartment {
    var unit: String
    weak var tenant: Person?  // Weak reference

    init(unit: String) {
        self.unit = unit
    }

    deinit {
        print("Apartment \(unit) is deinitialized.")
    }
}

2.Unowned References

An unowned reference does not increase the reference count, similar to a weak reference.Unlike weak references, it assumes the instance will always exist during its lifetime.Declared using the unowned keyword.

class Person {
    var name: String
    unowned var apartment: Apartment  // Unowned reference

    init(name: String, apartment: Apartment) {
        self.name = name
        self.apartment = apartment
    }
}

ARC and Closures

Closures can also cause retain cycles if they capture references to self or other objects.

class ViewController {
    var title: String = "MyViewController"

    lazy var printTitle: () -> Void = {
        print(self.title)
    }

    deinit {
        print("ViewController is deinitialized.")
    }
}

var vc: ViewController? = ViewController()
vc?.printTitle()
vc = nil  // Retain cycle: ViewController is not deallocated.

Using Capture Lists to Solve the Retain Cycle

Capture lists specify how references are captured within the closure.

class ViewController {
    var title: String = "MyViewController"

    lazy var printTitle: () -> Void = { [weak self] in
        guard let self = self else { return }
        print(self.title)
    }

    deinit {
        print("ViewController is deinitialized.")
    }
}

var vc: ViewController? = ViewController()
vc?.printTitle()
vc = nil  // Output: ViewController is deinitialized.

Weak vs Unowned in Swift

Weak Reference

  • Always optional, so you must handle cases where the reference is nil.
  • When the reference might become nil.
  • For optional references like delegates or relationships where one object might outlive the other.

Unowned Reference

  • Unsafe if the object is deallocated. Accessing it after the object is gone will cause a runtime crash.
  • When the reference will never become nil.
  • For non-optional references where the lifetime of the referred object is guaranteed to be longer than the reference.

Initializer

An initializer is a special method that prepares an instance of a class, structure, or enumeration for use. Initializers set up the initial state of an object by assigning values to its properties and performing any required setup.

Types of Initializers

1. Designated Initializer
  • The primary initializer for a class, struct, or enum.
  • Ensures that all properties are initialized.
  • A class must have at least one designated initializer.
2. Convenience Initializer
  • A secondary initializer to provide additional customization or shortcuts.
  • Calls a designated initializer from the same class.
3. Failable Initializer
  • Returns nil if initialization fails.
  • Used when initialization can fail due to invalid input or conditions.
4. Required Initializer
  • Ensures that every subclass implements the initializer.
5. Default Initializer
  • Automatically provided for structs and classes that have all properties with default values.

1.Designated Initializer

struct Person {
    var name: String
    var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

2.Convenience Initializer

Convenience initializers are used to simplify initialization by calling designated initializers

class Rectangle {
    var width: Double
    var height: Double

    init(width: Double, height: Double) {
        self.width = width
        self.height = height
    }

    convenience init(side: Double) {
        self.init(width: side, height: side)
    }



3.Failable Initializer

Use failable initializers when initialization might fail.

struct Temperature {
    var celsius: Double

    init?(celsius: Double) {
        if celsius < -273.15 {
            return nil // Initialization fails if below absolute zero
        }
        self.celsius = celsius
    }
}

if let temp = Temperature(celsius: -300) {
    print("Temperature: \(temp.celsius)°C")
} else {
    print("Invalid temperature.") 
}

 // Output: Invalid temperature.

4.Required Initializer

class Animal {
    var name: String

    required init(name: String) {
        self.name = name
    }
}

class Dog: Animal {
    required init(name: String) {
        super.init(name: name)
    }
}

let dog = Dog(name: "Buddy")
print(dog.name)  

// Output: Buddy

Class

Classes are reference types used to define reusable and flexible blueprints for objects. Classes can have properties, methods, initialisers, deinitialisers and can conform to protocols. They also support inheritance, which makes them powerful for building complex systems.

Characteristic of Class

  1. Reference Type: Instances are passed by reference, meaning multiple variables can point to the same instance.
  2. Inheritance: Classes can inherit properties and methods from other classes.
  3. Deinitialisers: Classes can define deinitialisers (deinit) to release resources.
  4. Mutable Properties: Properties of a class instance can be modified, even if the instance is declared with let.
  5. Protocols and Extensions: Classes can adopt protocols and be extended.
class Person {
    var name: String

    init(name: String) {
        self.name = name
    }

    func description() -> String {
        return "\(name)"
    }
}

// Create an instance
let person = Person(name: "Bishal")
print(person.description())  // Output: Bishal

Initialiser(Constructor)

Classes can have multiple initialisers, and these can be overridden in subclasses

class Rectangle {
    var width: Double
    var height: Double

    init(width: Double, height: Double) {
        self.width = width
        self.height = height
    }

    convenience init(side: Double) {
        self.init(width: side, height: side)
    }
}

let square = Rectangle(side: 5.0)
print("Square: \(square.width) x \(square.height)")  // Output: Square: 5.0 x 5.0

Deinitialisers

Deinitialisers are used to perform cleanup when a class instance is deallocated

class FileManager {
    var fileName: String

    init(fileName: String) {
        self.fileName = fileName
        print("\(fileName) opened.")
    }

    deinit {
        print("\(fileName) closed.")
    }
}

var file: FileManager? = FileManager(fileName: "data.txt")
file = nil  // Output: data.txt closed.

Class as Reference Type

// Define a class
class PersonClass {
    var name: String
    var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

var classPerson1 = PersonClass(name: "Ram", age: 20)
var classPerson2 = classPerson1 // References the same instance

// Modify classPerson2
classPerson2.name = "Shyam"

print("Class Person 1: \(classPerson1.name), Age: \(classPerson1.age)") // Shyam, 20
print("Class Person 2: \(classPerson2.name), Age: \(classPerson2.age)") // Shyam, 20

When we assign classPerson1 to classPerson2, both variables reference the same instance. Changes made to classPerson2 are reflected in classPerson1

Struct

Struct are similar to classes but have distinct features and are often preferred in value-oriented programming.

Features

  1. Value Types: Structures are value types, meaning instances are copied when passed around.
  2. Custom Initializers: Swift provides a default initializer, but you can define your own.
  3. Properties and Methods: Structures can have stored and computed properties, as well as methods.
  4. Immutability with let: If a structure instance is declared with let, all its properties become immutable.
  5. Protocols: Structures can adopt and conform to protocols.
  6. No Inheritance: Unlike classes, structures do not support inheritance.
struct Rectangle {
    var length: Double
    var width: Double

    func area() -> Double {
        return length * width
    }
}


var rect = Rectangle(length: 10.0, width: 5.0)
print("Area of rectangle: \(rect.area())")  
// Output: 50.0

Mutating Methods

To modify a structure’s properties within a method, you need to mark the method as mutating

struct Counter {
    var count: Int = 0

    mutating func increment() {
        count += 1
    }

    mutating func reset() {
        count = 0
    }
}

var counter = Counter()
counter.increment()
print("Count: \(counter.count)")  // Output: Count: 1
counter.reset()
print("Count: \(counter.count)")  // Output: Count: 0

Use Cases of Structs

  1. Data Models: Structures are ideal for representing simple data models like coordinates, dimensions, and configurations.
  2. Immutable Entities: Use structures when you need value semantics and immutability.
  3. Lightweight Objects: They are more efficient for small, lightweight objects compared to classes.

// Define a struct
struct PersonStruct {
    var name: String
    var age: Int
}

var structPerson1 = PersonStruct(name: "Bishal", age: 25)
var structPerson2 = structPerson1 // Copies the values

// Modify structPerson2
structPerson2.name = "Ram"

print("Struct Person 1: \(structPerson1.name), Age: \(structPerson1.age)") //Output: Bishal, 25
print("Struct Person 2: \(structPerson2.name), Age: \(structPerson2.age)") //Output: Ram, 25

Closures

Closures are self-contained blocks of functionality that can be passed around and used in your code. They can capture and store references to variables and constants from their surrounding context, making them extremely powerful for functional programming.

{ (parameters) -> ReturnType in
    // Code
}

//Example
let greet = { (name: String) -> String in
    return "Hello, \(name)!"
}

print(greet("Swift"))

//Output
Hello, Swift!

1.Closure as a Function Argument

func performOperation(_ operation: (Int, Int) -> Int) {
    let result = operation(4, 2)
    print("Result: \(result)")
}

// Passing a closure
performOperation { (a, b) -> Int in
    return a + b
}

// Output
Result: 6



2.Trailing Closure

If a closure is the last argument to a function, you can use trailing closure syntax.

performOperation { $0 * $1 }

//Output
Result: 8

3.Non-Escaping Closures

A non-escaping closure is executed within the scope of the function it is passed to. By default, function parameters that accept closures are non-escaping.

  1. The closure cannot outlive the function.
  2. It does not require explicit management of memory for captured variables.
  3. No need to use self explicitly when referring to instance properties or methods.

func performTask(closure: () -> Void) {
    print("Before closure execution")
    closure()
    print("After closure execution")
}

performTask {
    print("Closure is running")
}

//Output
Before closure execution
Closure is running
After closure execution

4.Escaping Closure

An escaping closure is executed after the function returns. It escapes the function’s scope, meaning it is stored for later execution, often in asynchronous tasks or completion handlers. To mark a closure as escaping, use the @escaping keyword.

  1. Outlives the function it is passed to.
  2. Captures variables in its context, potentially creating strong reference cycles.
  3. Requires explicit use of self when referring to instance properties or methods.

var completionHandlers: [() -> Void] = []

func addCompletionHandler(_ handler: @escaping () -> Void) {
    completionHandlers.append(handler)
}

addCompletionHandler {
    print("Escaping closure executed later!")
}


completionHandlers.forEach { $0() }


// Output
Escaping closure executed later!


Escaping closures are commonly used in:

  1. Asynchronous Tasks
    E.g., Network requests or delayed operations.
  2. Completion Handlers
    For actions that should occur after a process completes.
  3. Storage for Later Execution
    E.g., Keeping a closure in an array or variable for future use.

Capture List

A capture list in Swift is used within a closure to define how the closure should manage captured variables. It is primarily used to prevent strong reference cycles or to control the ownership of variables captured by the closure.

Capturing Variables by Reference

By default, closures capture variables by reference. This means the closure retains access to the latest value of the variable, even if it changes after the closure is created.

var fruit = "Apple"

let closure = {
    return fruit
}

print("\(closure())")  // Output: Apple

fruit = "Banana"

print("\(closure())")  // Output: Banana

1.Initially, the fruit variable has the value "Apple".

2.The closure captures the fruit variable by reference.

3.When we change the value of fruit to "Banana", the closure reflects this change because it holds a reference to the variable, not a copy.

Capture List for Immutable Copies

If you don’t want a closure to reflect changes in the captured variable, you can use a capture list. The capture list allows you to capture variables as immutable copies, effectively freezing their values at the time the closure is created.

var fruit = "Apple"

let closure = { [fruit] in
    return fruit
}

print("\(closure())")  // Output: Apple

fruit = "Banana"

print("\(closure())")  // Output: Apple

1.[fruit] in the capture list creates an immutable copy of fruit at the time the closure is created.

2.Even though we change the value of fruit to "Banana" later, the closure retains the original value ("Apple").

Optionals in Swift

Optionals in Swift are a powerful feature that allows a variable to hold either a value or nil (no value). This helps handle the absence of a value safely and avoids runtime crashes caused by null pointers.

var name: String? // Can be nil or hold a string value

Unwrapping Optionals

1.Force Unwrapping

Access the value of an optional using ! if you are sure it contains a value. Force unwrapping a nil optional causes a runtime crash.

var name: String? = "Swift"
print(name!) 
// Output: Swift

2.Optional Binding

Use if let or guard let to safely unwrap optionals.

// Using if let
if let unwrappedName = name {
    print("Name: \(unwrappedName)")
} else {
    print("Name is nil")
}

// Using guard let 
func greet(person: String?) {
    guard let name = person else {
        print("Name is nil")
        return
    }
    print("Hello, \(name)!")
}

greet(person: "Swift")

3. Nil-Coalescing Operator (??)

Provide a default value for an optional when it is nil

let person = name ?? "Swift"
print("Hello, \(person)") // Output: Hello, Swift

4.Optional Chaining

struct Person {
    var address: String?
}

var person: Person? = Person(address: "123 Swift")
print(person?.address ?? "None") // Output: 123 Swift

5.Implicitly Unwrapped Optionals

Sometimes, we can declare an optional that will always have a value after being set. We can use ! instead of ?. Implicitly unwrapped optionals can lead to runtime crashes

var age: Int! = 25
print(age + 5) // Output: 30

Enums

Enums define a group of related values in a type-safe way. They are particularly useful for defining states, options, or categories that a variable can have.

enum CompassPoint {
    case north
    case south
    case east
    case west
}

var direction = CompassPoint.north

switch direction {
case .north:
    print("Heading North")
case .south:
    print("Heading South")
case .east:
    print("Heading East")
case .west:
    print("Heading West")
}

//Output
print("Heading North")

Raw Values

Enums can have raw values associated with each case. The raw value must be of the same type and unique.

enum Planet: Int {
    case mercury
    case venus
    case earth
    case mars
}
print(Planet.mercury.rawValue) 

// Output
 0

enum Seasons: String {
    case spring = "Blossoms"
    case summer = "Heat"
    case winter = "Snow"
}

print(Seasons.winter.rawValue) 
// Outputs: Snow

Associated Values

Enums can store additional associated values for each case.

enum Barcode {
    case upc(Int, Int, Int, Int)
    case qrCode(String)
}

var productCode = Barcode.upc(8, 85909, 51226, 3)

switch productCode {
case .upc(let numberSystem, let manufacturer, let product, let checkDigit):
    print("UPC: \(numberSystem)-\(manufacturer)-\(product)-\(checkDigit)")
case .qrCode(let code):
    print("QR Code: \(code)")
}

//Output
UPC: 8-85909-51226-3

Enum with Function

enum TrafficLight {
    case red, yellow, green

    func action() -> String {
        switch self {
        case .red:
            return "Stop"
        case .yellow:
            return "Get Ready"
        case .green:
            return "Go"
        }
    }
}

let light = TrafficLight.red
print(light.action()) 

// Output
 Stop

Enum with CaseIterable

enum Beverage: CaseIterable {
    case coffee, tea, juice
}

for beverage in Beverage.allCases {
    print(beverage)
}

//Output
coffee
tea
juice

Sets

A Set in Swift is an unordered collection of unique elements. Unlike an Array, it does not allow duplicate values and does not maintain the order of elements.

var fruits: Set<String> = ["Apple", "Banana", "Cherry"]
print(fruits)

//Output

["Cherry", "Apple", "Banana"]  // Order may vary

Key Set Operations

//Adding an Element

fruits.insert("Mango")
print(fruits)

//Output
["Cherry", "Apple", "Mango", "Banana"]


//Removing an Element

fruits.remove("Banana")
print(fruits)

//Output
fruits.remove("Banana")
print(fruits)

Set Operations

1. Union

Combines all unique elements from two sets.


let setA: Set = [1, 2, 3]
let setB: Set = [3, 4, 5]
let unionSet = setA.union(setB)
print(unionSet)

//Output
[1, 2, 3, 4, 5]

2.Intersection

Finds common elements between two sets.

let intersectionSet = setA.intersection(setB)
print(intersectionSet)

//Output
[3]

Difference Between Sets and Arrays

FeatureSetArray
OrderUnorderedOrdered
DuplicatesNot allowedAllowed
PerformanceFaster for lookups and operationsSlower for lookups (O(n))
Use CaseEnsuring unique values, mathematical operationsSequential data, maintaining order

Dictionary

A dictionary in Swift is a collection type that stores key-value pairs. Each key is unique and you use it to access its corresponding value.

dictionary[key] = value
var scores: [String: Int] = [
    "Bishal": 1,
    "Swift": 2,
    "iOS": 3
]

// Access a value
if let score = scores["Bishal"] {
    print("Bishal's score: \(score)")
}

//OutPut
Bishal's score: 1

// Add a new key-value pair
scores["Ram"] = 10

// Update a value
scores["Swift"] = 92

// Remove a key-value pair
scores["iOS"] = nil

print(scores)

//OutPut 
["Xcode": 1, "Swift": 92, "Ram": 10]

Iterating Over a Dictionary

var countries: [String: Int] = [ "USA": 1, "India": 2]

// Iterate over key-value pairs
for (key, value) in countries {
    print("\(key): \(value)")
}

// Output
[USA: 1, India: 2]


//Iterate Over Keys Only

for key in countries.keys {
    print("Key: \(key)")
}

// Output

Key: USA
Key: India

//Iterate Over Values Only

for value in countries.values {
    print("Value: \(value)")
}


Output:

Value: 1
Value: 2