Mastering Swift 5 Interviews: The Complete Ultimate Guide

 

Mastering Swift 5 Interviews: The Complete Ultimate Guide

Table of Contents

  1. Introduction to Swift 5 Interview Preparation
  2. Fundamental Swift Concepts
  3. Object-Oriented Programming in Swift
  4. Protocol-Oriented Programming
  5. Memory Management and ARC
  6. Concurrency and Asynchronous Programming
  7. Error Handling and Optionals
  8. Collections and Generics
  9. Advanced Swift Features
  10. iOS Development Integration
  11. Design Patterns in Swift
  12. Testing and Debugging
  13. Performance Optimization
  14. Real-World Coding Challenges
  15. System Design Questions
  16. Behavioral Interview Preparation


1. Introduction to Swift 5 Interview Preparation

Understanding the Swift Interview Landscape

Swift has evolved significantly since its introduction in 2014, and Swift 5 represents a mature, stable version of the language with ABI stability and numerous enhancements. As you prepare for Swift interviews, understanding that interviewers are looking for more than just syntax knowledge is crucial. They want to assess your problem-solving abilities, architectural thinking, understanding of iOS ecosystem, and ability to write clean, maintainable code.

Modern Swift interviews typically consist of multiple rounds covering technical fundamentals, coding challenges, system design, and behavioral questions. Technical rounds often include live coding sessions where you'll solve algorithmic problems, discuss language features, explain architectural decisions, and demonstrate your understanding of iOS frameworks and best practices.

What Interviewers Really Look For

Interviewers assess candidates across several dimensions. Technical competency is fundamental but not sufficient. They evaluate your communication skills by observing how you explain complex concepts, your problem-solving approach through how you break down challenges, your code quality in terms of readability and maintainability, your understanding of trade-offs when making design decisions, and your passion for iOS development through your engagement and curiosity.

The best candidates demonstrate a growth mindset, showing willingness to learn from mistakes and accept feedback. They think aloud during problem-solving, making their thought process transparent. They ask clarifying questions before diving into solutions, demonstrating thoroughness and attention to requirements.

Structuring Your Preparation

Effective interview preparation requires a structured approach spanning several weeks or months. Begin with fundamentals, ensuring solid understanding of Swift syntax, data structures, and algorithms. Progress to intermediate topics including protocols, generics, memory management, and concurrency. Advance to expert-level subjects like custom operators, property wrappers, result builders, and advanced architectural patterns.

Dedicate time to practical coding practice through platforms like LeetCode, HackerRank, and Codewars. Focus on problems commonly asked in iOS interviews including array manipulation, string processing, tree and graph algorithms, dynamic programming, and linked list operations. Practice implementing these solutions in Swift, utilizing the language's unique features and idiomatic patterns.


2. Fundamental Swift Concepts

Variables, Constants, and Type Inference

Swift distinguishes between variables and constants using var and let keywords. This distinction is fundamental to Swift's safety and performance characteristics. Constants declared with let are immutable, preventing accidental modification and enabling compiler optimizations. Variables declared with var are mutable, allowing value changes throughout their scope.

let maximumLoginAttempts = 3  // Constant - cannot be changed

var currentLoginAttempt = 0   // Variable - can be modified


// Type inference allows Swift to deduce types

let inferredInteger = 42        // Swift infers Int

let inferredDouble = 3.14159    // Swift infers Double

let inferredString = "Hello"    // Swift infers String


// Explicit type annotations

let explicitDouble: Double = 70

let explicitString: String = "Explicitly typed string"

Interview Question: Why does Swift differentiate between var and let, and how does this impact performance?

Answer: Swift's distinction between var and let serves multiple purposes. Immutability provides thread safety guarantees, as constants cannot be modified by multiple threads simultaneously. The compiler can optimize constant access, potentially storing values in registers or applying constant folding. Immutability also clarifies intent, making code more readable by explicitly indicating which values change and which remain constant. This design philosophy encourages developers to default to immutability, only using mutability when necessary.

Data Types and Type Safety

Swift is a strongly typed language with comprehensive type safety. The type system prevents type mismatches at compile time, catching errors before runtime. Swift provides numerous built-in types including integers, floating-point numbers, booleans, strings, and collections.

// Numeric Types

let smallInteger: Int8 = 127

let standardInteger: Int = 42

let largeInteger: Int64 = 9223372036854775807

let floatingPoint: Float = 3.14

let doublePrecision: Double = 3.14159265359


// Boolean

let isSwiftAwesome: Bool = true


// String and Character

let greeting: String = "Hello, Swift!"

let exclamationMark: Character = "!"


// Type safety prevents invalid operations

// let invalid = greeting + isSwiftAwesome  // Compile error

let valid = greeting + String(isSwiftAwesome)  // Explicit conversion required

Optionals: Swift's Solution to Null Safety

Optionals represent one of Swift's most distinctive features, addressing the notorious "billion-dollar mistake" of null pointer exceptions. An optional either contains a value or contains nil, representing the absence of a value. This explicit handling of absence forces developers to acknowledge and handle missing values.

// Declaring optionals

var optionalString: String? = "Hello"

var optionalInteger: Int? = nil


// Unwrapping optionals safely

if let unwrappedString = optionalString {

    print("String contains: \(unwrappedString)")

} else {

    print("String is nil")

}


// Guard statements for early exit

func processUser(name: String?) {

    guard let userName = name else {

        print("Name is required")

        return

    }

    print("Processing user: \(userName)")

}


// Optional chaining

struct Person {

    var residence: Residence?

}


struct Residence {

    var address: Address?

}


struct Address {

    var street: String

}


let person = Person()

let street = person.residence?.address?.street  // Returns nil safely


// Nil coalescing operator

let displayName = optionalString ?? "Guest"


// Force unwrapping (use cautiously)

let forcedString = optionalString!  // Crashes if nil


// Implicitly unwrapped optionals

var implicitString: String! = "Implicitly unwrapped"

Interview Question: Explain the difference between optional binding, optional chaining, and force unwrapping. When should each be used?

Answer: Optional binding using if let or guard let safely extracts values from optionals, executing code only when values exist. This is the preferred method for most scenarios as it handles both cases explicitly. Optional chaining allows calling properties, methods, and subscripts on optional values, returning nil if any element in the chain is nil. It's ideal for accessing nested optional properties without writing multiple unwrapping statements. Force unwrapping using ! should be used sparingly, only when you have absolute certainty a value exists, as it crashes the program if the optional is nil. Implicitly unwrapped optionals using ! declaration are useful for values that are initially nil but will definitely have values before being accessed, common with IBOutlets in UIKit.

Control Flow

Swift provides comprehensive control flow constructs including conditional statements, loops, and pattern matching through switch statements.

// If-else statements

let temperature = 72

if temperature < 60 {

    print("Cold weather")

} else if temperature < 80 {

    print("Pleasant weather")

} else {

    print("Hot weather")

}


// Switch statements with pattern matching

let coordinate = (x: 3, y: 2)

switch coordinate {

case (0, 0):

    print("Origin")

case (_, 0):

    print("On x-axis")

case (0, _):

    print("On y-axis")

case (-2...2, -2...2):

    print("Inside 2x2 box")

default:

    print("Outside the box")

}


// For-in loops

for index in 1...5 {

    print("Index: \(index)")

}


let names = ["Alice", "Bob", "Charlie"]

for name in names {

    print("Hello, \(name)")

}


// Enumerated iteration

for (index, name) in names.enumerated() {

    print("\(index): \(name)")

}


// While loops

var countdown = 5

while countdown > 0 {

    print(countdown)

    countdown -= 1

}


// Repeat-while loops

repeat {

    print("Execute at least once")

} while false

Functions and Closures

Functions in Swift are first-class citizens, meaning they can be assigned to variables, passed as arguments, and returned from other functions. This enables functional programming patterns and higher-order functions.

// Basic function

func greet(person: String) -> String {

    return "Hello, \(person)!"

}


// Function with multiple parameters

func calculateArea(width: Double, height: Double) -> Double {

    return width * height

}


// Function with default parameters

func greet(person: String, greeting: String = "Hello") -> String {

    return "\(greeting), \(person)!"

}


// Variadic parameters

func average(_ numbers: Double...) -> Double {

    let sum = numbers.reduce(0, +)

    return sum / Double(numbers.count)

}


// Inout parameters for modifying arguments

func swapValues(_ a: inout Int, _ b: inout Int) {

    let temp = a

    a = b

    b = temp

}


var x = 5, y = 10

swapValues(&x, &y)

print("x: \(x), y: \(y)")  // x: 10, y: 5


// Function types

var mathFunction: (Int, Int) -> Int


mathFunction = { (a: Int, b: Int) -> Int in

    return a + b

}


// Closures - self-contained blocks of functionality

let numbers = [1, 2, 3, 4, 5]

let doubledNumbers = numbers.map { $0 * 2 }

let evenNumbers = numbers.filter { $0 % 2 == 0 }

let sum = numbers.reduce(0) { $0 + $1 }


// Trailing closure syntax

func performOperation(on value: Int, operation: (Int) -> Int) -> Int {

    return operation(value)

}


let result = performOperation(on: 5) { value in

    return value * value

}


// Capturing values

func makeIncrementer(incrementAmount: Int) -> () -> Int {

    var total = 0

    return {

        total += incrementAmount

        return total

    }

}


let incrementByTwo = makeIncrementer(incrementAmount: 2)

print(incrementByTwo())  // 2

print(incrementByTwo())  // 4


// Escaping closures

func performAsyncOperation(completion: @escaping (String) -> Void) {

    DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {

        completion("Operation completed")

    }

}

Interview Question: What is the difference between escaping and non-escaping closures? Provide examples of when each would be used.

Answer: Non-escaping closures are the default in Swift. They execute within the function scope and are guaranteed to complete before the function returns. Higher-order functions like map, filter, and reduce use non-escaping closures. Escaping closures, marked with @escaping, can outlive the function they're passed to, commonly used for asynchronous operations, completion handlers, and stored closures. When a closure is stored in a property or passed to an asynchronous function, it must be marked as escaping. The distinction matters for memory management because escaping closures must capture self explicitly, helping developers avoid retain cycles.


3. Object-Oriented Programming in Swift

Classes and Structures

Swift supports both classes and structures as fundamental building blocks for object-oriented programming. Understanding when to use each is crucial for effective Swift development.

// Structure definition

struct Point {

    var x: Double

    var y: Double

    

    // Computed property

    var distanceFromOrigin: Double {

        return (x * x + y * y).squareRoot()

    }

    

    // Method

    mutating func moveBy(deltaX: Double, deltaY: Double) {

        x += deltaX

        y += deltaY

    }

}


// Class definition

class Vehicle {

