Lazy Properties

A lazy property is a property whose initial value is not calculated until the first time it is accessed.

class Example {
    lazy var greeting: String = {
        print("Hello CodewithSwift")
        return "Hello, Swift!"
    }()
}

let example = Example()
print("Before accessing the lazy property")
print(example.greeting) // Hello CodewithSwift
print(example.greeting) // This will reuse the initialized value
class ViewController {
    lazy var label: UILabel = {
        let label = UILabel()
        label.text = "Label"
        label.textColor = .black
        label.textAlignment = .center
        return label
    }()
}

let vc = ViewController()
print(vc.label.text ?? "") // Accesses and initializes the label

TypeAlias

A typealias is used to provide an alternative name for an existing type. It helps improve code readability, especially when working with complex or verbose types. It does not create a new type; it only creates a new name for an existing type.

typealias NewName = ExistingType
typealias Age = Int

func printAge(_ age: Age) {
    print("Your age is \(age).")
}

// Example usage
printAge(25) // Output: Your age is 25.
protocol Drivable {
    func drive()
}

protocol Fuelable {
    func refuel()
}

typealias Vehicle = Drivable & Fuelable

class Car: Vehicle {
    func drive() {
        print("Driving the car")
    }

    func refuel() {
        print("Refueling the car")
    }
}

// Example usage
func operate(vehicle: Vehicle) {
    vehicle.drive()
    vehicle.refuel()
}

let myCar = Car()
operate(vehicle: myCar)
// Output:
// Driving the car
// Refueling the car

Defer

defer is a statement used to execute a block of code just before the current scope e.g a function or loop exits.

func hello() {
    defer {
        print("Codewithswift")
    }
    print("Hello World")
}

hello()
// Output:
// Hello World
// Codewithswift
 func multipleDefers() {
    defer {
        print("First defer")
    }
    defer {
        print("Second defer")
    }
    defer {
        print("Third defer")
    }
    print("Inside function")
}

multipleDefers()
// Output:
// Inside function
// Third defer
// Second defer
// First defer
func performTaskWithLock() {
    let lock = NSLock()
    lock.lock()
    print("Lock acquired")
    
    defer {
        lock.unlock()
        print("Lock released")
    }
    
    print("Performing task")
}

performTaskWithLock()
// Output:
// Lock acquired
// Performing task
// Lock released

Higher Order Functions

Higher-order functions are widely used for functional programming. Swift provides many built-in higher-order functions such as map, filter, reduce, and compactMap

1. map

The map function transforms each element in a collection by applying a closure.

let numbers = [1, 2, 3, 4, 5]
let squaredNumbers = numbers.map { $0 * $0 }
print(squaredNumbers) // Output: [1, 4, 9, 16, 25]

//
let numbers = [1, 2, 3, 4, 5]
let squaredNumbers = numbers.map { $0 * 2 }
print(squaredNumbers) // Output: [2, 4, 6, 8, 10]

//
let names = ["swift", "codewithswift", "swiftui"]
let uppercaseNames = names.map { $0.uppercased() }
print(uppercaseNames) // Output: ["SWIFT", "CODEWITHSWIFT", "SWIFTUI"]

2. filter

The filter function filters elements of a collection based on a condition.

let numbers = [1, 2, 3, 4, 5]
let evenNumbers = numbers.filter { $0 % 2 == 0 }
print(evenNumbers) // Output: [2, 4]

3.reduce

The reduce function combines all elements of a collection into a single value.

let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(0) { $0 + $1 }
print(sum) // Output: 15

4.compactMap

The compactMap function transforms a collection while removing any nil values.

let strings = ["1", "2", "three", "4", "five"]
let numbers = strings.compactMap { Int($0) }
print(numbers) // Output: [1, 2, 4]

5.flatMap

flatMap is a higher-order function similar to map, but with an additional feature: it flattens or removes nesting from the resulting collection. It is useful when the transformation produces optional values or nested collections.

1.Removing nil Values