    var currentSpeed: Double = 0.0

    var description: String {

        return "Traveling at \(currentSpeed) mph"

    }

    

    func makeNoise() {

        // Default implementation does nothing

    }

}


// Inheritance

class Car: Vehicle {

    var gear: Int = 1

    

    override func makeNoise() {

        print("Vroom vroom!")

    }

    

    override var description: String {

        return super.description + " in gear \(gear)"

    }

}


// Creating instances

var point = Point(x: 3.0, y: 4.0)

print(point.distanceFromOrigin)  // 5.0


let car = Car()

car.currentSpeed = 60.0

car.gear = 4

print(car.description)  // Traveling at 60.0 mph in gear 4

Key Differences Between Classes and Structures:

Structures are value types, copied when assigned or passed. They're ideal for representing simple data structures, geometric shapes, or ranges. Structures don't support inheritance, have automatic memberwise initializers, and are more performant for small data types.

Classes are reference types, shared when assigned or passed. They support inheritance, allowing polymorphism and subclassing. Classes require explicit initializers, can be deinitialized, and support reference counting. Use classes for complex objects with identity, shared mutable state, or when Objective-C interoperability is required.

// Value semantics with structures

var point1 = Point(x: 1, y: 2)

var point2 = point1

point2.x = 5

print(point1.x)  // 1 - point1 unchanged


// Reference semantics with classes

let car1 = Car()

let car2 = car1

car2.currentSpeed = 100

print(car1.currentSpeed)  // 100 - car1 affected

Properties

Swift provides stored properties, computed properties, property observers, and type properties, offering flexible ways to associate values with types.

class BankAccount {

    // Stored properties

    var accountNumber: String

    var balance: Double = 0.0

    

    // Lazy stored property - computed only when accessed

    lazy var accountStatement: String = {

        return "Account \(accountNumber) has balance \(balance)"

    }()

    

    // Computed properties

    var formattedBalance: String {

        return String(format: "$%.2f", balance)

    }

    

    var isOverdrawn: Bool {

        return balance < 0

    }

    

    // Property with getter and setter

    private var _interestRate: Double = 0.0

    var interestRate: Double {

        get {

            return _interestRate

        }

        set {

            _interestRate = max(0.0, min(newValue, 0.15))  // Clamp between 0-15%

        }

    }

    

    // Property observers

    var status: String = "Active" {

        willSet {

            print("Status will change from \(status) to \(newValue)")

        }

        didSet {

            if status != oldValue {

                print("Status changed from \(oldValue) to \(status)")

            }

        }

    }

    

    init(accountNumber: String) {

        self.accountNumber = accountNumber

    }

    

    // Type property (static)

    static let minimumBalance: Double = 100.0

}


// Using properties

let account = BankAccount(accountNumber: "12345")

account.balance = 1000.0

print(account.formattedBalance)  // $1000.00

account.interestRate = 0.20  // Will be clamped to 0.15

account.status = "Frozen"  // Triggers property observers

Methods and Subscripts

Methods define type-specific functionality, while subscripts provide convenient access to collection elements.

class Matrix {

    var grid: [[Double]]

    let rows: Int

    let columns: Int

    

    init(rows: Int, columns: Int) {

        self.rows = rows

        self.columns = columns

        grid = Array(repeating: Array(repeating: 0.0, count: columns), count: rows)

    }

    

    // Instance method

    func transpose() -> Matrix {

        let result = Matrix(rows: columns, columns: rows)

        for i in 0..<rows {

            for j in 0..<columns {

                result[j, i] = self[i, j]

            }

        }

        return result

    }

    

    // Type method (static)

    static func identityMatrix(size: Int) -> Matrix {

        let matrix = Matrix(rows: size, columns: size)

        for i in 0..<size {

            matrix[i, i] = 1.0

        }

        return matrix

    }

    

    // Subscript for convenient element access

    subscript(row: Int, column: Int) -> Double {

        get {

            precondition(row >= 0 && row < rows && column >= 0 && column < columns,

                        "Index out of range")

            return grid[row][column]

        }

        set {

            precondition(row >= 0 && row < rows && column >= 0 && column < columns,

                        "Index out of range")

            grid[row][column] = newValue

        }

    }

}


// Using methods and subscripts

let matrix = Matrix(rows: 3, columns: 3)

matrix[0, 0] = 1.0

matrix[1, 1] = 2.0

matrix[2, 2] = 3.0


let transposed = matrix.transpose()

let identity = Matrix.identityMatrix(size: 3)

Initialization and Deinitialization

Swift's initialization system ensures all properties have values before use, preventing undefined behavior.

class Person {

    let name: String

    var age: Int

    var address: String?

    

    // Designated initializer

    init(name: String, age: Int) {

        self.name = name

        self.age = age

    }

    

    // Convenience initializer

    convenience init(name: String) {

        self.init(name: name, age: 0)

    }

    

    // Failable initializer

    init?(name: String, age: Int, validAge: Bool) {

        guard validAge && age >= 0 else {

            return nil

        }

        self.name = name

        self.age = age

    }

    

    deinit {

        print("Person \(name) is being deinitialized")

    }

}


class Employee: Person {

    let employeeID: String

    var department: String

    

    init(name: String, age: Int, employeeID: String, department: String) {

        self.employeeID = employeeID

        self.department = department

        super.init(name: name, age: age)

    }

    

    convenience override init(name: String, age: Int) {

        self.init(name: name, age: age, employeeID: "TEMP", department: "Unassigned")

    }

}


// Using initializers

let person1 = Person(name: "Alice", age: 30)

let person2 = Person(name: "Bob")

let person3 = Person(name: "Charlie", age: -5, validAge: false)  // nil

Interview Question: Explain the initialization process in Swift, including the difference between designated and convenience initializers. What is two-phase initialization?

Answer: Swift initialization follows a two-phase process ensuring all properties are initialized before use. Phase one initializes all stored properties from the most derived class up to the base class. Phase two allows customization of properties before the instance is ready. Designated initializers are primary initializers that fully initialize all properties introduced by a class and call a superclass designated initializer. Convenience initializers are secondary, calling a designated initializer from the same class, eventually delegating to a designated initializer. This system prevents accessing properties before initialization and ensures inheritance chains initialize properly. The rules are: designated initializers must call a designated superclass initializer, convenience initializers must call another initializer from the same class, and convenience initializers must ultimately call a designated initializer.


4. Protocol-Oriented Programming

Understanding Protocols

Protocols define blueprints of methods, properties, and requirements that types can adopt. Protocol-oriented programming emphasizes composition over inheritance, a paradigm Swift strongly encourages.

// Basic protocol

protocol Drawable {

    func draw()

}


// Protocol with property requirements

protocol Named {

    var name: String { get set }

    var fullName: String { get }

}


// Protocol with initializer requirement

protocol Initializable {

    init()

}


// Implementing protocols

struct Circle: Drawable {

    var radius: Double

    

    func draw() {

        print("Drawing circle with radius \(radius)")

    }

}


class Rectangle: Drawable, Named {

    var width: Double

    var height: Double

    var name: String

    

    var fullName: String {

        return "Rectangle: \(name)"

    }

    

    init(width: Double, height: Double, name: String) {

        self.width = width

        self.height = height

        self.name = name

    }

    

    func draw() {

        print("Drawing rectangle \(width)x\(height)")

    }

}


// Using protocols as types

let drawables: [Drawable] = [

    Circle(radius: 5),

    Rectangle(width: 10, height: 20, name: "Main")

]


for drawable in drawables {

    drawable.draw()

}

Protocol Extensions

Protocol extensions provide default implementations, enabling powerful code reuse without inheritance.

protocol Vehicle {

    var numberOfWheels: Int { get }

    var color: String { get set }

    func description() -> String

}


// Protocol extension with default implementation

extension Vehicle {

    func description() -> String {

        return "A \(color) vehicle with \(numberOfWheels) wheels"

    }

    

    func isMotorized() -> Bool {

        return numberOfWheels > 1

    }

}


struct Bicycle: Vehicle {

    var numberOfWheels: Int = 2

    var color: String

}


struct Car: Vehicle {

    var numberOfWheels: Int = 4

    var color: String

    var brand: String

    

    // Override default implementation

    func description() -> String {

        return "A \(color) \(brand) car with \(numberOfWheels) wheels"

    }

}


let bike = Bicycle(color: "red")

print(bike.description())  // Uses default implementation

print(bike.isMotorized())  // true


let car = Car(color: "blue", brand: "Tesla")

print(car.description())  // Uses custom implementation

Protocol Composition and Inheritance

Protocols can inherit from other protocols and be composed to create powerful type requirements.

protocol Identifiable {

    var id: String { get }

}


protocol Timestamped {

    var createdAt: Date { get }

    var updatedAt: Date { get set }

}


// Protocol inheritance

protocol Entity: Identifiable, Timestamped {

    var version: Int { get set }

}


// Protocol composition in function parameters

func processEntity(_ entity: Identifiable & Timestamped) {

    print("Processing entity \(entity.id) created at \(entity.createdAt)")

}


struct User: Entity {

    let id: String

    let createdAt: Date

    var updatedAt: Date

    var version: Int

    var username: String

}


let user = User(

    id: "123",

    createdAt: Date(),

    updatedAt: Date(),

    version: 1,

    username: "john_doe"

)


processEntity(user)

Associated Types and Generic Protocols

Associated types enable protocols to work with generic types, providing flexibility while maintaining type safety.

protocol Container {

    associatedtype Item

    

    var count: Int { get }

    mutating func append(_ item: Item)

    subscript(i: Int) -> Item { get }

}


struct Stack<Element>: Container {

    var items: [Element] = []

    

    var count: Int {

        return items.count

    }

    

    mutating func append(_ item: Element) {

        items.append(item)

    }

    

    subscript(i: Int) -> Element {

        return items[i]

    }

    

    mutating func pop() -> Element? {

        return items.popLast()

    }

}


var intStack = Stack<Int>()

intStack.append(1)

intStack.append(2)

intStack.append(3)

print(intStack[0])  // 1


// Generic function with protocol constraint

func allItemsMatch<C1: Container, C2: Container>(

    _ container1: C1,

    _ container2: C2

) -> Bool where C1.Item == C2.Item, C1.Item: Equatable {

    if container1.count != container2.count {

        return false

    }

    

    for i in 0..<container1.count {

        if container1[i] != container2[i] {

            return false

        }

    }

    

    return true

}

Protocol Witnesses and Conditional Conformance

Swift allows conditional protocol conformance, where a type conforms to a protocol only when certain conditions are met.

extension Array: Container {

    // Array already satisfies Container requirements

    // This makes Array conditionally conform to Container

}


// Conditional conformance

extension Array: Equatable where Element: Equatable {

    // Array is Equatable only when its elements are Equatable

}


// Using conditional conformance

let array1 = [1, 2, 3]

let array2 = [1, 2, 3]

print(array1 == array2)  // true - works because Int is Equatable


// Protocol witness pattern for static polymorphism

protocol Drawable {

    func draw() -> String

}


struct DrawingContext {

    func render<D: Drawable>(_ drawable: D) -> String {

        return drawable.draw()

    }

}


struct Point: Drawable {

    var x: Int

    var y: Int

    

    func draw() -> String {

        return "Point(\(x), \(y))"

    }

}


let context = DrawingContext()

let point = Point(x: 10, y: 20)

print(context.render(point))

Interview Question: What is protocol-oriented programming, and how does it differ from object-oriented programming? What advantages does it provide?

Answer: Protocol-oriented programming emphasizes composition and protocol conformance over class inheritance. Unlike OOP which relies on inheritance hierarchies, POP uses protocols to define capabilities that types can adopt. Advantages include greater flexibility since types can conform to multiple protocols but only inherit from one class, value types like structs and enums can participate fully in protocol-oriented designs, default implementations through protocol extensions provide code reuse without inheritance overhead, and retroactive modeling allows adding protocol conformance to existing types including types you don't own. POP reduces coupling, encourages smaller focused protocols, and makes code more testable through protocol-based dependency injection. Swift's standard library exemplifies POP with protocols like Sequence, Collection, Equatable, and Hashable forming the foundation of the type system.


5. Memory Management and ARC

Automatic Reference Counting

Swift uses Automatic Reference Counting (ARC) to manage memory automatically. ARC tracks how many strong references exist to each class instance and deallocates instances when their reference count reaches zero.

class Person {

    let name: String

    var apartment: Apartment?

    

    init(name: String) {

        self.name = name

        print("\(name) is being initialized")

    }

    

    deinit {

        print("\(name) is being deinitialized")

    }

}


class Apartment {

    let unit: String

    var tenant: Person?

    

    init(unit: String) {

        self.unit = unit

        print("Apartment \(unit) is being initialized")

    }

    

    deinit {

        print("Apartment \(unit) is being deinitialized")

    }

}


// Creating strong reference

var john: Person? = Person(name: "John")  // Reference count: 1

var unit4A: Apartment? = Apartment(unit: "4A")  // Reference count: 1


// Creating bidirectional relationship creates strong reference cycle

john?.apartment = unit4A

unit4A?.tenant = john


// Setting to nil doesn't deallocate due to retain cycle

john = nil

unit4A = nil

// Neither Person nor Apartment is deinitialized - memory leak!

Weak and Unowned References

To break retain cycles, Swift provides weak and unowned references that don't increase reference count.

class Person {

    let name: String

    var apartment: Apartment?

    

    init(name: String) {

        self.name = name

    }

    

    deinit {

        print("\(name) is being deinitialized")

    }

}


class Apartment {

    let unit: String

    weak var tenant: Person?  // Weak reference breaks the cycle

    

    init(unit: String) {

        self.unit = unit

    }

    

    deinit {

        print("Apartment \(unit) is being deinitialized")

    }

}


var john: Person? = Person(name: "John")

var unit4A: Apartment? = Apartment(unit: "4A")


john?.apartment = unit4A

unit4A?.tenant = john  // Weak reference doesn't increase reference count


john = nil  // John is deinitialized

// Apartment's weak reference to Person becomes nil automatically


// Unowned references for non-optional relationships

class Customer {

    let name: String

    var card: CreditCard?

    

    init(name: String) {

        self.name = name

    }

    

    deinit {

        print("\(name) is being deinitialized")

    }

}


class CreditCard {

    let number: String

    unowned let customer: Customer  // Unowned because card always has customer

    

    init(number: String, customer: Customer) {

        self.number = number

        self.customer = customer

    }

    

    deinit {

        print("Card \(number) is being deinitialized")

    }

}


var alice: Customer? = Customer(name: "Alice")

alice?.card = CreditCard(number: "1234", customer: alice!)


alice = nil  // Both Customer and CreditCard are deinitialized

Key Differences:

  • Weak references: Optional, automatically become nil when referenced instance is deallocated. Use when reference can legitimately be nil.
  • Unowned references: Non-optional, assume instance always exists. Use when reference should never be nil during its lifetime. Accessing deallocated unowned reference causes crash.

Closure Capture Lists

Closures can create retain cycles by capturing references to instances. Capture lists resolve this by specifying how values should be captured.

class HTMLElement {

    let name: String

    let text: String?

    

    // Closure captures self strongly, creating retain cycle

    lazy var asHTML: () -> String = {

        if let text = self.text {

            return "<\(self.name)>\(text)</\(self.name)>"

        } else {

            return "<\(self.name) />"

        }

    }

    

    init(name: String, text: String? = nil) {

        self.name = name

        self.text = text

    }

    

    deinit {

        print("\(name) is being deinitialized")

    }

}


var paragraph: HTMLElement? = HTMLElement(name: "p", text: "Hello")

print(paragraph?.asHTML() ?? "")

paragraph = nil  // Not deinitialized due to retain cycle!


// Solution: Use capture list with weak or unowned

class HTMLElementFixed {

    let name: String

    let text: String?

    

    lazy var asHTML: () -> String = { [weak self] in

        guard let self = self else {

            return ""

        }

        if let text = self.text {

            return "<\(self.name)>\(text)</\(self.name)>"

        } else {

            return "<\(self.name) />"

        }

    }

    

    // Alternative: unowned self

    lazy var asHTMLUnowned: () -> String = { [unowned self] in

        if let text = self.text {

            return "<\(self.name)>\(text)</\(self.name)>"

        } else {

            return "<\(self.name) />"

        }

    }

    

    init(name: String, text: String? = nil) {

        self.name = name

        self.text = text

    }

    

    deinit {

        print("\(name) is being deinitialized")

    }

}


// Capture list with multiple references

class ViewController {

    var label: UILabel?

    var data: [String] = []

    

    func setupHandler() {

        // Capturing multiple values with different reference types

        someAsyncOperation { [weak self, weak label] result in

            guard let self = self else { return }

            

            self.data.append(result)

            label?.text = result

        }

    }

    

    func someAsyncOperation(completion: @escaping (String) -> Void) {

        // Simulated async operation

    }

}

Value Types and Copy-on-Write

Swift's value types (structs, enums) are copied on assignment, but Swift optimizes this with copy-on-write for collection types.

// Structs are value types - copied on assignment

struct Point {

    var x: Int

    var y: Int

}


var point1 = Point(x: 1, y: 2)

var point2 = point1  // Copied

point2.x = 10

print(point1.x)  // 1 - unchanged


// Arrays use copy-on-write optimization

var array1 = [1, 2, 3, 4, 5]

var array2 = array1  // No copy yet - both reference same storage


array2.append(6)  // Now array2 copies the data before modifying

print(array1.count)  // 5

print(array2.count)  // 6


// Implementing copy-on-write for custom types

final class Storage<T> {

    var items: [T]

    

    init(items: [T] = []) {

        self.items = items

    }

}


struct COWArray<T> {

    private var storage: Storage<T>

    

    init() {

        storage = Storage()

    }

    

    var count: Int {

        return storage.items.count

    }

    

    subscript(index: Int) -> T {

        get {

            return storage.items[index]

        }

        set {

            // Copy storage if it's shared

            if !isKnownUniquelyReferenced(&storage) {

                storage = Storage(items: storage.items)

            }

            storage.items[index] = newValue

        }

    }

    

    mutating func append(_ item: T) {

        if !isKnownUniquelyReferenced(&storage) {

            storage = Storage(items: storage.items)

        }

        storage.items.append(item)

    }

}

Interview Question: Explain the difference between weak and unowned references. When would you use each, and what happens if you use the wrong one?

Answer: Weak references are optional and automatically set to nil when the referenced instance is deallocated. They're used when the reference can legitimately be nil at some point, such as delegate patterns or optional relationships. Unowned references are non-optional and assume the instance always exists during the reference's lifetime. They're used when the reference should never be nil, such as a child object that cannot exist without its parent. Using weak when unowned would suffice adds unnecessary optional handling. Using unowned when weak is needed causes crashes if you access the reference after deallocation. In closures, use weak when self might be nil when the closure executes (like completion handlers), and unowned when self is guaranteed to exist (like immediate callbacks where the closure completes before the instance is deallocated).


6. Concurrency and Asynchronous Programming

Grand Central Dispatch (GCD)

GCD provides low-level APIs for managing concurrent operations using dispatch queues.

// Main queue - for UI updates

DispatchQueue.main.async {

    // Update UI here

    label.text = "Updated"

}


// Global concurrent queue

DispatchQueue.global(qos: .userInitiated).async {

    // Perform background work

    let data = fetchData()

    

    // Return to main queue for UI update

    DispatchQueue.main.async {

        updateUI(with: data)

    }

}


// Quality of Service levels

DispatchQueue.global(qos: .userInteractive).async {

    // Highest priority - user interactive

}


DispatchQueue.global(qos: .userInitiated).async {

    // High priority - user initiated

}


DispatchQueue.global(qos: .utility).async {

    // Low priority - utility

}


DispatchQueue.global(qos: .background).async {

    // Lowest priority - background

}


// Custom serial queue

let serialQueue = DispatchQueue(label: "com.app.serialQueue")

serialQueue.async {

    // Operations execute serially

}


// Custom concurrent queue

let concurrentQueue = DispatchQueue(

    label: "com.app.concurrentQueue",

    attributes: .concurrent

)


concurrentQueue.async {

    // Operations execute concurrently

}


// Dispatch groups for coordinating multiple tasks

let group = DispatchGroup()


group.enter()

fetchUserData { data in

    processUserData(data)

    group.leave()

}


group.enter()

fetchPosts { posts in

    processPosts(posts)

    group.leave()

}


group.notify(queue: .main) {

    print("All tasks completed")

    updateUI()

}


// Barrier for read-write synchronization

class ThreadSafeArray<T> {

    private var array: [T] = []

    private let queue = DispatchQueue(

        label: "com.app.arrayQueue",

        attributes: .concurrent

    )

    

    func append(_ element: T) {

        queue.async(flags: .barrier) {

            self.array.append(element)

        }

    }

    

    func get(at index: Int) -> T? {

        var result: T?

        queue.sync {

            if index < array.count {

                result = array[index]

            }

        }

        return result

    }

    

    var count: Int {

        var result = 0

        queue.sync {

            result = array.count

        }

        return result

    }

}


// Semaphores for limiting concurrent access

let semaphore = DispatchSemaphore(value: 3)  // Max 3 concurrent operations