let strings = ["1", "2", "three", "4", "five"]
let numbers = strings.flatMap { Int($0) } // Converts valid strings to Int
print(numbers) // Output: [1, 2, 4]


2.Flattening Nested Arrays
let nestedArrays = [[1, 2, 3], [4, 5], [6, 7, 8]]
let flattenedArray = nestedArrays.flatMap { $0 }
print(flattenedArray) // Output: [1, 2, 3, 4, 5, 6, 7, 8]

Associated Type(Generics in Protocol)

Associated types are placeholders that a protocol defines for the type it expects. The actual type is specified when a protocol is adopted. This allows the protocol to work generically with any type.

protocol Container {
    associatedtype Item

    func add(item: Item)
    func getAllItems() -> [Item]
}


//Example with a Generic Class
class Box<T>: Container {
    typealias Item = T
    private var items: [T] = []

    func add(item: T) {
        items.append(item)
    }

    func getAllItems() -> [T] {
        return items
    }
}

let intBox = Box<Int>()
intBox.add(item: 42)
intBox.add(item: 7)
print(intBox.getAllItems()) // Output: [42, 7]

let stringBox = Box<String>()
stringBox.add(item: "Swift")
stringBox.add(item: "Generics")
print(stringBox.getAllItems()) // Output: ["Swift", "Generics"]



//Example with a Struct
struct Bag: Container {
    typealias Item = String
    private var items: [String] = []

    func add(item: String) {
        items.append(item)
    }

    func getAllItems() -> [String] {
        return items
    }
}

var bag = Bag()
bag.add(item: "Laptop")
bag.add(item: "Notebook")
print(bag.getAllItems()) // Output: ["Laptop", "Notebook"]

Advantages of Associatedtype

  1. associatedtype: Placeholder for a type to be specified later.
  2. Flexible and Reusable: Protocols with associated types are more flexible than concrete type protocols.
  3. Conforming Types Specify the Type: The type is defined when a protocol is implemented.
  4. Constraints: You can constrain associated types to conform to certain protocols.

Realtime Example Using in API call

// Define the protocol
protocol APIDataFetchable {
    associatedtype Model: Decodable
    
    func fetch(from url: URL, completion: @escaping (Result<Model, Error>) -> Void)
}



// Extend the protocol with a default implementation for fetching and decoding JSON

extension APIDataFetchable {
    func fetch(from url: URL, completion: @escaping (Result<Model, Error>) -> Void) {
        URLSession.shared.dataTask(with: url) { data, response, error in
            guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
                completion(.failure(error))
                return
            }
            
            guard let data = data else {
                completion(.failure(error))
                return
            }
            
            do {
                let decodedData = try JSONDecoder().decode(Model.self, from: data) // Model is generic type here 
                completion(.success(decodedData))
            } catch {
                 completion(.failure(error))
            }
        }.resume()
    }
}

// Example usage:
struct Post: Codable {
    let userId: Int
    let id: Int
    let title: String
    let body: String
}

struct User: Codable {
    let id: Int
    let name: String
    let username: String
}

// Conform specific types to APIDataFetchable
extension Post: APIDataFetchable {}
extension User: APIDataFetchable {}

// Example usage of fetching posts
let postURL = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!

Post().fetch(from: postURL) { (result: Result<Post, Error>) in
    switch result {
    case .success(let post):
        print("Post title: \(post.title)")
    case .failure(let error):
        print("Error: \(error)")
    }
}

// Example usage of fetching users
let userURL = URL(string: "https://jsonplaceholder.typicode.com/users/1")!
User().fetch(from: userURL) { (result: Result<User, Error>) in
    switch result {
    case .success(let user):
        print("User name: \(user.name)")
    case .failure(let error):
        print("Error: \(error)")
    }
}

Generics

Generics in Swift allow you to write flexible, reusable, and type-safe code by enabling you to define functions, classes, structures, and enumerations that work with any type.

Example of Generics

func swapValues<T>(_ a: inout T, _ b: inout T) {
    let temp = a
    a = b
    b = temp
}

var x = 10
var y = 20
swapValues(&x, &y)
print(x, y)  // Output: 20, 10

var str1 = "Hello"
var str2 = "World"
swapValues(&str1, &str2)
print(str1, str2)  // Output: World, Hello

Generic Function Example

func printValue<T>(_ value: T) {
    print("The value is: \(value)")
}

//Passing an Integer
printValue(42)
// Output: The value is: 42


//Passing a String
printValue("Hello, Swift!")
// Output: The value is: Hello, Swift!


//Passing a Double
printValue(3.14)
// Output: The value is: 3.14


//Passing a Boolean
printValue(true)
// Output: The value is: true

Explaination

  1. Generic Type Placeholder:
    • The T in <T> is a placeholder for any type. Swift replaces T with the actual type passed when the function is called.
  2. Type Safety:
    • Even though the function works with any type, Swift ensures type safety at compile time.
  3. Reusability:
    • Instead of writing separate functions for Int, String, Double, etc., we can write one generic function that works for all types.

Generic Typealias

typealias StringDictionary<T> = Dictionary<String, T>

var scores: StringDictionary<Int> = ["Bishal": 90, "Ram": 85]
print(scores["Bishal"]!)  // Output: 90

Generics and Closures

func performOperation<T>(_ value: T, operation: (T) -> Void) {
    operation(value)
}

performOperation(10) { value in
    print("Value is \(value)")
}
// Output: Value is 10

Generic Addition Function for Numeric Types

func add<T: AdditiveArithmetic>(_ a: T, _ b: T) -> T {
    return a + b
}

//Adding Integers
let intResult = add(10, 20)
print("Sum of Integers: \(intResult)")
// Output: Sum of Integers: 30


//Adding Doubles
let doubleResult = add(3.14, 2.71)
print("Sum of Doubles: \(doubleResult)")
// Output: Sum of Doubles: 5.85



//Adding Floats
let floatResult = add(1.5, 2.5)
print("Sum of Floats: \(floatResult)")
// Output: Sum of Floats: 4.0

Generic Function With Array Elements

func printArrayElements<T>(array: [T]) {
    for element in array {
        print(element)
    }
}

let intArray = [1, 2, 3, 4, 5]
let stringArray = ["Swift", "Generics", "Array"]
let doubleArray = [3.14, 2.71, 1.62]

printArrayElements(array: intArray)
// Output: 1 2 3 4 5

printArrayElements(array: stringArray)
// Output: Swift Generics Array

printArrayElements(array: doubleArray)
// Output: 3.14 2.71 1.62

Protocol Oriented Programming

Protocol-Oriented Programming (POP) is a programming paradigm in Swift that emphasizes the use of protocols to define shared behaviour and default implementations. It is an alternative to traditional Object-Oriented Programming (OOP) and promotes code reusability, flexibility, and type safety.

Introduced by Apple in Swift 2.0, POP leverages Swift’s features like protocol extensions, default implementations, and generics to make code more modular and reusable.

Concepts of Protocol-Oriented Programming

  1. Protocol as Blueprints:
    • Protocols define a set of requirements (methods, properties) that conforming types must implement.
  2. Protocol Extensions:
    • Extensions provide default implementations of protocol methods, allowing conforming types to inherit behavior automatically.
  3. Protocol Composition:
    • Combine multiple protocols into a single requirement.
  4. Value Types over Classes:
    • Prefer structs and enums (value types) over classes to avoid issues like shared mutable state and inheritance complexity.
  5. Generics with Protocols:
    • Use protocols with generics to write type-safe and reusable code.

1.Protocol with Default Implementation

protocol Greetable {
    func greet()
}

extension Greetable {
    func greet() {
        print("Hello, nice to meet you!")
    }
}

struct Person: Greetable {}
struct Robot: Greetable {}

let person = Person()
person.greet()  // Output: Hello, nice to meet you!

let robot = Robot()
robot.greet()  // Output: Hello, nice to meet you!

2.Protocol Composition

protocol Drivable {
    func drive()
}