func performLimitedOperation() {

    DispatchQueue.global().async {

        semaphore.wait()

        defer { semaphore.signal() }

        

        // Perform operation

        print("Executing operation")

        Thread.sleep(forTimeInterval: 2)

    }

}


// Dispatch work items for cancellation

var workItem: DispatchWorkItem?


func scheduleWork() {

    workItem = DispatchWorkItem {

        print("Performing work")

        Thread.sleep(forTimeInterval: 5)

        print("Work completed")

    }

    

    DispatchQueue.global().async(execute: workItem!)

}


func cancelWork() {

    workItem?.cancel()

}

Operation Queues

Operation queues provide higher-level abstraction over GCD with additional features like dependencies and cancellation.

// Basic operation queue

let operationQueue = OperationQueue()

operationQueue.maxConcurrentOperationCount = 3


// Block operations

let operation1 = BlockOperation {

    print("Operation 1")

}


let operation2 = BlockOperation {

    print("Operation 2")

}


operationQueue.addOperation(operation1)

operationQueue.addOperation(operation2)


// Custom operation subclass

class DataProcessingOperation: Operation {

    let data: [Int]

    var result: Int?

    

    init(data: [Int]) {

        self.data = data

        super.init()

    }

    

    override func main() {

        guard !isCancelled else { return }

        

        result = data.reduce(0, +)

        

        guard !isCancelled else { return }

        

        // Additional processing

    }

}


// Operation dependencies

let downloadOperation = BlockOperation {

    print("Downloading data")

    Thread.sleep(forTimeInterval: 2)

}


let processOperation = BlockOperation {

    print("Processing data")

    Thread.sleep(forTimeInterval: 1)

}


let uploadOperation = BlockOperation {

    print("Uploading results")

    Thread.sleep(forTimeInterval: 1)

}


processOperation.addDependency(downloadOperation)

uploadOperation.addDependency(processOperation)


operationQueue.addOperations([

    downloadOperation,

    processOperation,

    uploadOperation

], waitUntilFinished: false)


// Completion block

downloadOperation.completionBlock = {

    print("Download completed")

}


// Cancelling operations

func cancelAllOperations() {

    operationQueue.cancelAllOperations()

}

Async/Await (Swift 5.5+)

Swift 5.5 introduced async/await, providing cleaner asynchronous code syntax.

// Async function

func fetchUser(id: String) async throws -> User {

    let url = URL(string: "https://api.example.com/users/\(id)")!

    let (data, _) = try await URLSession.shared.data(from: url)

    return try JSONDecoder().decode(User.self, from: data)

}


// Calling async functions

func loadUserProfile() async {

    do {

        let user = try await fetchUser(id: "123")

        print("Loaded user: \(user.name)")

    } catch {

        print("Error loading user: \(error)")

    }

}


// Multiple async operations sequentially

func loadUserAndPosts() async throws {

    let user = try await fetchUser(id: "123")

    let posts = try await fetchPosts(userId: user.id)

    

    print("User has \(posts.count) posts")

}


// Parallel async operations with async let

func loadUserData() async throws {

    async let user = fetchUser(id: "123")

    async let posts = fetchPosts(userId: "123")

    async let friends = fetchFriends(userId: "123")

    

    // Await all results

    let (userData, userPosts, userFriends) = try await (user, posts, friends)

    

    print("Loaded all data")

}


// Task groups for dynamic parallelism

func fetchAllUsers(ids: [String]) async throws -> [User] {

    try await withThrowingTaskGroup(of: User.self) { group in

        for id in ids {

            group.addTask {

                try await fetchUser(id: id)

            }

        }

        

        var users: [User] = []

        for try await user in group {

            users.append(user)

        }

        return users

    }

}


// Async sequences

func processDataStream() async throws {

    let url = URL(string: "https://api.example.com/stream")!

    let (bytes, _) = try await URLSession.shared.bytes(from: url)

    

    for try await byte in bytes {

        processByte(byte)

    }

}


// Actor for thread-safe state management

actor BankAccount {

    private var balance: Double = 0

    

    func deposit(amount: Double) {

        balance += amount

    }

    

    func withdraw(amount: Double) throws {

        guard balance >= amount else {

            throw BankError.insufficientFunds

        }

        balance -= amount

    }

    

    func getBalance() -> Double {

        return balance

    }

}


enum BankError: Error {

    case insufficientFunds

}


// Using actors

func performBankingOperations() async throws {

    let account = BankAccount()

    

    await account.deposit(amount: 1000)

    try await account.withdraw(amount: 500)

    let balance = await account.getBalance()

    

    print("Current balance: \(balance)")

}


// MainActor for UI updates

@MainActor

class ViewModel {

    var data: [String] = []

    

    func loadData() async {

        // This automatically runs on main thread

        data = await fetchDataFromBackground()

        // UI update happens on main thread automatically

    }

    

    nonisolated func fetchDataFromBackground() async -> [String] {

        // This can run on background thread

        return ["Item 1", "Item 2"]

    }

}


// Task cancellation

func searchUsers(query: String) async throws -> [User] {

    try Task.checkCancellation()

    

    let results = try await performSearch(query)

    

    if Task.isCancelled {

        return []

    }

    

    return results

}


// Creating and managing tasks

func startBackgroundTask() {

    Task {

        await performLongRunningOperation()

    }

    

    // Detached task - doesn't inherit context

    Task.detached(priority: .background) {

        await performIndependentOperation()

    }

}


// Structured concurrency with async properties

class DataManager {

    var data: [String] {

        get async {

            await fetchData()

        }

    }

    

    private func fetchData() async -> [String] {

        // Fetch data asynchronously

        return ["Data 1", "Data 2"]

    }

}

Interview Question: Compare GCD, Operation Queues, and async/await. When would you use each approach?

Answer: GCD is best for simple asynchronous tasks and low-level thread management, offering minimal overhead and direct control over queues. Use it for simple background tasks, dispatching to main queue, or barrier-based synchronization. Operation Queues provide higher-level abstractions with dependencies, priorities, and cancellation, ideal for complex workflows, dependent operations, or when you need fine-grained control over operation execution. Async/await is Swift's modern concurrency model, providing cleaner syntax, better error handling, and compiler-enforced safety. Use it for any new asynchronous code, API calls, sequential dependent operations, or parallel operations with structured concurrency. Actors complement async/await for thread-safe state management. In practice, use async/await for new code, operation queues for complex dependent workflows, and GCD for performance-critical low-level operations or maintaining legacy code.


7. Error Handling and Optionals

Error Handling with Do-Catch

Swift's error handling provides a clear, type-safe way to propagate and handle errors.

// Defining error types

enum NetworkError: Error {

    case invalidURL

    case noConnection

    case serverError(code: Int)

    case decodingFailed

    case timeout

}


// Functions that throw errors

func fetchData(from urlString: String) throws -> Data {

    guard let url = URL(string: urlString) else {

        throw NetworkError.invalidURL

    }

    

    guard Reachability.isConnected else {

        throw NetworkError.noConnection

    }

    

    // Simulated network request

    let statusCode = 200

    guard statusCode == 200 else {

        throw NetworkError.serverError(code: statusCode)

    }

    

    return Data()

}


// Handling errors with do-catch

func loadUserData() {

    do {

        let data = try fetchData(from: "https://api.example.com/user")

        let user = try JSONDecoder().decode(User.self, from: data)

        print("Loaded user: \(user.name)")

    } catch NetworkError.invalidURL {

        print("Invalid URL provided")

    } catch NetworkError.noConnection {

        print("No internet connection")

    } catch NetworkError.serverError(let code) {

        print("Server error with code: \(code)")

    } catch {

        print("Unexpected error: \(error)")

    }

}


// Try? for optional results

func loadOptionalData() {

    if let data = try? fetchData(from: "https://api.example.com/user") {

        print("Data loaded successfully")

    } else {

        print("Failed to load data")

    }

}


// Try! for guaranteed success (use cautiously)

func loadRequiredData() {

    let data = try! fetchData(from: "https://api.example.com/user")

    // Crashes if error is thrown - only use when failure is impossible

}


// Rethrows for functions that take throwing closures

func performOperation<T>(

    operation: () throws -> T

) rethrows -> T {

    return try operation()

}


// Converting errors

enum AppError: Error {

    case dataError

    case networkError(NetworkError)

}


func loadData() throws {

    do {

        let data = try fetchData(from: "https://api.example.com/user")

        processData(data)

    } catch let error as NetworkError {

        throw AppError.networkError(error)

    } catch {

        throw AppError.dataError

    }

}


// Custom error with localized description

enum ValidationError: LocalizedError {

    case emptyField(fieldName: String)

    case invalidEmail

    case passwordTooShort(minimumLength: Int)

    

    var errorDescription: String? {

        switch self {

        case .emptyField(let fieldName):

            return "\(fieldName) cannot be empty"

        case .invalidEmail:

            return "Please enter a valid email address"

        case .passwordTooShort(let length):

            return "Password must be at least \(length) characters"

        }

    }

}


// Using custom errors

func validateUser(email: String?, password: String?) throws {

    guard let email = email, !email.isEmpty else {

        throw ValidationError.emptyField(fieldName: "Email")

    }

    

    guard email.contains("@") && email.contains(".") else {

        throw ValidationError.invalidEmail

    }

    

    guard let password = password, !password.isEmpty else {

        throw ValidationError.emptyField(fieldName: "Password")

    }

    

    guard password.count >= 8 else {

        throw ValidationError.passwordTooShort(minimumLength: 8)

    }

}

Result Type

The Result type encapsulates success or failure, useful for asynchronous operations.

// Using Result type

func fetchUser(

    id: String,

    completion: @escaping (Result<User, NetworkError>) -> Void

) {

    // Simulated async operation

    DispatchQueue.global().asyncAfter(deadline: .now() + 1) {

        if id.isEmpty {

            completion(.failure(.invalidURL))

        } else {

            let user = User(id: id, name: "John Doe")

            completion(.success(user))

        }

    }

}


// Handling Result

fetchUser(id: "123") { result in

    switch result {

    case .success(let user):

        print("Loaded user: \(user.name)")

    case .failure(let error):

        print("Error: \(error)")

    }

}


// Result with map and flatMap

let result: Result<Int, Error> = .success(5)


let doubled = result.map { $0 * 2 }  // Result<Int, Error>

let stringResult = result.map { String($0) }  // Result<String, Error>


// FlatMap for chaining Results

func parseJSON(data: Data) -> Result<[String: Any], Error> {

    // Parsing implementation

    return .success([:])

}


func extractUser(json: [String: Any]) -> Result<User, Error> {

    // Extraction implementation

    return .success(User(id: "1", name: "Alice"))

}