protocol Flyable {
    func fly()
}

typealias FlyingCar = Drivable & Flyable

struct FutureCar: FlyingCar {
    func drive() {
        print("Driving on the road.")
    }
    
    func fly() {
        print("Flying in the air.")
    }
}

let car = FutureCar()
car.drive()  // Output: Driving on the road.
car.fly()    // Output: Flying in the air.

3.Protocol with Associated Types

protocol Container {
    associatedtype Item
    var items: [Item] { get set }
    mutating func addItem(_ item: Item)
}

extension Container {
    mutating func addItem(_ item: Item) {
        items.append(item)
    }
}

struct StringContainer: Container {
    var items: [String] = []
}

var container = StringContainer()
container.addItem("Swift")
container.addItem("Protocol-Oriented Programming")
print(container.items)  // Output: ["Swift", "Protocol-Oriented Programming"]

4.Value Types in POP

protocol Shape {
    func area() -> Double
}

struct Rectangle: Shape {
    var width: Double
    var height: Double
    
    func area() -> Double {
        return width * height
    }
}

struct Circle: Shape {
    var radius: Double
    
    func area() -> Double {
        return .pi * radius * radius
    }
}

let rect = Rectangle(width: 5, height: 10)
let circle = Circle(radius: 7)

print("Rectangle Area: \(rect.area())")  // Output: 50.0
print("Circle Area: \(circle.area())")  // Output: 153.93804002589985

Extensions

Extensions in Swift are a powerful feature that allows you to add new functionality to an existing class, struct, enum, or protocol without modifying the original source code. Extensions can add methods, computed properties, initializers, and even conform types to protocols.

Syntax

extension TypeName {
    // New functionality here
}

Adding Computed Properties

Extensions can add computed properties, but not stored properties.

extension Double {
    var square: Double {
        return self * self
    }

    var cube: Double {
        return self * self * self
    }
}

let number: Double = 3.0
print(number.square)  // Output: 9.0
print(number.cube)    // Output: 27.0

//

extension String {
    func reversedString() -> String {
        return String(self.reversed())
    }
    
    func isPalindrome() -> Bool {
        return self.lowercased() == self.reversedString().lowercased()
    }
}

let word = "Radar"
print(word.reversedString())  // Output: radaR
print(word.isPalindrome())    // Output: true

Adding Methods

We can add new instance or static methods to existing types.

extension String {
    func reverse() -> String {
        return String(self.reversed())
    }
}

let name = "Swift"
print(name.reverse())  // Output: tfiwS

Adding Initializers

Extensions can define new initializers for a type. However, you cannot add initializers to existing classes if they already have custom initializers defined.

struct Point {
    var x: Double
    var y: Double
}

extension Point {
    init(value: Double) {
        self.x = value
        self.y = value
    }
}

let point = Point(value: 5.0)
print(point)  // Output: Point(x: 5.0, y: 5.0)

Extending Protocol and Default Implementation

Extensions can add functionality to protocols, allowing conforming types to inherit default behaviour.

protocol Greeting {
    func sayHello()
}

extension Greeting {
    func sayHello() {
        print("Hello, world")
    }
}

struct Person: Greeting {}
let person = Person()
person.sayHello()  // Output: Hello, world

Protocols

Protocols in Swift define a blueprint of methods, properties, and other requirements that a conforming type must implement. They are a cornerstone of protocol-oriented programming, which Swift emphasizes over traditional inheritance.

Syntax

protocol Vehicle {
    var numberOfWheels: Int { get }
    func drive()
}

Conforming to a Protocol

A class, struct, or enum can conform to a protocol by implementing all its requirements.

struct Car: Vehicle {
    var numberOfWheels: Int = 4

    func drive() {
        print("Driving a car")
    }
}

let car = Car()
print(car.numberOfWheels)  // Output: 4
car.drive()                // Output: Driving a car

Protocol Inheritance

Protocols can specify methods that conforming types must implement.

protocol Person {
    var name: String { get }
}

protocol Employee: Person {
    var employeeID: String { get }
}

struct Developer: Employee {
    var name: String
    var employeeID: String
}

let dev = Developer(name: "Bishal", employeeID: "123")
print(dev.name)         // Output: Bishal
print(dev.employeeID)   // Output: 123

Protocol Composition

A protocol can inherit from one or more protocols.

protocol Flyable {
    func fly()
}

protocol Swimmable {
    func swim()
}

struct Duck: Flyable, Swimmable {
    func fly() {
        print("Duck is flying")
    }

    func swim() {
        print("Duck is swimming")
    }
}

let duck: Flyable & Swimmable = Duck()
duck.fly()
duck.swim()

Protocols with Default Implementation

Using extensions, protocols can provide default implementations for methods and computed properties.

protocol Greeting {
    func sayHello()
}

extension Greeting {
    func sayHello() {
        print("Hello!")
    }
}

struct Person: Greeting {}
let person = Person()
person.sayHello()  // Output: Hello!

Protocols with Associated Types

Protocols can use associated types as placeholders for a type that is defined when the protocol is adopted. (Will discuss in next Generics topic)

protocol Container {
    associatedtype Item
    func add(item: Item)
    func getItem() -> Item
}

struct Box: Container {
    var item: String

    func add(item: String) {
        print("Adding \(item) to the box")
    }

    func getItem() -> String {
        return item
    }
}

let box = Box(item: "Book")
box.add(item: "Notebook")
print(box.getItem())  // Output: Book

Object Oriented Programming(OOP)

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of objects which can contain data (properties) and code (methods). Swift, as a modern programming language, fully supports OOP principles, including encapsulation, inheritance, polymorphism and abstraction.

Core Principles of OOP

  1. Encapsulation
  2. Inheritance
  3. Polymorphism
  4. Abstraction

Encapsulation

  • Binding data (properties) and behaviors (methods) within a class or struct.
  • Restricting access to internal implementation using access control (private, fileprivate, etc.).
class Person {
    private var name: String

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

    func getName() -> String {
        return name
    }
}

let person = Person(name: "User")
print(person.getName())  // Output: User

Inheritance

  • Creating a new class that inherits properties and methods from an existing class.
  • The superclass provides the base functionality, while the subclass can override or extend it.
class User {
    func name() {
        print("My Name is Bishal")
    }
}

class Department: User {
    override func name() {
        print("My Name is Elon")
    }
}

let dept = Department()
dept.name()  // Output: My Name is Elon

1.Single Inheritance

In Swift, a subclass can inherit from only one superclass. This is the default type of inheritance in Swift.

class Animal {
    func eat() {
        print("Animal is eating")
    }
}

class Dog: Animal {
    func bark() {
        print("Dog is barking")
    }
}

let dog = Dog()
dog.eat()  // Inherited from Animal
dog.bark() // Specific to Dog

//OutPut:
Animal is eating
Dog is barking

2.Multilevel Inheritance

Multilevel inheritance refers to a chain of inheritance where a class inherits from a subclass of another class.

class Animal {
    func breathe() {
        print("Breathing...")
    }
}

class Mammal: Animal {
    func walk() {
        print("Walking...")
    }
}

class Human: Mammal {
    func speak() {
        print("Speaking...")
    }
}

let human = Human()
human.breathe() // From Animal
human.walk()    // From Mammal
human.speak()   // Specific to Human

3.Hierarchical Inheritance

In hierarchical inheritance, multiple subclasses inherit from a single superclass.

class Shape {
    func area() {
        print("Calculating area")
    }
}

class Circle: Shape {
    func radius() {
        print("Radius of the circle")
    }
}

class Rectangle: Shape {
    func dimensions() {
        print("Dimensions of the rectangle")
    }
}

let circle = Circle()
circle.area()    // Inherited from Shape
circle.radius()  // Specific to Circle

let rectangle = Rectangle()
rectangle.area()      // Inherited from Shape
rectangle.dimensions() // Specific to Rectangle

4.Multiple Inheritance