let dataResult: Result<Data, Error> = .success(Data())

let userResult = dataResult

    .flatMap { parseJSON(data: $0) }

    .flatMap { extractUser(json: $0) }


// Converting Result to throwing expression

func loadUser() throws -> User {

    let result = fetchUserSynchronously()

    return try result.get()  // Throws if failure

}


// Converting throwing expression to Result

let result2 = Result { try loadUser() }

Advanced Optional Patterns

// Optional chaining

class Person {

    var residence: Residence?

}


class Residence {

    var rooms: [Room] = []

    var address: Address?

}


class Room {

    let name: String

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

}


class Address {

    var street: String?

    var city: String?

}


let person = Person()

let roomCount = person.residence?.rooms.count  // nil

let street = person.residence?.address?.street  // nil


// Optional pattern matching

let optionalValue: Int? = 42


if case let value? = optionalValue {

    print("Value is \(value)")

}


// Switch with optionals

switch optionalValue {

case .none:

    print("No value")

case .some(let value):

    print("Value: \(value)")

}


// Nil coalescing with multiple optionals

let primary: String? = nil

let secondary: String? = nil

let fallback = "Default"


let result3 = primary ?? secondary ?? fallback


// Combining optionals

func fullName(first: String?, last: String?) -> String? {

    guard let first = first, let last = last else {

        return nil

    }

    return "\(first) \(last)"

}


// Optional dictionary access

var scores: [String: Int] = ["Alice": 95, "Bob": 87]

let aliceScore = scores["Alice"]  // Int?

let charlieScore = scores["Charlie", default: 0]  // Int


// Compacting optionals from arrays

let possibleNumbers = ["1", "2", "three", "4", "five"]

let actualNumbers = possibleNumbers.compactMap { Int($0) }

print(actualNumbers)  // [1, 2, 4]

Interview Question: Explain the difference between try, try?, and try!. When should each be used?

Answer: The try keyword is used with do-catch blocks for explicit error handling, allowing you to handle different error cases distinctly. Use it when you want to handle errors specifically and take different actions based on error types. try? converts the result to an optional, returning nil if an error occurs, discarding the actual error. Use it when you only care about success/failure but not the specific error, or when failure is acceptable and nil is a reasonable fallback. try! force-unwraps the result, crashing if an error occurs. Only use it when you're absolutely certain an error cannot occur, such as loading bundled resources or in scenarios where continuing would be meaningless. In production code, use try with do-catch for most cases, try? for optional results, and avoid try! except in truly exceptional circumstances.


8. Collections and Generics

Arrays

Arrays are ordered collections of values of the same type.

// Creating arrays

var integers: [Int] = [1, 2, 3, 4, 5]

var strings = ["apple", "banana", "cherry"]

var emptyArray: [String] = []

var repeatingArray = Array(repeating: 0, count: 5)


// Accessing elements

let first = integers[0]

let last = integers[integers.count - 1]

let safeFirst = integers.first  // Optional

let safeLast = integers.last    // Optional


// Modifying arrays

integers.append(6)

integers.insert(0, at: 0)

integers.remove(at: 0)

integers.removeLast()

integers += [7, 8, 9]


// Array slicing

let slice = integers[1...3]  // ArraySlice

let subArray = Array(slice)  // Convert to Array


// Iterating

for number in integers {

    print(number)

}


for (index, number) in integers.enumerated() {

    print("\(index): \(number)")

}


// Higher-order functions

let doubled = integers.map { $0 * 2 }

let evens = integers.filter { $0 % 2 == 0 }

let sum = integers.reduce(0, +)

let product = integers.reduce(1, *)


// Sorted and sorting

let sortedIntegers = integers.sorted()

let descendingIntegers = integers.sorted(by: >)


integers.sort()  // Mutating sort


// Contains and first(where:)

let containsFive = integers.contains(5)

let firstEven = integers.first(where: { $0 % 2 == 0 })


// All and none

let allPositive = integers.allSatisfy { $0 > 0 }

let noneNegative = !integers.contains(where: { $0 < 0 })


// Partition

var numbers = [3, 1, 4, 1, 5, 9, 2, 6]

let pivotIndex = numbers.partition(by: { $0 < 5 })

// Elements < 5 are before pivotIndex


// Chunked (Swift 5.7+)

let chunked = numbers.chunks(ofCount: 3)

Sets

Sets store unique, unordered values.

// Creating sets

var fruits: Set<String> = ["apple", "banana", "cherry"]

var numbers: Set = [1, 2, 3, 4, 5]

var emptySet = Set<Int>()


// Adding and removing

fruits.insert("date")

fruits.remove("banana")


if let removed = fruits.remove("cherry") {

    print("Removed: \(removed)")

}


// Set operations

let set1: Set = [1, 2, 3, 4, 5]

let set2: Set = [4, 5, 6, 7, 8]


let union = set1.union(set2)              // [1, 2, 3, 4, 5, 6, 7, 8]

let intersection = set1.intersection(set2) // [4, 5]

let difference = set1.subtracting(set2)    // [1, 2, 3]

let symmetricDiff = set1.symmetricDifference(set2)  // [1, 2, 3, 6, 7, 8]


// Set relationships

let smallSet: Set = [1, 2, 3]

let largeSet: Set = [1, 2, 3, 4, 5]


smallSet.isSubset(of: largeSet)      // true

largeSet.isSuperset(of: smallSet)    // true

smallSet.isDisjoint(with: set2)      // false


// Checking membership

if fruits.contains("apple") {

    print("Has apple")

}


// Iterating

for fruit in fruits.sorted() {

    print(fruit)

}

Dictionaries

Dictionaries store key-value pairs with unique keys.

// Creating dictionaries

var airports: [String: String] = ["YYZ": "Toronto", "DUB": "Dublin"]

var scores = ["Alice": 95, "Bob": 87, "Charlie": 92]

var emptyDict: [String: Int] = [:]


// Accessing values

let torontoCode = airports["YYZ"]  // Optional

let aliceScore = scores["Alice", default: 0]  // Non-optional with default


// Modifying dictionaries

airports["LHR"] = "London"

airports["DUB"] = "Dublin Airport"  // Update

airports.updateValue("Frankfurt", forKey: "FRA")


if let oldValue = airports.updateValue("Sydney", forKey: "SYD") {

    print("Replaced \(oldValue)")

} else {

    print("Added new value")

}


airports["YYZ"] = nil  // Remove

airports.removeValue(forKey: "LHR")


// Iterating

for (code, city) in airports {

    print("\(code): \(city)")

}


for code in airports.keys {

    print(code)

}


for city in airports.values {

    print(city)

}


// Sorted dictionaries

let sortedAirports = airports.sorted { $0.key < $1.key }


// Mapping and filtering

let lowercaseCodes = airports.mapValues { $0.lowercased() }

let filtered = airports.filter { $0.value.count > 6 }


// Merging dictionaries

var dict1 = ["a": 1, "b": 2]

let dict2 = ["b": 3, "c": 4]


dict1.merge(dict2) { (current, new) in new }  // Keep new values

// dict1 is now ["a": 1, "b": 3, "c": 4]


// Grouping

let words = ["apple", "banana", "apricot", "blueberry", "avocado"]

let grouped = Dictionary(grouping: words) { $0.first! }

// ["a": ["apple", "apricot", "avocado"], "b": ["banana", "blueberry"]]


// Compacting

let sparse: [String: String?] = ["a": "apple", "b": nil, "c": "cherry"]

let compacted = sparse.compactMapValues { $0 }

// ["a": "apple", "c": "cherry"]

Generics

Generics enable writing flexible, reusable functions and types.

// Generic function

func swapValues<T>(_ a: inout T, _ b: inout T) {

    let temp = a

    a = b

    b = temp

}


var x = 5, y = 10

swapValues(&x, &y)


var str1 = "hello", str2 = "world"

swapValues(&str1, &str2)


// Generic type

struct Queue<Element> {

    private var elements: [Element] = []

    

    mutating func enqueue(_ element: Element) {

        elements.append(element)

    }

    

    mutating func dequeue() -> Element? {

        return elements.isEmpty ? nil : elements.removeFirst()

    }

    

    func peek() -> Element? {

        return elements.first

    }

    

    var isEmpty: Bool {

        return elements.isEmpty

    }

    

    var count: Int {

        return elements.count

    }

}


var intQueue = Queue<Int>()

intQueue.enqueue(1)

intQueue.enqueue(2)

print(intQueue.dequeue())  // Optional(1)


// Generic constraints

func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {

    for (index, value) in array.enumerated() {

        if value == valueToFind {

            return index

        }

    }

    return nil

}


let numbers2 = [1, 2, 3, 4, 5]

let index = findIndex(of: 3, in: numbers2)  // Optional(2)


// Multiple generic parameters

func combine<T, U>(first: T, second: U) -> (T, U) {

    return (first, second)

}


let result4 = combine(first: 42, second: "hello")


// Associated types with protocols

protocol Container {

    associatedtype Item

    mutating func append(_ item: Item)

    var count: Int { get }

    subscript(i: Int) -> Item { get }

}


struct Stack<Element>: Container {

    private var items: [Element] = []

    

    mutating func append(_ item: Element) {

        items.append(item)

    }

    

    var count: Int {

        return items.count

    }

    

    subscript(i: Int) -> Element {

        return items[i]

    }

    

    mutating func pop() -> Element? {

        return items.popLast()

    }

}


// Generic where clauses

extension Container where Item: Equatable {

    func contains(_ item: Item) -> Bool {

        for i in 0..<count {

            if self[i] == item {

                return true

            }

        }

        return false

    }

}


// Generic subscripts

extension Container {

    subscript<Indices: Sequence>(indices: Indices) -> [Item]

        where Indices.Element == Int {

        var result: [Item] = []

        for index in indices {

            result.append(self[index])

        }

        return result

    }

}


// Opaque types with some keyword

protocol Shape {

    func draw() -> String

}


struct Triangle: Shape {

    func draw() -> String {

        return "Triangle"

    }

}


struct Square: Shape {

    func draw() -> String {

        return "Square"

    }

}


func makeShape(isTriangle: Bool) -> some Shape {

    if isTriangle {

        return Triangle()

    } else {

        return Square()

    }

}

Interview Question: Explain the purpose of generics in Swift. How do they differ from using Any or protocols?

Answer: Generics provide type-safe, reusable code that works with any type while maintaining type information at compile time. Unlike Any, which erases type information requiring runtime casting, generics preserve types, enabling compiler optimization and catching type errors early. Compared to protocols, generics avoid protocol requirements and associated type complexity when you simply need type flexibility. Generics excel for collections, algorithms, and data structures that work identically across types. They're more performant than protocols with associated types due to static dispatch and specialization. The compiler generates specific versions of generic code for each type used, enabling optimal performance. Use generics when you need type flexibility without sacrificing type safety or performance. Use protocols when you need polymorphism with different implementations or when working with heterogeneous collections.


9. Advanced Swift Features

Property Wrappers

Property wrappers encapsulate property access logic, reducing boilerplate code.

// Basic property wrapper

@propertyWrapper

struct Capitalized {

    private var value: String = ""

    

    var wrappedValue: String {

        get { return value }

        set { value = newValue.capitalized }

    }

}


struct User {

    @Capitalized var firstName: String

    @Capitalized var lastName: String

}


var user = User()

user.firstName = "john"

user.lastName = "doe"

print(user.firstName)  // "John"

print(user.lastName)   // "Doe"


// Property wrapper with initial value

@propertyWrapper

struct Clamped<T: Comparable> {

    private var value: T

    private let range: ClosedRange<T>

    

    var wrappedValue: T {

        get { return value }

        set { value = min(max(range.lowerBound, newValue), range.upperBound) }

    }

    

    init(wrappedValue: T, _ range: ClosedRange<T>) {

        self.range = range

        self.value = min(max(range.lowerBound, wrappedValue), range.upperBound)

    }

}


struct Game {

    @Clamped(0...100) var health: Int = 100

    @Clamped(0...100) var energy: Int = 100

}


var game = Game()

game.health = 150  // Clamped to 100

game.energy = -10  // Clamped to 0


// Property wrapper with projected value

@propertyWrapper

struct Validated<T> {

    private var value: T

    private var isValid: Bool = true

    private let validator: (T) -> Bool

    

    var wrappedValue: T {

        get { return value }

        set {

            isValid = validator(newValue)

            if isValid {

                value = newValue

            }

        }

    }

    

    var projectedValue: Bool {

        return isValid

    }

    

    init(wrappedValue: T, validator: @escaping (T) -> Bool) {

        self.validator = validator

        self.value = wrappedValue

        self.isValid = validator(wrappedValue)

    }

}


struct SignupForm {

    @Validated(validator: { $0.contains("@") && $0.contains(".") })

    var email: String = ""

    

    @Validated(validator: { $0.count >= 8 })

    var password: String = ""

    

    var canSubmit: Bool {

        return $email && $password  // Accessing projected values

    }

}


var form = SignupForm()

form.email = "user@example.com"

form.password = "password123"

print(form.canSubmit)  // true


// UserDefaults property wrapper

@propertyWrapper

struct UserDefault<T> {

    let key: String

    let defaultValue: T

    

    var wrappedValue: T {

        get {

            return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue

        }

        set {

            UserDefaults.standard.set(newValue, forKey: key)

        }

    }

}


struct Settings {

    @UserDefault(key: "isDarkMode", defaultValue: false)

    static var isDarkMode: Bool

    

    @UserDefault(key: "fontSize", defaultValue: 14.0)

    static var fontSize: Double

}


// Using the settings

Settings.isDarkMode = true

print(Settings.isDarkMode)

Result Builders

Result builders enable creating DSL-like syntax for building values.

// HTML builder

@resultBuilder

struct HTMLBuilder {

    static func buildBlock(_ components: String...) -> String {

        components.joined()

    }

    

    static func buildOptional(_ component: String?) -> String {

        component ?? ""

    }

    

    static func buildEither(first component: String) -> String {

        component

    }

    

    static func buildEither(second component: String) -> String {

        component

    }

    

    static func buildArray(_ components: [String]) -> String {

        components.joined()

    }

}


func html(@HTMLBuilder content: () -> String) -> String {

    return "<html>\(content())</html>"

}


func body(@HTMLBuilder content: () -> String) -> String {

    return "<body>\(content())</body>"

}


func div(@HTMLBuilder content: () -> String) -> String {

    return "<div>\(content())</div>"

}


func p(_ text: String) -> String {

    return "<p>\(text)</p>"

}


func h1(_ text: String) -> String {

    return "<h1>\(text)</h1>"

}


// Using the HTML builder

let webpage = html {

    body {

        div {

            h1("Welcome")

            p("This is a paragraph")

        }

    }

}


print(webpage)


// SwiftUI-style builder

@resultBuilder

struct ViewBuilder {

    static func buildBlock(_ components: View...) -> View {

        VStack(components)

    }

    

    static func buildOptional(_ component: View?) -> View {

        component ?? EmptyView()

    }

    

    static func buildEither(first component: View) -> View {

        component

    }

    

    static func buildEither(second component: View) -> View {

        component

    }

}


protocol View {

    func render() -> String

}


struct Text: View {

    let content: String

    func render() -> String { return content }

}


struct EmptyView: View {

    func render() -> String { return "" }

}


struct VStack: View {

    let children: [View]

    

    init(_ children: [View]) {

        self.children = children

    }

    

    func render() -> String {

        return children.map { $0.render() }.joined(separator: "\n")

    }

}


func buildView(@ViewBuilder content: () -> View) -> View {

    return content()

}


// Using the view builder

let view = buildView {

    Text("Title")

    Text("Subtitle")

}

Custom Operators

Swift allows defining custom operators with specific precedence and associativity.

// Custom infix operator

infix operator **: MultiplicationPrecedence


func **(base: Double, exponent: Double) -> Double {

    return pow(base, exponent)

}


let result5 = 2 ** 3  // 8.0


// Custom operator for vector addition

struct Vector2D {

    var x: Double

    var y: Double

}


func +(left: Vector2D, right: Vector2D) -> Vector2D {

    return Vector2D(x: left.x + right.x, y: left.y + right.y)

}


func *(vector: Vector2D, scalar: Double) -> Vector2D {

    return Vector2D(x: vector.x * scalar, y: vector.y * scalar)

}


let v1 = Vector2D(x: 1, y: 2)

let v2 = Vector2D(x: 3, y: 4)

let v3 = v1 + v2  // Vector2D(x: 4, y: 6)

let v4 = v1 * 2   // Vector2D(x: 2, y: 4)


// Custom precedence group

precedencegroup ExponentiationPrecedence {

    higherThan: MultiplicationPrecedence

    associativity: right

}


infix operator ^^^: ExponentiationPrecedence


func ^^^(base: Int, exponent: Int) -> Int {

    return Int(pow(Double(base), Double(exponent)))

}


let result6 = 2 ^^^ 3 ^^^ 2  // 512 (right associative)


// Pattern matching operator

struct Range<T: Comparable> {

    let lower: T

    let upper: T

}


func ~=<T: Comparable>(pattern: Range<T>, value: T) -> Bool {

    return value >= pattern.lower && value <= pattern.upper

}


let temperatureRange = Range(lower: 60, upper: 80)

let temperature = 75


switch temperature {

case temperatureRange:

    print("Pleasant temperature")

default:

    print("Extreme temperature")

}

Key Paths

Key paths provide type-safe references to properties.

struct Person2 {

    var name: String

    var age: Int

    var address: Address2

}


struct Address2 {

    var street: String

    var city: String

}


// Creating key paths

let nameKeyPath = \Person2.name

let ageKeyPath = \Person2.age

let cityKeyPath = \Person2.address.city


var person = Person2(

    name: "John",

    age: 30,

    address: Address2(street: "Main St", city: "New York")

)


// Reading values

let name = person[keyPath: nameKeyPath]

let city = person[keyPath: cityKeyPath]


// Writing values

person[keyPath: nameKeyPath] = "Jane"

person[keyPath: ageKeyPath] = 31


// Using key paths for sorting

let people = [

    Person2(name: "Alice", age: 25, address: Address2(street: "1st Ave", city: "Boston")),

    Person2(name: "Bob", age: 35, address: Address2(street: "2nd Ave", city: "Chicago")),

    Person2(name: "Charlie", age: 30, address: Address2(street: "3rd Ave", city: "Austin"))

]


let sortedByAge = people.sorted(by: { $0[keyPath: ageKeyPath] < $1[keyPath: ageKeyPath] })


// Key path member lookup

@dynamicMemberLookup

struct JSON {

    private var data: [String: Any]

    

    init(_ data: [String: Any]) {

        self.data = data

    }

    

    subscript(dynamicMember member: String) -> Any? {

        return data[member]

    }

}


let json = JSON(["name": "John", "age": 30])

let jsonName = json.name

let jsonAge = json.age

Type Erasure

Type erasure hides concrete types behind protocol interfaces.

// Problem: Protocols with associated types can't be used as types

protocol Iterator {

    associatedtype Element

    func next() -> Element?

}


// Type-erased wrapper

struct AnyIterator<Element>: Iterator {

    private let _next: () -> Element?

    

    init<I: Iterator>(_ iterator: I) where I.Element == Element {

        var iterator = iterator

        _next = { iterator.next() }

    }

    

    func next() -> Element? {

        return _next()

    }

}


// Concrete iterator

struct ArrayIterator<T>: Iterator {

    private var array: [T]

    private var index = 0

    

    init(_ array: [T]) {

        self.array = array

    }

    

    mutating func next() -> T? {

        guard index < array.count else { return nil }

        defer { index += 1 }

        return array[index]

    }

}


// Using type erasure

let iterator = ArrayIterator([1, 2, 3])

let anyIterator = AnyIterator(iterator)


while let value = anyIterator.next() {

    print(value)

}


// Another example: Type-erased storage

protocol Storage {

    associatedtype Item

    func store(_ item: Item)

    func retrieve() -> Item?

}


class AnyStorage<Item>: Storage {

    private let _store: (Item) -> Void

    private let _retrieve: () -> Item?

    

    init<S: Storage>(_ storage: S) where S.Item == Item {

        var storage = storage

        _store = { storage.store($0) }

        _retrieve = { storage.retrieve() }

    }

    

    func store(_ item: Item) {

        _store(item)

    }

    

    func retrieve() -> Item? {

        return _retrieve()

    }

}

Interview Question: What are property wrappers and what problem do they solve? Provide practical examples.

Answer: Property wrappers encapsulate property access logic, eliminating repetitive code for common patterns like validation, transformation, persistence, or thread safety. They solve the problem of duplicating get/set logic across multiple properties. Practical examples include UserDefaults storage where @UserDefault automatically persists and retrieves values, validation where @Validated ensures values meet criteria, clamping where @Clamped restricts values to ranges, lazy initialization patterns, and thread-safe access. SwiftUI extensively uses property wrappers like @State, @Binding, and @Published. Property wrappers provide projected values through the dollar sign prefix, allowing access to auxiliary information like validation state or bindings. They improve code readability by making property behavior explicit and reusable, reduce errors by centralizing logic, and make codebases more maintainable.


10. iOS Development Integration

UIKit Integration

// View Controller lifecycle

class MyViewController: UIViewController {