Swift does not allow direct multiple inheritance (a class inheriting from more than one class). However, it supports multiple inheritance using protocols. A class or struct can conform to multiple protocols to achieve the desired behaviour.

protocol Flyable {
    func fly()
}

protocol Swimmable {
    func swim()
}

class Duck: Flyable, Swimmable {
    func fly() {
        print("Duck is flying")
    }

    func swim() {
        print("Duck is swimming")
    }
}

let duck = Duck()
duck.fly()
duck.swim()

5.Hybrid Inheritance

Swift allows combining inheritance, protocols, and composition to achieve hybrid inheritance patterns.

protocol Sound {
    func makeSound()
}

class Animal {
    func eat() {
        print("Animal is eating")
    }
}

class Dog: Animal, Sound {
    func makeSound() {
        print("Dog is barking")
    }
}

class Bird: Animal, Sound {
    func makeSound() {
        print("Bird is chirping")
    }
}

let dog = Dog()
dog.eat()
dog.makeSound()

let bird = Bird()
bird.eat()
bird.makeSound()

Combination with Protocol Oriented Programming

In Swift, protocol-oriented programming complements inheritance by encouraging the use of protocols for shared behaviour.

protocol Drivable {
    func drive()
}

protocol Flyable {
    func fly()
}

class Car: Drivable {
    func drive() {
        print("Car is driving")
    }
}

class Airplane: Flyable, Drivable {
    func fly() {
        print("Airplane is flying")
    }

    func drive() {
        print("Airplane is taxiing on the runway")
    }
}

let car = Car()
car.drive()

let airplane = Airplane()
airplane.drive()
airplane.fly()

Polymorphism

  • Allowing objects to be treated as instances of their parent class.
  • Achieved through method overriding and protocol conformance.
class Animal {
    func getName() {
        print("Animal name")
    }
}

class Cat: Animal {
    override func getName() {
        print("Cat")
    }
}

class Dog: Animal {
    override func getName() {
        print("Dog")
    }
}

let animals: [Animal] = [Cat(), Dog()]
for animal in animals {
    animal.makeSound()
}
// Output:
// Cat
// Dog

Types of Polymorphism

  1. Compile-Time Polymorphism (Method Overloading)
    • Swift allows multiple methods with the same name but different parameters in the same scope. This is called method overloading.
class Math {
    func add(a: Int, b: Int) -> Int {
        return a + b
    }

    func add(a: Double, b: Double) -> Double {
        return a + b
    }

    func add(a: Int, b: Int, c: Int) -> Int {
        return a + b + c
    }
}

let math = Math()
print(math.add(a: 2, b: 3))        // Output: 5
print(math.add(a: 2.5, b: 3.5))    // Output: 6.0
print(math.add(a: 1, b: 2, c: 3))  // Output: 6

2.Run-Time Polymorphism (Method Overriding)

  • In run-time polymorphism, a subclass can override a method in its superclass to provide a specific implementation.
class Animal {
    func makeSound() {
        print("Animal makes a sound")
    }
}

class Dog: Animal {
    override func makeSound() {
        print("Dog barks")
    }
}

class Cat: Animal {
    override func makeSound() {
        print("Cat meows")
    }
}

let animals: [Animal] = [Dog(), Cat(), Animal()]
for animal in animals {
    animal.makeSound()
}
// Output:
// Dog barks
// Cat meows
// Animal makes a sound

Abstraction

  • Hiding implementation details while exposing essential features through protocols or abstract classes.
  • Achieved in Swift using protocols.
protocol Shape {
    func area() -> Double
}

class Circle: Shape {
    var radius: Double
    init(radius: Double) {
        self.radius = radius
    }

    func area() -> Double {
        return 3.14 * radius * radius
    }
}

class Rectangle: Shape {
    var width: Double
    var height: Double

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

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

let shapes: [Shape] = [Circle(radius: 5), Rectangle(width: 4, height: 6)]
for shape in shapes {
    print(shape.area())
}
// Output:
// 78.5
// 24.0