    // MARK: - Properties

    private lazy var tableView: UITableView = {

        let table = UITableView()

        table.delegate = self

        table.dataSource = self

        table.register(UITableViewCell.self, forCellReuseIdentifier: "cell")

        return table

    }()

    

    private var data: [String] = []

    

    // MARK: - Lifecycle

    override func viewDidLoad() {

        super.viewDidLoad()

        setupUI()

        loadData()

    }

    

    override func viewWillAppear(_ animated: Bool) {

        super.viewWillAppear(animated)

        // View is about to appear

    }

    

    override func viewDidAppear(_ animated: Bool) {

        super.viewDidAppear(animated)

        // View has appeared

    }

    

    override func viewWillDisappear(_ animated: Bool) {

        super.viewWillDisappear(animated)

        // View is about to disappear

    }

    

    override func viewDidDisappear(_ animated: Bool) {

        super.viewDidDisappear(animated)

        // View has disappeared

    }

    

    // MARK: - Setup

    private func setupUI() {

        view.addSubview(tableView)

        tableView.translatesAutoresizingMaskIntoConstraints = false

        

        NSLayoutConstraint.activate([

            tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),

            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),

            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),

            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)

        ])

    }

    

    private func loadData() {

        // Load data asynchronously

        DispatchQueue.global().async { [weak self] in

            let fetchedData = self?.fetchData() ?? []

            

            DispatchQueue.main.async {

                self?.data = fetchedData

                self?.tableView.reloadData()

            }

        }

    }

    

    private func fetchData() -> [String] {

        // Simulate data fetching

        return ["Item 1", "Item 2", "Item 3"]

    }

}


// MARK: - UITableViewDataSource

extension MyViewController: UITableViewDataSource {

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

        return data.count

    }

    

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)

        cell.textLabel?.text = data[indexPath.row]

        return cell

    }

}


// MARK: - UITableViewDelegate

extension MyViewController: UITableViewDelegate {

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

        tableView.deselectRow(at: indexPath, animated: true)

        print("Selected: \(data[indexPath.row])")

    }

}


// Delegation pattern

protocol DataManagerDelegate: AnyObject {

    func dataManager(_ manager: DataManager, didUpdate data: [String])

    func dataManager(_ manager: DataManager, didFailWithError error: Error)

}


class DataManager {

    weak var delegate: DataManagerDelegate?

    

    func fetchData() {

        // Simulate async data fetching

        DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [weak self] in

            guard let self = self else { return }

            

            let data = ["Data 1", "Data 2", "Data 3"]

            

            DispatchQueue.main.async {

                self.delegate?.dataManager(self, didUpdate: data)

            }

        }

    }

}


// Using delegation

class ViewController: UIViewController, DataManagerDelegate {

    let dataManager = DataManager()

    

    override func viewDidLoad() {

        super.viewDidLoad()

        dataManager.delegate = self

        dataManager.fetchData()

    }

    

    func dataManager(_ manager: DataManager, didUpdate data: [String]) {

        print("Received data: \(data)")

    }

    

    func dataManager(_ manager: DataManager, didFailWithError error: Error) {

        print("Error: \(error)")

    }

}

SwiftUI Integration

import SwiftUI


// Basic SwiftUI view

struct ContentView: View {

    @State private var count = 0

    @State private var text = ""

    @State private var isPresented = false

    

    var body: some View {

        NavigationView {

            VStack(spacing: 20) {

                Text("Counter: \(count)")

                    .font(.largeTitle)

                

                HStack {

                    Button("Decrement") {

                        count -= 1

                    }

                    

                    Button("Increment") {

                        count += 1

                    }

                }

                

                TextField("Enter text", text: $text)

                    .textFieldStyle(RoundedBorderTextFieldStyle())

                    .padding()

                

                Button("Show Sheet") {

                    isPresented = true

                }

                .sheet(isPresented: $isPresented) {

                    SheetView(count: $count)

                }

                

                List {

                    ForEach(0..<5) { index in

                        Text("Row \(index)")

                    }

                }

            }

            .navigationTitle("SwiftUI Demo")

        }

    }

}


struct SheetView: View {

    @Binding var count: Int

    @Environment(\.presentationMode) var presentationMode

    

    var body: some View {

        VStack {

            Text("Sheet View")

                .font(.title)

            

            Text("Count: \(count)")

            

            Button("Dismiss") {

                presentationMode.wrappedValue.dismiss()

            }

        }

    }

}


// ObservableObject for state management

class UserViewModel: ObservableObject {

    @Published var users: [User] = []

    @Published var isLoading = false

    @Published var errorMessage: String?

    

    func loadUsers() {

        isLoading = true

        errorMessage = nil

        

        // Simulate API call

        DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in

            self?.users = [

                User(id: "1", name: "Alice"),

                User(id: "2", name: "Bob"),

                User(id: "3", name: "Charlie")

            ]

            self?.isLoading = false

        }

    }

    

    func deleteUser(at indexSet: IndexSet) {

        users.remove(atOffsets: indexSet)

    }

}


struct UserListView: View {

    @StateObject private var viewModel = UserViewModel()

    

    var body: some View {

        NavigationView {

            Group {

                if viewModel.isLoading {

                    ProgressView("Loading...")

                } else if let error = viewModel.errorMessage {

                    Text("Error: \(error)")

                        .foregroundColor(.red)

                } else {

                    List {

                        ForEach(viewModel.users) { user in

                            NavigationLink(destination: UserDetailView(user: user)) {

                                Text(user.name)

                            }

                        }

                        .onDelete(perform: viewModel.deleteUser)

                    }

                }

            }

            .navigationTitle("Users")

            .toolbar {

                ToolbarItem(placement: .navigationBarTrailing) {

                    Button("Refresh") {

                        viewModel.loadUsers()

                    }

                }

            }

        }

        .onAppear {

            viewModel.loadUsers()

        }

    }

}


struct UserDetailView: View {

    let user: User

    

    var body: some View {

        VStack {

            Text(user.name)

                .font(.largeTitle)

            

            Text("ID: \(user.id)")

                .font(.caption)

                .foregroundColor(.secondary)

        }

        .navigationTitle("User Details")

    }

}

Combine Framework

import Combine


// Publishers and Subscribers

class DataService {

    @Published var data: [String] = []

    

    func fetchData() {

        // Simulate async data fetching

        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {

            self.data = ["Item 1", "Item 2", "Item 3"]

        }

    }

}


class ViewModel {

    private var cancellables = Set<AnyCancellable>()

    private let service = DataService()

    

    @Published var items: [String] = []

    @Published var isLoading = false

    

    init() {

        setupBindings()

    }

    

    private func setupBindings() {

        service.$data

            .assign(to: &$items)

        

        $items

            .map { !$0.isEmpty }

            .assign(to: &$isLoading)

    }

    

    func loadData() {

        service.fetchData()

    }

}


// Advanced Combine operators

class SearchViewModel {

    @Published var searchText = ""

    @Published var searchResults: [String] = []

    

    private var cancellables = Set<AnyCancellable>()

    

    init() {

        $searchText

            .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)

            .removeDuplicates()

            .filter { !$0.isEmpty }

            .map { query in

                self.performSearch(query: query)

            }

            .switchToLatest()

            .receive(on: DispatchQueue.main)

            .assign(to: &$searchResults)

    }

    

    private func performSearch(query: String) -> AnyPublisher<[String], Never> {

        Future { promise in

            // Simulate network request

            DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {

                let results = ["Result 1", "Result 2", "Result 3"]

                    .filter { $0.localizedCaseInsensitiveContains(query) }

                promise(.success(results))

            }

        }

        .eraseToAnyPublisher()

    }

}


// Combining multiple publishers

class CombineExamples {

    private var cancellables = Set<AnyCancellable>()

    

    func combineLatestExample() {

        let publisher1 = PassthroughSubject<Int, Never>()

        let publisher2 = PassthroughSubject<String, Never>()

        

        Publishers.CombineLatest(publisher1, publisher2)

            .sink { number, text in

                print("Combined: \(number) - \(text)")

            }

            .store(in: &cancellables)

        

        publisher1.send(1)

        publisher2.send("A")  // Prints: "Combined: 1 - A"

        publisher1.send(2)    // Prints: "Combined: 2 - A"

    }

    

    func zipExample() {

        let publisher1 = PassthroughSubject<Int, Never>()

        let publisher2 = PassthroughSubject<String, Never>()

        

        Publishers.Zip(publisher1, publisher2)

            .sink { number, text in

                print("Zipped: \(number) - \(text)")

            }

            .store(in: &cancellables)

        

        publisher1.send(1)

        publisher2.send("A")  // Prints: "Zipped: 1 - A"

        publisher1.send(2)

        // Waits for publisher2

    }

    

    func mergeExample() {

        let publisher1 = PassthroughSubject<Int, Never>()

        let publisher2 = PassthroughSubject<Int, Never>()

        

        Publishers.Merge(publisher1, publisher2)

            .sink { value in

                print("Merged: \(value)")

            }

            .store(in: &cancellables)

        

        publisher1.send(1)  // Prints: "Merged: 1"

        publisher2.send(2)  // Prints: "Merged: 2"

        publisher1.send(3)  // Prints: "Merged: 3"

    }

}


11. Design Patterns in Swift

Singleton Pattern


Below is a fully detailed, enhanced, crystal-clear explanation of the three major design patterns you shared:

✅ Singleton

✅ Factory / Abstract Factory

✅ Observer


Each explanation includes:

✨ What the pattern is

✨ Why we use it

✨ Where it is useful in iOS development

✨ Problems it solves

✨ How your code works, step-by-step

✨ Real-world examples


11. Design Patterns in Swift — Full Enhanced Explanation1. Singleton Pattern (With Testable Alternative)

 What is the Singleton Pattern?A Singleton ensures that only one instance of a class exists in the entire application and that this instance is globally accessible.


Think of it like a shared manager used everywhere in the app. Why do we use Singleton?Because some systems should have a single point of control, such as:


  • Network manager
  • User session manager
  • App settings manager
  • Database manager
  • Audio managerFor example, you don’t want multiple network managers creating conflicting requests — one consistent shared instance is preferred.


 Your Code Breakdown

Thread-safe singleton

class NetworkManager {

    static let shared = NetworkManager()  // Only one instance


    private init() {

        // Prevent others from creating instances

    }


    func fetchData(completion: @escaping (Data?) -> Void) {

        // Network implementation

    }

}

Usage

NetworkManager.shared.fetchData { data in

    // Handle data

}

Why does this work? static let shared → created once, guaranteed by Swift’s thread safety.

  • private init() → prevents code like NetworkManager() anywhere else. Problem With Singleton: Hard to TestIf your entire app depends on:NetworkManager.sharedThen in unit tests you cannot replace the network layer with a mock.


 Testable Alternative (Dependency Injection)Instead of using the singleton everywhere, define a protocol:protocol NetworkService {

    func fetchData(completion: @escaping (Data?) -> Void)

}

Default implementation

class DefaultNetworkService: NetworkService {

    func fetchData(completion: @escaping (Data?) -> Void) {

        // Real network implementation

    }

}

Service container

class ServiceContainer {

    static let shared = ServiceContainer()

    var networkService: NetworkService = DefaultNetworkService()


    private init() {}

}

Mock service for testing

class MockNetworkService: NetworkService {

    func fetchData(completion: @escaping (Data?) -> Void) {

        completion(nil)

    }

}

Why this is better?Because you can replace:ServiceContainer.shared.networkService = MockNetworkService() Your code becomes testable without modifying app logic.


 Real-World ExampleWhen writing unit tests for login:ServiceContainer.shared.networkService = MockNetworkService()No API calls happen — the mock returns fake data.2. Factory Pattern (and Abstract Factory)

 What is the Factory Pattern?A Factory Pattern creates objects without exposing creation logic to the user.


Instead of:let car = Car()You do:let vehicle = VehicleFactory.createVehicle(type: .car)This improves scalability, decoupling, and readability.


 Your Factory Example Explained

Step 1: Define a product protocol

protocol Vehicle2 {

    var type: String { get }

    func start()

}Every vehicle must have a type and a start() method.Step 2: Create concrete products

class Car2: Vehicle2 { ... }

class Motorcycle: Vehicle2 { ... }

class Truck: Vehicle2 { ... }

Step 3: Factory that creates vehicles

class VehicleFactory {

    enum VehicleType { case car, motorcycle, truck }


    static func createVehicle(type: VehicleType) -> Vehicle2 {

        switch type {

        case .car: return Car2()

        case .motorcycle: return Motorcycle()

        case .truck: return Truck()

        }

    }

}

Usage

let vehicle = VehicleFactory.createVehicle(type: .car)

vehicle.start()

 Why is Factory Pattern Useful? Reduces init() duplication

  • Makes code scalable (easily add new vehicle types)
  • Hides complex object creation logic
  • Used in game development (create new enemies / NPCs)
  • Used in iOS UI frameworks Abstract Factory Pattern ExplanationAn Abstract Factory creates families of related objects.


Example from your code:

You want UI components for iOS and Android.GUIFactory defines the blueprint

protocol GUIFactory {

    func createButton() -> Button

    func createTextField() -> TextField

}

Button and TextField protocolsEach platform will implement them differently.iOS Factory

class iOSFactory: GUIFactory {

    func createButton() -> Button { iOSButton() }

    func createTextField() -> TextField { iOSTextField() }

}

Android Factory

class AndroidFactory: GUIFactory {

    func createButton() -> Button { AndroidButton() }

    func createTextField() -> TextField { AndroidTextField() }

}

 When is Abstract Factory Useful? Multi-platform apps (iOS/Android/macOS)

  • Different themes (Dark/Light Mode)
  • Games (FantasyFactory, SciFiFactory → Weapons, Characters, Maps)

3. Observer Pattern

 What is the Observer Pattern?It allows objects to subscribe to changes in another object.


When subject changes → observers get notified automatically.


This is how:


  • NotificationCenter
  • Combine Publishers
  • KVO
  • SwiftUI @Publishedwork internally.


 Your Observer Code Explained

Observer protocol

protocol Observer: AnyObject {

    func update(value: Int)

}

Subject class

class Subject {

    private var observers: [Observer] = []


    var value: Int = 0 {

        didSet { notifyObservers() }

    }


    func attach(_ observer: Observer) {

        observers.append(observer)

    }


    func detach(_ observer: Observer) {

        observers.removeAll { $0 === observer }

    }


    private func notifyObservers() {

        observers.forEach { $0.update(value: value) }

    }

}

 How it works step-by-step 1 Observers register using .attach()

  1. Subject.value changes
  2. didSet triggers notifyObservers()
  3. All observers receive update(value:)
  4. They update their UI or logicExample Observer

class ConcreteObserver: Observer {

    let name: String

    

    init(name: String) {

        self.name = name

    }


    func update(value: Int) {

        print("\(name) received new value: \(value)")

    }

}

 Real-World Uses Live dashboard updating when data changes

  • Chat app updating messages
  • Stock prices auto-refresh
  • Game UI updating health/score
  • SwiftUI’s data-binding system

  • 1. Testing and Debugging


  • A. Types of Testing in iOS


  • 1. Unit Testing (XCTest)

  • Used to test small pieces of logic like functions or view models.

  • Example:
  • func testLoginSuccess() {
  •     let viewModel = LoginViewModel(service: MockAuthService())
  •     viewModel.login(username: "hiren", password: "1234")
  •     XCTAssertTrue(viewModel.isLoggedIn)
  • }
  • Ensures:

    • Core business logic works correctly
    • Functions return expected outputs
    • Easy to catch bugs early




  • 2. UI Testing

  • Simulates real user interactions using XCUIApplication().

  • Example:
  • func testLoginButtonExists() {
  •     let app = XCUIApplication()
  •     app.launch()
  •    
  •     XCTAssert(app.buttons["LoginButton"].exists)
  • }

  • Checks:
    • UI flow
    • Accessibility
    • Buttons, text fields, labels
  • 3. Snapshot Testing
  • Used to test UI layout visually.
  • Useful for:
    • Dynamic screens
    • Dark/Light mode
    • Device variations
  • Debugging Skills Every iOS Developer Must Know
  • 1. Breakpoints
    • Normal breakpoints
    • Conditional breakpoints
    • Symbolic breakpoints (e.g., viewDidLoad)
  • Example:
  • breakpointCondition: user.isLoggedIn == false
  • 2. Xcode Debug Navigator
  • Used for:
    • Monitoring memory
    • CPU
    • FPS
    • Leaks
  • 3. View Debugging
  • Debug View Hierarchy helps detect:
    • Misaligned constraints
    • Hidden views
    • Layer hierarchy issues
  • 4. Instruments
  • Used for deep debugging:
    • Leaks
    • Allocations
    • Time profiler
    • Network requests
    • Energy usage

  • 2. Performance Optimization

  • Performance optimization is about making your app…
    • Fast
    • Smooth (60+ FPS)
    • Memory efficient
    • Battery friendly
  • Top Areas to Optimize
  • 1. Fix Retain Cycles
  • Using weak self inside closures:
  • network.fetch { [weak self] data in
  •     self?.updateUI()
  • }

  • 2. Optimize AutoLayout
    • Reduce nested views
    • Use UIStackView effectively
    • Pre-calculate heights when possible

  • 3. Use Background Threads
  • Never block the main thread.
  • DispatchQueue.global().async {
  •     // Heavy work
  •     DispatchQueue.main.async {
  •         self.updateUI()
  •     }
  • }


  • 4. Reduce Memory Usage
    • Use weak delegates
    • Clear cache when low memory
    • Use UIImage(named:) carefully
    • Defer expensive decoding
  • 5. Use Instruments
  • Key tools:
    • Time Profiler → slow functions
    • Leaks Tool → memory leaks
    • Energy Log → battery drain

  • 3. Real-World Coding Challenges (iOS-Focused)
  • Here are common types of problems companies ask:
  • A. Algorithm Challenges

  • ✔ Array & string manipulation
  • ✔ HashMap usage
  • ✔ Sorting customization
  • ✔ Two-pointer technique
  • ✔ Dynamic programming (rare, but possible)

  • Example:
  • Find second highest number in an array
  • func secondHighest(_ nums: [Int]) -> Int? {
  •     let unique = Array(Set(nums))
  •     guard unique.count > 1 else { return nil }
  •     return unique.sorted().dropLast().last
  • }

  • B. iOS-Specific Challenges
  • 1. Implement caching logic
    • URLCache
    • NSCache
    • Image caching with placeholders
  • 2. Build a small MVVM module
  • Many interviews ask:
  • Build a screen that lists items from an API using MVVM.
  • 3. Concurrency challenges
  • Write async/await version:
  • func fetchUser() async throws -> User
  • or handle race conditions.
  • C. UI Challenges
  • Examples:
    • Build 3-column grid dynamically
    • Custom collection view layout
    • Infinite scrolling
    • Pull to refresh
  • 4. System Design Questions (iOS Level)
  • System design in iOS focuses on app architecture, scalability, modularity, and best practices.
  • Common System Design Questions
  • 1. Design an Instagram Feed
  • Topics you must cover:
    • Pagination
    • Image caching
    • Offline storage
    • Async loading
    • MVVM architecture
  • 2. Design a Chat Application
  • Cover:
    • WebSockets
    • Message queues
    • Local database (Realm/Core Data)
    • Delivery acknowledgment
    • Typing indicators
  • 3. Design a Video Streaming App
  • Include:
    • Adaptive bitrate streaming
    • Caching strategy
    • Background downloads
    • Offline mode
    • Player optimization
  • Key Concepts Interviewers Look For
  •  Architectural knowledge
    • MVC
    • MVVM
    • VIPER
    • Clean architecture
  •  Modularity
  • How to break features into reusable modules.
  •  Data flow
  • How data moves between layers.
  •  Concurrency
  • GCD / OperationQueue / async-await.
  •  Caching strategy
  • Memory + disk cache.
  •  Networking architecture
    • REST
    • GraphQL
    • Combine/Async sequences
  • 5. Behavioural Interview Preparation

  • Behavioral interviews judge your:
    • Communication
    • Teamwork
    • Leadership
    • Problem-solving
    • Cultural fit
  • Use the STAR Method
  • S → Situation
  • T → Task
  • A → Action
  • R → Result
  • Common Behavioral Questions (With guidance)
  • 1. Tell me about yourself
  • Explain:

    • Background
    • Skills
    • Projects
    • What you want next
  • 2. Tell me about a time you faced a challenge
  • Show problem-solving ability.
  • 3. How do you handle conflicts in a team?
  • Show professionalism & communication.
  • 4. Tell me a time when you improved a system or solved a bug
  • Show technical ownership.
  • 5. Example good answer
  • Q: Tell me about a time you solved a complex bug.
  • A (STAR):

    • S: App was crashing when scrolling product list.
    • T: Find root cause & fix quickly.
    • A: Used Xcode instruments → found memory leak due to image caching. Implemented NSCache + asynchronous decoding.
    • R: Scrolling became smooth, and crash rate dropped to 0%.


Comments

Popular posts from this blog

Complete iOS Developer Guide - Swift 5 by @hiren_syl |  You Must Have To Know 😎 

piano online keyboard

Higher-Order Functions in Swift