Swift Interview Questions and Answers

Find 100+ Swift interview questions and answers to assess candidates’ skills in iOS development, Swift syntax, memory management, app architecture, and best practices.
By
WeCP Team

As organizations build high-quality applications for Apple’s iOS, macOS, watchOS, and tvOS ecosystems, recruiters must identify Swift developers who can write clean, safe, and high-performance code. Swift is the primary language for modern Apple development, combining speed, safety, and expressive syntax.

This resource, "100+ Swift Interview Questions and Answers," is tailored for recruiters to simplify the evaluation process. It covers a wide range of topics—from Swift fundamentals to advanced iOS development concepts, including memory management, concurrency, and app architecture.

Whether you're hiring iOS Developers, Mobile Engineers, or Apple Platform Specialists, this guide enables you to assess a candidate’s:

  • Core Swift Knowledge: Variables, data types, optionals, control flow, functions, structs, classes, protocols, and error handling.
  • Advanced Skills: Protocol-oriented programming, closures, generics, ARC, concurrency (async/await, GCD), and performance optimization.
  • Real-World Proficiency: Building iOS applications using UIKit or SwiftUI, managing app lifecycle, integrating APIs, handling state, and publishing apps to the App Store.

For a streamlined assessment process, consider platforms like WeCP, which allow you to:

  • Create customized Swift assessments tailored to iOS or Apple-platform development roles.
  • Include hands-on tasks such as building UI components, writing business logic, or debugging Swift code.
  • Proctor exams remotely while ensuring integrity.
  • Evaluate results with AI-driven analysis for faster, more accurate decision-making.

Save time, enhance your hiring process, and confidently hire Swift developers who can build secure, performant, and production-ready Apple applications from day one.

Swift Interview Questions

Swift – Beginner (1–40)

  1. What is Swift and why was it created?
  2. What are variables and constants in Swift?
  3. What are data types in Swift?
  4. Explain optional types in Swift.
  5. What is type inference in Swift?
  6. What is the difference between let and var?
  7. What are tuples in Swift?
  8. What is optional binding?
  9. What is forced unwrapping and when should you avoid it?
  10. What is the nil-coalescing operator?
  11. What is the guard statement used for?
  12. What are functions in Swift?
  13. Explain parameters vs arguments.
  14. What are default parameter values?
  15. What is an array in Swift?
  16. What is a dictionary in Swift?
  17. What is a set in Swift?
  18. Explain value types vs reference types.
  19. What are structs in Swift?
  20. What are classes in Swift?
  21. What is an initializer?
  22. What is the difference between designated and convenience initializers?
  23. What is a computed property?
  24. What is a stored property?
  25. What are property observers (willSet/didSet)?
  26. Explain method overloading.
  27. What is enum in Swift?
  28. What are enum associated values?
  29. What are raw values in enums?
  30. What is switch statement and why is it powerful in Swift?
  31. What is optional chaining?
  32. What is the difference between break and continue?
  33. What are loops in Swift?
  34. What are closures in Swift?
  35. What is trailing closure syntax?
  36. What is the difference between == and === in Swift?
  37. What is ARC (Automatic Reference Counting)?
  38. Explain strong, weak, and unowned references.
  39. What is a protocol in Swift?
  40. What is protocol conformance?

Swift – Intermediate (1–40)

  1. Explain extension and its usage.
  2. What is protocol-oriented programming?
  3. What is the difference between struct and class in detail?
  4. What is mutating keyword in Swift structs?
  5. What are lazy properties and when should you use them?
  6. What is typealias?
  7. Explain error handling (try, throw, catch).
  8. What is a do-catch block?
  9. What are higher-order functions (map, filter, reduce)?
  10. What is escaping vs non-escaping closure?
  11. What is autoclosure?
  12. What are generics in Swift?
  13. Explain generic constraints.
  14. What is associated type in protocols?
  15. What is protocol inheritance?
  16. Explain optional protocol requirements.
  17. What is deinitializer?
  18. What is inout parameter?
  19. What is operator overloading?
  20. What are static and class methods?
  21. Explain access control levels (private, fileprivate, internal, public, open).
  22. What is KeyPath in Swift?
  23. What is the difference between map, flatMap, and compactMap?
  24. What is dispatch queue?
  25. Explain synchronous vs asynchronous tasks.
  26. What is GCD (Grand Central Dispatch)?
  27. What is NSOperationQueue and when to use it?
  28. What are property wrappers?
  29. What is @propertyWrapper and when is it helpful?
  30. What is Codable protocol?
  31. What is Decodable vs Encodable?
  32. What is JSONSerialization?
  33. What is Result type in Swift?
  34. Explain defer statement.
  35. What is Singleton pattern and how to implement it?
  36. What is memory leak and how can you avoid it?
  37. What is retain cycle in closures?
  38. What is the use of [weak self] and [unowned self] in closures?
  39. Explain ARC optimization techniques.
  40. What is copy-on-write (COW) in Swift?

Swift – Experienced (1–40)

  1. Explain Swift runtime internals.
  2. Describe how ARC works at compile time vs runtime.
  3. What is Swift ABI stability and why does it matter?
  4. What is the difference between value semantics and reference semantics at scale?
  5. How does Swift manage memory for structs containing reference types?
  6. Explain the Swift Concurrency model.
  7. What are actors in Swift?
  8. Explain async/await in Swift.
  9. What is Task and TaskGroup?
  10. What is Sendable protocol and why is it required?
  11. Explain re-entrancy in actors.
  12. What is the difference between @MainActor and DispatchQueue.main?
  13. Explain how Swift handles thread safety.
  14. What are advanced generic constraints?
  15. Explain opaque types (some keyword).
  16. What is the difference between opaque types and generics?
  17. Explain the use of protocols with associated types (PAT) and challenges.
  18. What is existential type and how is it used?
  19. How does Swift optimize protocol witness tables?
  20. Explain the internal working of ARC optimization (Swift compiler).
  21. What is method dispatch in Swift (dynamic, static, vtable, witness table)?
  22. Explain copy-on-write in depth with examples.
  23. What are advanced memory management tools in Xcode?
  24. How do you detect and fix memory leaks in Swift?
  25. What is Swift Package Manager architecture?
  26. Explain modularization in Swift projects.
  27. What is the difference between frameworks, modules, and packages?
  28. Explain performance optimization in Swift code.
  29. What is the difference between escaping closures and concurrency async closures?
  30. How does Swift handle bridging with Objective-C?
  31. What is @objc and how does dynamic dispatch work?
  32. What are key differences between SwiftUI and UIKit?
  33. Explain SwiftUI’s state management system.
  34. What is @State, @Binding, @ObservedObject, @EnvironmentObject?
  35. Explain diffing in SwiftUI rendering.
  36. Explain view identity in SwiftUI.
  37. What is the role of Combine framework?
  38. What are publishers and subscribers in Combine?
  39. Explain backpressure handling in Combine.
  40. How do you architect a large-scale Swift application (MVVM, VIPER, Clean Architecture)?

Swift Interview Questions and Answers

Beginner (Q&A)

1. What is Swift and why was it created?

Swift is a modern, general-purpose, compiled programming language created by Apple in 2014. It was designed to replace Objective-C for building applications across the Apple ecosystem—iOS, macOS, watchOS, tvOS, and backend services. Swift was built with clear goals: performance, safety, developer productivity, and modern syntax.

The creators of Swift wanted a language that could combine the best of modern programming concepts—type safety, memory safety, functional programming features, automatic memory management—while still offering high performance comparable to C-based languages. Objective-C had several limitations such as verbose syntax, lack of compile-time safety checks, and reliance on dynamic runtime, which could lead to runtime crashes.

Swift introduced:

  • Strong type safety to prevent common coding mistakes.
  • Optionals to eliminate null pointer crashes.
  • Automatic Reference Counting (ARC) for memory management.
  • A clean, expressive, and concise syntax that reduces boilerplate.
  • Interoperability with Objective-C, so existing codebases remain usable.

Overall, Swift was created to provide developers with a safer and more modern language while still delivering high performance for Apple platforms.

2. What are variables and constants in Swift?

In Swift, variables and constants are fundamental ways to store data in memory.

  • A variable (declared with var) is a storage location whose value can change during execution.
  • A constant (declared with let) is immutable—once you assign a value, you cannot modify it again.

Swift strongly encourages using let whenever possible, because immutability leads to safer and more predictable code. Immutable values prevent accidental modifications, reduce bugs, allow for compiler optimizations, and help developers write more secure and thread-safe code.

Example:

let pi = 3.14     // constant
var counter = 0   // variable
counter += 1      // allowed
pi = 3.14159      // error: cannot modify a constant

Using constants wherever feasible is considered a best practice, aligning with Swift’s emphasis on safety and clarity.

3. What are data types in Swift?

Swift is a strongly typed language, meaning every value has a specific data type determined at compile time. This ensures type correctness and reduces runtime errors.

Common built-in Swift data types include:

  • Int — integer numbers (e.g., 10, −5)
  • Double — 64-bit floating-point numbers
  • Float — 32-bit floating-point numbers
  • String — sequences of characters
  • Bool — true or false values
  • Character — a single Unicode character

Swift also includes complex and collection types:

  • Array — ordered collection of values
  • Dictionary — key-value pairs
  • Set — unique unordered values

Swift’s strong type system enables the compiler to catch mistakes early. You can also create your own custom types using structs, classes, enums, and protocols.

4. Explain optional types in Swift.

Optionals are one of Swift’s most powerful features. They represent a variable that may contain a value or may be nil. This eliminates common runtime crashes caused by null values in other languages.

An optional is declared using ?:

var name: String? = "Aman"
var email: String? = nil

Optionals allow Swift to force developers to safely handle missing data. Before using the value inside an optional, you must “unwrap” it—meaning you must safely check whether it contains a value or not.

Optionals improve safety because the compiler requires developers to explicitly handle the case when the value is nil, reducing accidental crashes from null references.

5. What is type inference in Swift?

Type inference is Swift’s ability to automatically determine the type of a variable or constant based on the value you assign.

For example:

let age = 25        // Swift infers age is Int
let pi = 3.14       // Swift infers pi is Double
let message = "Hi"  // Swift infers message is String

Although you can explicitly declare types, Swift’s type inference reduces boilerplate and makes code cleaner while still maintaining strong static typing.

Type inference works at compile time and does not reduce type safety. You get a strong, safe type system without needing to explicitly annotate every type.

6. What is the difference between let and var?

In Swift:

  • let defines a constant (immutable).
  • var defines a variable (mutable).

Key differences:

let (Constant)var (Variable)Value cannot be changed after assignmentValue can be changed anytimeEnsures code safetyAllows flexibilityHelps avoid bugs from accidental modificationShould be used only when necessaryCompiler can optimize performanceLess optimized

Example:

let username = "John"
var score = 0
score = 10        // works
username = "Bob"  // error

Using let is a best practice unless mutability is required. It supports Swift’s core philosophy of safety and predictability.

7. What are tuples in Swift?

A tuple is a lightweight way to group multiple values into a single compound value. Tuples are useful when you want to return several values from a function or temporarily bundle data without creating a struct or class.

Example of a tuple:

let person = ("Aman", 25, true)

Tuples can also have named elements:

let person = (name: "Aman", age: 25, isActive: true)
print(person.name) // Aman

Tuples are fixed in size and type—once created, you cannot add or remove elements. They are ideal for simple grouped data but not recommended for complex models.

8. What is optional binding?

Optional binding is a safe way to unwrap an optional by checking whether it contains a value. It avoids crashes caused by trying to use nil.

Using if let:

var name: String? = "Aman"

if let actualName = name {
    print("Name is \(actualName)")
} else {
    print("Name is nil")
}

Using guard let:

func greet(_ name: String?) {
    guard let actualName = name else {
        print("No name provided")
        return
    }
    print("Hello, \(actualName)")
}

Optional binding ensures safe and controlled access to optional values, allowing code to handle missing data gracefully.

9. What is forced unwrapping and when should you avoid it?

Forced unwrapping is done using the ! operator to directly access the value inside an optional.

Example:

let number: Int? = 5
let result = number!  // forced unwrapping

However, forced unwrapping is dangerous because if the optional is nil, the program will crash at runtime.

You should avoid forced unwrapping unless you are absolutely certain the optional contains a value. Safe alternatives include:

  • optional binding (if let, guard let)
  • nil-coalescing operator (??)
  • optional chaining (?.)

Forced unwrapping is mainly used for scenarios like outlets in UIKit where the value is guaranteed to be set before use.

10. What is the nil-coalescing operator?

The nil-coalescing operator (??) provides a default value when an optional is nil.

Example:

let username: String? = nil
let displayName = username ?? "Guest"
print(displayName) // Guest

It works like this:

  • If the optional contains a value → return the unwrapped value.
  • If it is nil → return the default value.

It is cleaner and more concise than writing a full conditional check.

Example with real-world use:

let savedAge = userDefaults.integer(forKey: "age") ?? 0

The nil-coalescing operator is one of Swift’s most common tools for handling missing or optional values safely.

11. What is the guard statement used for?

The guard statement in Swift is used to ensure that certain conditions are met before executing the rest of a block of code. It provides an early exit from a function, loop, or scope if the condition fails. This makes code cleaner, more readable, and less deeply nested.

A guard statement must include:

  1. A condition that must be true to continue.
  2. An else block that exits the current scope.

Example:

func printName(_ name: String?) {
    guard let actualName = name else {
        print("Name is missing")
        return
    }
    print("Name: \(actualName)")
}

Why guard is useful:

  • It prevents pyramid of doom caused by nested if statements.
  • Makes success-path logic more linear.
  • Improves readability in functions requiring mandatory values (like parameters or optional properties).

guard is most commonly used for:

  • Optional unwrapping
  • Early validation
  • Ensuring preconditions are met

It is considered a best practice in Swift for writing safe, clean, and maintainable code.

12. What are functions in Swift?

Functions in Swift are reusable blocks of code designed to perform a specific task. They can take input values (parameters), execute logic, and optionally return a result.

Basic function example:

func greet() {
    print("Hello!")
}

Functions can:

  • Accept parameters
  • Return values
  • Have multiple return types via tuples
  • Use default parameters
  • Support external and internal parameter names
  • Throw errors
  • Be nested inside other functions
  • Be passed around as a value (higher-order functions)

Example with parameters and return value:

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

Functions are a fundamental building block in Swift, supporting both procedural and functional programming styles.

13. Explain parameters vs arguments.

In Swift, the terms parameters and arguments are related but distinct:

Parameters

  • Defined in the function declaration.
  • They act as placeholders that specify what kind of input the function accepts.

Example:

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

Here, a and b are parameters.

Arguments

  • Actual values passed to the function when it is called.

Example:

add(a: 5, b: 10)

Here, 5 and 10 are arguments.

In summary:

TermWhere it appearsMeaningParameterFunction definitionPlaceholder name/type for inputsArgumentFunction callActual value provided

Understanding this distinction is important because Swift supports both internal parameter names and external argument labels, enhancing clarity.

14. What are default parameter values?

Default parameter values allow a function to automatically use a predefined value when an argument is not supplied during the function call.

Example:

func greet(name: String = "Guest") {
    print("Hello, \(name)")
}

greet()            // Hello, Guest
greet(name: "Aman") // Hello, Aman

Benefits of default parameters:

  • Reduce function overloading
  • Improve readability and convenience
  • Allow flexibility for caller code
  • Simplify APIs by reducing the number of required arguments

They are especially useful in functions where optional customization is common.

15. What is an array in Swift?

An array in Swift is an ordered collection that stores multiple values of the same type. Arrays maintain the order in which elements are inserted.

Example:

var numbers: [Int] = [1, 2, 3, 4]

Key features:

  • Arrays are zero-indexed.
  • They support dynamic resizing.
  • Values must be of the same type.
  • Operations include append, remove, insert, sort, etc.

Common operations:

numbers.append(5)
let first = numbers[0]
numbers.remove(at: 2)

Arrays in Swift are value types, meaning they get copied when assigned or passed, but Swift uses copy-on-write to optimize performance.

16. What is a dictionary in Swift?

A dictionary in Swift stores key-value pairs, where each key must be unique, and each key maps to a value.

Example:

var user: [String: String] = [
    "name": "Aman",
    "city": "Delhi"
]

Key Characteristics:

  • Keys must be hashable.
  • Values can be of any type.
  • You access values by referring to their keys.

Example usage:

user["name"] = "Rahul"
let city = user["city"]  // returns Optional("Delhi")

Dictionaries are ideal for fast lookups, storing attributes, and mapping identifiers to values.

17. What is a set in Swift?

A set in Swift is an unordered collection of unique values. It is similar to mathematical sets and is useful when uniqueness matters and order does not.

Example:

var colors: Set<String> = ["Red", "Blue", "Green"]

Key features:

  • No duplicates allowed.
  • Very fast lookup due to hashing.
  • Unordered collection.
  • Good for checking membership efficiently.

Example operations:

colors.insert("Yellow")
colors.contains("Blue")
colors.remove("Green")

Sets also support mathematical operations:

  • union
  • intersection
  • symmetricDifference
  • subtracting

Sets are excellent for tasks requiring fast search and uniqueness guarantees.

18. Explain value types vs reference types.

In Swift, all types belong to one of two categories: value types or reference types.

Value Types

Examples: Int, Double, Bool, String, Array, Dictionary, Set, Struct, Enum

  • Stored directly.
  • When assigned or passed to a function, a copy is made.
  • Each instance maintains its own data.
  • Safer for concurrent programming because no shared state exists.

Example:

var a = 10
var b = a
b = 20
// a still equals 10

Reference Types

Examples: Class, Function references, Actor, NSObjects

  • Stored in heap memory.
  • Multiple references can point to the same instance.
  • Changing one reference affects all others.
  • Uses ARC (Automatic Reference Counting) for memory management.

Example:

class Person { var name = "Aman" }

var p1 = Person()
var p2 = p1
p2.name = "Rahul"

// p1.name is also "Rahul" because both refer to the same object

Key takeaway:
Value types provide safety and predictability; reference types provide shared, modifiable state when needed.

19. What are structs in Swift?

A struct (structure) in Swift is a value type used to combine related data and functionality. Structs are lightweight, efficient, and preferred in Swift due to value semantics.

Example:

struct Person {
    var name: String
    var age: Int
    
    func greet() {
        print("Hello, my name is \(name)")
    }
}

Characteristics:

  • Stored on the stack (or optimized by Swift).
  • Copied on assignment due to value semantics.
  • Can have:
    • properties
    • methods
    • initializers
    • extensions
    • protocols
  • Cannot inherit from another struct.

Swift encourages using structs by default because they:

  • Are safer due to immutability.
  • Avoid unintended side effects.
  • Perform very efficiently with copy-on-write optimization.

20. What are classes in Swift?

A class in Swift is a reference type used to create objects that share behavior and state across multiple references.

Example:

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

Key features of classes:

  • Reference semantics → multiple variables can reference the same instance.
  • Inheritance → one class can inherit from another.
  • Deinitializers → clean-up code when an instance is deallocated.
  • ARC-based memory management.
  • Identity operators (=== and !==) to compare instance references.

Classes are ideal when:

  • You need shared state.
  • Inheritance is required.
  • You’re modeling complex objects with mutable behavior.

However, Swift encourages using structs first unless class-specific features are necessary.

21. What is an initializer?

An initializer in Swift is a special method used to set up an instance of a class, struct, or enum. It prepares the object for use by assigning initial values to its stored properties and performing any necessary setup work.

Example:

struct Person {
    var name: String
    var age: Int

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

let p = Person(name: "Aman", age: 25)

Key points:

  • Every stored property must be initialized before an instance is used.
  • Structs automatically receive a memberwise initializer unless you define your own.
  • Classes do not get an automatic memberwise initializer.
  • Initializers do not return a value; they simply prepare an instance.

Initializers ensure that every object begins life in a valid, predictable state, which is a core part of Swift’s safety model.

22. What is the difference between designated and convenience initializers?

Swift classes support two types of initializers:

Designated Initializers

  • Primary initializers of a class.
  • They fully initialize all properties introduced by that class.
  • Must call a designated initializer of the superclass (if any).
  • Every class must have at least one designated initializer (unless inherited).

Example:

class Person {
    var name: String
    var age: Int

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

Convenience Initializers

  • Secondary, helper initializers.
  • Must call another initializer from the same class (not superclass).
  • Provide shortcuts, default values, or commonly used setups.

Example:

class Person {
    var name: String
    var age: Int

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

    convenience init(name: String) {  // convenience
        self.init(name: name, age: 18)
    }
}

Summary Table

FeatureDesignatedConveniencePrimary initializerYesNoCallsSuperclass or another designatedAnother initializer of same classResponsibilityFully initialize all propertiesProvide shortcuts or defaults

This initializer system ensures proper initialization order, especially in inheritance chains.

23. What is a computed property?

A computed property does not store a value directly. Instead, it computes its value each time it is accessed. It can have a getter, a setter, or both.

Example:

struct Rectangle {
    var width: Double
    var height: Double

    var area: Double {  // computed property
        return width * height
    }
}

Setter example:

var side: Double = 0

var area: Double {
    get { return side * side }
    set { side = sqrt(newValue) }
}

Computed properties allow you to:

  • Derive values dynamically
  • Encapsulate logic instead of storing redundant data
  • Maintain clean and maintainable code

They do not take up stored memory unless they depend on stored properties.

24. What is a stored property?

A stored property is a variable or constant that actually stores data within a class or struct instance.

Example:

struct Person {
    var name: String      // stored property
    var age: Int          // stored property
}

Key features:

  • Can be variable (var) or constant (let)
  • Constants in structs must be initialized at creation time
  • Classes allow stored properties with default values or defined in initializers
  • Unlike computed properties, stored properties occupy memory

Stored properties represent the real state of your data model.

25. What are property observers (willSet/didSet)?

Property observers let you respond to changes in a stored property’s value. They trigger whenever the property’s value changes, regardless of whether the new value is different.

There are two observers:

  1. willSet — called before the value changes
  2. didSet — called after the value changes

Example:

var score: Int = 0 {
    willSet {
        print("Score is about to change to \(newValue)")
    }
    didSet {
        print("Score changed from \(oldValue) to \(score)")
    }
}

Use cases:

  • Logging changes
  • Validating data
  • Synchronizing dependent values
  • Triggering UI updates
  • Enforcing limits

Property observers do not apply to computed properties because they already have logic built-in.

26. Explain method overloading.

Method overloading is the ability to define multiple methods with the same name but different:

  • parameter types
  • number of parameters
  • argument labels

Example:

func greet() {
    print("Hello!")
}

func greet(name: String) {
    print("Hello, \(name)!")
}

func greet(name: String, age: Int) {
    print("Hello, \(name). You are \(age) years old.")
}

Swift decides which version to call based on the function signature.

Benefits:

  • Improves code readability
  • Keeps related functionality grouped under one name
  • Makes APIs more expressive and easier to use

Overloading does NOT depend on return type alone; parameters must differ.

27. What is enum in Swift?

An enum (enumeration) defines a group of related values in a type-safe way. Enums let you model fixed sets of options, states, or choices.

Example:

enum Direction {
    case north, south, east, west
}

Swift enums are especially powerful because they support:

  • Raw values
  • Associated values
  • Methods
  • Computed properties
  • Conformance to protocols

Enums allow you to express intent clearly and safely in your code.

28. What are enum associated values?

Associated values let an enum case store additional information of any type. This makes Swift enums significantly more expressive than enums in many other languages.

Example:

enum LoginStatus {
    case success(userID: Int)
    case failure(message: String)
}

Using the enum:

let status = LoginStatus.success(userID: 101)

Benefits:

  • Carry additional context with cases
  • Replace complex data structures
  • Improve type safety by encoding state transitions

Associated values behave like parameters attached to individual enum cases.

29. What are raw values in enums?

Raw values are predefined values attached to enum cases, all of the same type. They are normally used when the cases need an underlying, consistent primitive representation.

Example:

enum Weekday: Int {
    case monday = 1
    case tuesday
    case wednesday
}

Example with strings:

enum Direction: String {
    case north = "N"
    case south = "S"
}

Key features:

  • Raw values must be of the same type (Int, String, Double, etc.)
  • They are fixed, unlike associated values
  • Enums with raw values get automatic initializers:

let day = Weekday(rawValue: 1) // monday

Raw values are best used when mapping enums to external systems such as database codes, JSON keys, or UI labels.

30. What is switch statement and why is it powerful in Swift?

The switch statement in Swift is more powerful than in most languages because:

  • It supports any type, not just integers.
  • It requires exhaustive case coverage, increasing safety.
  • It supports pattern matching: ranges, tuples, enums, conditions, and more.
  • It does not require a break after every case unless specified.
  • It allows multiple patterns in one case.
  • Works extremely well with Swift enums.

Example:

let score = 85

switch score {
case 0:
    print("Zero")
case 1..<50:
    print("Below average")
case 50..<80:
    print("Average")
case 80...100:
    print("Excellent")
default:
    print("Invalid score")
}

Pattern matching example:

let point = (2, 3)

switch point {
case (0, 0):
    print("Origin")
case (let x, 0):
    print("X-axis at \(x)")
case (0, let y):
    print("Y-axis at \(y)")
default:
    print("Somewhere else")
}

Why it's powerful:

  • Expressive and readable
  • Eliminates many nested if statements
  • Works perfectly with enums and associated values
  • Safer because of exhaustive checking

Swift’s switch is one of its strongest control-flow features.

31. What is optional chaining?

Optional chaining is a safe way to access properties, methods, or subscripts on an optional that may be nil. Instead of crashing when an optional is nil, optional chaining simply returns nil and continues execution gracefully.

Example:

var person: Person?
let city = person?.address?.city

Here:

  • If person is nil → expression returns nil
  • If address is nil → expression returns nil
  • If all values exist → return the city

Optional chaining allows you to traverse complex nested structures without writing multiple if let or guard let statements.

Benefits:

  • Safer and cleaner optional access
  • Reduces nested unwrapping
  • Works with method calls, properties, subscripts, and optional functions

Example with method:

person?.greet()

If person is nil, the method will not be called.

Optional chaining is a key component of Swift’s safety model, helping avoid null-pointer crashes.

32. What is the difference between break and continue?

Both break and continue are control-flow statements used inside loops, but they behave differently.

break

  • Immediately exits the entire loop.
  • Execution moves to the code after the loop.

Example:

for i in 1...5 {
    if i == 3 {
        break
    }
    print(i)
}

Output: 1 2
The loop stops when i == 3.

continue

  • Skips the current iteration.
  • Moves to the next iteration of the loop.

Example:

for i in 1...5 {
    if i == 3 {
        continue
    }
    print(i)
}

Output: 1 2 4 5

Summary Table

StatementBehaviorbreakExits loop completelycontinueSkips current iteration only

These statements improve control over loop execution flow, making logic more flexible and efficient.

33. What are loops in Swift?

Loops allow you to repeat a block of code multiple times. Swift provides three primary loop types:

1. for-in loop

Used to iterate over a range, array, dictionary, set, or sequence.

for i in 1...5 {
    print(i)
}

2. while loop

Repeats as long as a condition is true.

var count = 0
while count < 5 {
    print(count)
    count += 1
}

3. repeat-while loop

Similar to a while loop, but guaranteed to run at least once.

var index = 0
repeat {
    print(index)
    index += 1
} while index < 5

Swift loops are powerful and flexible, commonly used for:

  • Iterating collections
  • Repeating operations
  • Searching or filtering data
  • Performing animations
  • Implementing algorithms

34. What are closures in Swift?

A closure is a self-contained block of code that can be passed around and executed later. Closures in Swift are similar to lambdas or anonymous functions in other languages.

Example:

let greet = {
    print("Hello!")
}
greet()

Closures can:

  • Capture variables from their surrounding context
  • Be stored in variables
  • Be passed to functions
  • Return values
  • Support type inference, making syntax concise

Example with parameters and return value:

let add = { (a: Int, b: Int) -> Int in
    return a + b
}

Swift heavily uses closures in:

  • Completion handlers
  • Functional programming (map, filter, reduce)
  • Animation blocks
  • Networking callbacks

Closures are a core part of Swift’s functional programming capabilities.

35. What is trailing closure syntax?

Trailing closure syntax allows you to write cleaner and more readable code when the last parameter of a function is a closure.

Without trailing closure:

performTask(completion: { result in
    print(result)
})

With trailing closure:

performTask { result in
    print(result)
}

If a function has multiple closure parameters, only the last one can use trailing syntax.

Trailing closures improve readability, especially in nested closures like animations and networking.

Example with multiple closures:

fetchData(success: { data in
    print(data)
}) { error in
    print(error)
}

Trailing closures make Swift code expressive and concise.

36. What is the difference between == and === in Swift?

Swift uses two different equality operators depending on value type or reference type.

== (Equality operator)

  • Used for value comparison
  • Checks if two values are structurally equal

Example:

5 == 5        // true
"abc" == "abc" // true

For classes, you can overload == to define custom equality.

=== (Identity operator)

  • Used for reference comparison
  • Checks if two variables refer to the exact same object in memory
  • Works only with classes (reference types)

Example:

class Person {}
let p1 = Person()
let p2 = p1
let p3 = Person()

p1 === p2  // true (same instance)
p1 === p3  // false (different instances)

Summary Table

OperatorMeaningWorks On==Value equalityValue types + classes===Reference identityClasses only

This distinction is essential to understanding how reference types behave in Swift.

37. What is ARC (Automatic Reference Counting)?

ARC is Swift's memory management system that automatically handles allocation and deallocation of memory for class instances.

ARC tracks how many references point to each object:

  • When a new reference is created → count increases
  • When a reference is removed → count decreases
  • When count becomes zero → memory is freed

Example:

class Person {}
var p1: Person? = Person()  // reference count = 1
p1 = nil                    // reference count = 0 → object deallocated

ARC applies only to reference types (classes).
Value types (structs, enums) manage memory differently because they are copied.

ARC helps avoid memory leaks but can create retain cycles if not managed properly.

38. Explain strong, weak, and unowned references.

These reference types help ARC manage memory and prevent retain cycles.

1. Strong Reference (default)

A strong reference increases an object's reference count.

var person: Person? = Person()

Use strong references for normal ownership.

2. Weak Reference

  • Does not increase reference count
  • Must always be an optional
  • Automatically becomes nil when object deallocates

Used for preventing retain cycles, typically in parent-child relationships.

Example:

weak var delegate: SomeDelegate?

3. Unowned Reference

  • Does not increase reference count
  • Assumes the referenced object will never be nil once set
  • Crashes if accessed after deallocation

Used when the lifetime of both objects is tightly connected.

Example:

unowned var owner: Person

Summary Table

Reference TypeIncreases ARC Count?Can be nil?Safe?strongYesNoYesweakNoYesVery safeunownedNoNoUnsafe if object deallocates

Understanding these references is critical for preventing memory leaks in Swift.

39. What is a protocol in Swift?

A protocol defines a blueprint of methods, properties, and requirements that a type must implement. It is similar to an interface in other languages.

Example:

protocol Vehicle {
    var speed: Int { get set }
    func accelerate()
}

Structs, classes, and enums can all adopt protocols.

Protocols allow:

  • Abstraction
  • Code reuse
  • Loose coupling
  • Polymorphism
  • Testability

Example of adoption:

struct Car: Vehicle {
    var speed: Int = 0

    func accelerate() {
        print("Accelerating...")
    }
}

Protocols are foundational to protocol-oriented programming (POP), a core Swift paradigm.

40. What is protocol conformance?

Protocol conformance means that a type (struct, class, or enum) satisfies all the requirements declared in a protocol.

Example:

protocol Greetable {
    func greet()
}

struct Person: Greetable {  // conformance
    func greet() {
        print("Hello!")
    }
}

Swift checks conformance at compile time:

  • All required methods must be implemented
  • All required properties must be present
  • Correct signatures must be used

Conformance allows:

  • Types to behave differently but share a common interface
  • Generic programming
  • Dependency injection
  • Composition over inheritance

Protocol conformance is a key building block of Swift’s flexible and powerful type system.

Intermediate (Q&A)

1. Explain extension and its usage.

An extension in Swift allows you to add new functionality to an existing type without modifying its original source code. You can extend:

  • Structs
  • Classes
  • Enums
  • Protocols

Extensions can add:

  • Methods
  • Computed properties
  • Initializers (with restrictions)
  • Subscripts
  • Nested types
  • Protocol conformance

Example:

extension String {
    var reversedString: String {
        return String(self.reversed())
    }
}

Usage:

"Swift".reversedString // "tfiwS"

Why use extensions?

  1. Organizing code — Split logic into meaningful groups.
  2. Adding functionality — Add new methods to system or third-party types.
  3. Protocol conformance — Adopt a protocol through an extension.
  4. Separation of concerns — Keep core logic and custom logic independent.

Extensions embody Swift’s design philosophy of composition over inheritance. They allow modular, cleaner, reusable code without subclassing.

2. What is protocol-oriented programming?

Protocol-Oriented Programming (POP) is a Swift paradigm where protocols play a central role in defining behavior. Instead of relying heavily on class inheritance, Swift encourages using protocols with extensions to share behaviors.

Core concepts:

  • Protocols define behavior, not implementation.
  • Extensions provide default implementations for protocol requirements.
  • Types (structs, classes, enums) adopt protocols to gain behavior.
  • Value types (structs) are emphasized over reference types (classes).

Example:

protocol Vehicle {
    func start()
}

extension Vehicle {
    func start() {
        print("Starting the vehicle…")
    }
}

struct Car: Vehicle {}

Usage:

Car().start() // "Starting the vehicle…"

Why POP is powerful

  • Reduces reliance on inheritance
  • Promotes flexible composition
  • Avoids common OOP issues like deep class hierarchies
  • Supports multiple behavior adoption via protocol conformance
  • Enhances code reuse
  • Makes Swift safer and more predictable

POP is considered the “Swift way” to build clean, modern architectures.

3. What is the difference between struct and class in detail?

Structs and classes both define custom data types, but they differ in important ways.

1. Value Type vs Reference Type

  • Structs → Value types (copied on assignment)
  • Classes → Reference types (shared memory)

Example:

var a = MyStruct()
var b = a // copy

var a = MyClass()
var b = a // reference to same object

2. Memory Management

  • Structs → Stack memory, automatically managed
  • Classes → Heap memory with ARC (reference counting)

3. Inheritance

  • Structs → No inheritance
  • Classes → Support inheritance and method overriding

4. Mutability

  • Structs require mutating keyword to modify properties
  • Classes can always modify properties (if variable)

5. Deinitializers

  • Only classes support deinit

6. When to use which?

Use struct when:

  • Data is simple
  • You want predictable value semantics
  • No need for inheritance
  • High safety and performance are desired

Use class when:

  • Shared mutable state is needed
  • You need inheritance
  • ARC behavior is required

Swift encourages preferring structs unless class-specific behavior is necessary.

4. What is mutating keyword in Swift structs?

Because structs are value types, their instances are immutable by default—even when declared with var. To modify properties inside a struct method, you must mark the method as mutating.

Example:

struct Counter {
    var value: Int = 0

    mutating func increment() {
        value += 1
    }
}

Why mutating is required:

  • Struct methods operate on a copy of the instance.
  • Mutating methods replace the old instance with a new modified one.
  • Ensures safety and clarity around value semantics.

Classes do not require mutating because they are reference types.

5. What are lazy properties and when should you use them?

A lazy property is not initialized until the first time it is accessed. Use the lazy keyword.

Example:

class DataLoader {
    lazy var data = loadData()

    func loadData() -> [String] {
        print("Loading data...")
        return ["A", "B", "C"]
    }
}

Benefits:

  • Improves performance — expensive operations run only when needed.
  • Reduces memory use — no unnecessary initialization.
  • Useful for:
    • Heavy computations
    • Complex objects
    • Objects requiring external dependencies
    • Values not always used

Lazy properties must be var because their value is assigned after initialization.

6. What is typealias?

typealias creates a shorthand or alias for an existing type. It does not create a new type, only a convenient name.

Example:

typealias CompletionHandler = (Bool) -> Void

Usage:

func loadData(completion: CompletionHandler) { }

Benefits:

  • Improves readability
  • Simplifies long type definitions
  • Makes closure signatures clean
  • Helps reduce duplication

Typealias is widely used for:

  • Closures
  • Protocol composition
  • Complex generics

7. Explain error handling (try, throw, catch).

Swift uses a robust error-handling model based on throwing errors from functions and catching them where needed.

1. Defining an Error

Errors must conform to the Error protocol:

enum LoginError: Error {
    case invalidCredentials
}

2. Throwing an Error

func login(user: String, pass: String) throws {
    throw LoginError.invalidCredentials
}

3. Handling Errors with try/catch

do {
    try login(user: "Aman", pass: "123")
} catch {
    print("Login failed: \(error)")
}

Types of try

  • try → normal error propagation
  • try? → converts error into optional
  • try! → crash if error occurs (unsafe)

Error handling is powerful for:

  • Networking
  • File operations
  • Database operations
  • Authentication

It ensures clean separation between normal logic and error-handling logic.

8. What is a do-catch block?

A do-catch block is Swift’s mechanism to handle thrown errors safely.

Structure:

do {
    try someFunction()
    print("Success")
} catch SomeError.case1 {
    print("Case 1 occurred")
} catch {
    print("Unknown error: \(error)")
}

Key points:

  • The do block contains code that may throw errors.
  • Each catch block can handle specific error types.
  • The final catch catches all unhandled errors.

Benefits:

  • Prevents crashes
  • Separates error-handling from core logic
  • Supports multiple error cases
  • Often used in networking, parsing, and database tasks

9. What are higher-order functions (map, filter, reduce)?

Higher-order functions are functions that take other functions as parameters or return functions. Swift collections provide built-in higher-order functions.

map

Transforms each element and returns a new array.

let numbers = [1, 2, 3]
let squares = numbers.map { $0 * 2 } // [2, 4, 6]

filter

Filters elements based on a condition.

let even = numbers.filter { $0 % 2 == 0 } // [2]

reduce

Combines all elements into a single value.

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

Why higher-order functions are important?

  • Promote functional programming
  • Reduce boilerplate loops
  • Improve code readability and expressiveness
  • Enable powerful data transformations

Swift’s standard library is heavily optimized for these functions.

10. What is escaping vs non-escaping closure?

Closures passed to a function can be:

Non-escaping Closures

(Default behavior)

  • Executed within the function body.
  • Guaranteed not to outlive the function.
  • Cannot be stored for later use.

Example:

func perform(action: () -> Void) {
    action() // must run inside function
}

Escaping Closures

Marked with @escaping

  • May execute after the function returns.
  • Can be stored in variables or executed asynchronously.
  • Required in callbacks or async tasks.

Example:

func getData(completion: @escaping () -> Void) {
    DispatchQueue.global().async {
        completion()
    }
}

Why escaping closures matter

Closures that escape must capture self explicitly (e.g., [weak self]) to avoid strong reference cycles.

Escaping closures are used in:

  • Network requests
  • Animation blocks
  • Completion handlers
  • Async operations

Non-escaping closures are more efficient and safe but limited to synchronous execution.

11. What is autoclosure?

An autoclosure in Swift is a special type of closure that automatically wraps an expression inside a closure without requiring explicit closure syntax. It allows you to pass an expression that will be evaluated only when the closure is executed, enabling lazy evaluation.

You declare an autoclosure with the @autoclosure attribute.

Example:

func logMessage(_ message: @autoclosure () -> String) {
    print("LOG:", message())
}

logMessage("Process started")

Although "Process started" is a simple string, Swift automatically converts it into:

{ return "Process started" }

Why autoclosures are useful?

  • Provide cleaner and more readable code
  • Allow deferred execution of expressions
  • Useful in assert, fatalError, and logical operators like || and &&

Example showing deferred evaluation:

func lazyCheck(_ condition: @autoclosure () -> Bool) {
    print("Condition not evaluated yet")
    if condition() {
        print("Condition is true")
    }
}

Warning:

Autoclosures can hide performance-heavy expressions, so use them carefully.

12. What are generics in Swift?

Generics allow you to write flexible, reusable code that can work with any type while maintaining strong type safety. Instead of writing duplicate code for multiple data types, you write a single generic function, class, or struct.

Example:

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

Here, T is a placeholder for any type.

Generics can be used in:

  • Functions
  • Classes
  • Structs
  • Enums
  • Protocols

Example generic struct:

struct Stack<T> {
    var items: [T] = []
    
    mutating func push(_ item: T) { items.append(item) }
    mutating func pop() -> T { items.removeLast() }
}

Benefits:

  • Code reuse
  • Type safety
  • Performance (no runtime type casting)
  • Cleaner API design

Generics are a cornerstone of Swift’s powerful type system.

13. Explain generic constraints.

Generic constraints allow you to restrict what kinds of types can be used with a generic function or type. This ensures that the generic type can only be substituted with types that meet specific requirements.

There are two main types:

1. Protocol Constraints

func compare<T: Comparable>(_ a: T, _ b: T) -> Bool {
    return a > b
}

Here, T must conform to Comparable.

2. Class Constraints

func process<T: UIView>(_ view: T) {
    // Only UIView subclasses allowed
}

Where constraints (advanced)

func findIndex<T>(of value: T, in array: [T]) -> Int?
where T: Equatable {
    return array.firstIndex(of: value)
}

Benefits:

  • Enhances type safety
  • Prevents misuse of generic code
  • Ensures required methods/properties exist
  • Enables more powerful generic designs

Generic constraints make Swift’s generics extremely expressive and robust.

14. What is associated type in protocols?

When defining a protocol, you sometimes want to define a placeholder type that conforming types must specify. This placeholder type is called an associated type.

Example:

protocol Container {
    associatedtype Item
    func add(_ item: Item)
    func getAll() -> [Item]
}

A conforming struct chooses what Item should be:

struct IntContainer: Container {
    typealias Item = Int
    private var items: [Int] = []

    func add(_ item: Int) { }
    func getAll() -> [Int] { return items }
}

Why associated types are powerful?

  • Enable generic protocols
  • Allow protocols to describe concepts rather than concrete types
  • Allow flexibility in conforming types

They are fundamental to protocol-oriented programming and Swift standard library (e.g., Sequence, Collection).

15. What is protocol inheritance?

Protocol inheritance allows one protocol to inherit the requirements of another protocol. A child protocol must satisfy its own requirements plus those of its parent.

Example:

protocol Vehicle {
    func start()
}

protocol Car: Vehicle {
    func openDoor()
}

Now any type conforming to Car must:

  • Implement start() (inherited)
  • Implement openDoor() (own requirement)

Benefits:

  • Helps build hierarchical, reusable designs
  • Encourages clean and modular architecture
  • Allows grouping related functionality

Protocols can inherit from multiple protocols, unlike classes.

Example:

protocol SmartDevice: Camera, GPS, InternetEnabled { }

This improves type composition without deep inheritance chains.

16. Explain optional protocol requirements.

Optional protocol requirements allow methods or properties in a protocol to be optional for conforming types. This is only available for @objc protocols, meaning:

  • The protocol must be marked with @objc
  • Only classes can adopt the protocol (no structs or enums)

Example:

@objc protocol PrinterDelegate {
    @objc optional func didStartPrinting()
    @objc optional func didFinishPrinting()
}

Conforming class:

class Printer: PrinterDelegate {
    func didStartPrinting() {
        print("Started printing")
    }
    // didFinishPrinting not required
}

Why optional requirements?

  • Useful for delegate patterns (UIKit heavily uses them)
  • Allow flexible adoption
  • Reduce boilerplate

Swift prefers protocol extensions over optional requirements for default behavior, but optional requirements remain useful for Objective-C interoperability.

17. What is deinitializer?

A deinitializer (deinit) is a special method in Swift that runs automatically when a class instance is about to be deallocated from memory.

Example:

class FileHandler {
    init() {
        print("File opened")
    }
    
    deinit {
        print("File closed")
    }
}

Deinitializers:

  • Only available in classes (not structs or enums)
  • Run exactly once per instance
  • Cannot take parameters
  • Cannot be called manually
  • Used for cleanup tasks

Use cases:

  • Closing files
  • Releasing resources
  • Invalidating timers
  • Removing observers
  • Handling custom memory deallocation

ARC automatically triggers the deinitializer when reference count reaches zero.

18. What is an inout parameter?

An inout parameter allows a function to modify the passed argument directly. It essentially passes variables by reference instead of by value.

Syntax:

func increment(_ value: inout Int) {
    value += 1
}

Usage:

var count = 10
increment(&count)
print(count) // 11

Rules:

  • The passed argument must be a variable (var)
  • You must use & to indicate it's being passed by reference
  • Not allowed with constants, literals, or computed properties

Use cases:

  • Swapping values
  • Modifying arguments directly
  • Improving performance by avoiding copying large structs

19. What is operator overloading?

Operator overloading allows you to redefine how operators such as +, -, *, ==, etc., behave for custom types.

Example:

struct Vector {
    var x: Int
    var y: Int
}

func + (lhs: Vector, rhs: Vector) -> Vector {
    return Vector(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}

Usage:

let v1 = Vector(x: 1, y: 2)
let v2 = Vector(x: 3, y: 4)
let v3 = v1 + v2  // Vector(x: 4, y: 6)

Benefits:

  • Makes custom types feel natural during operations
  • Reduces verbosity
  • Abstracts complex logic behind familiar symbols

Swift allows overloading most operators to support mathematical and logical operations for user-defined types.

20. What are static and class methods?

Swift allows methods associated with the type itself rather than any particular instance. These are called static or class methods.

static methods

Defined using static keyword.
They cannot be overridden in subclasses.

Example:

struct Math {
    static func square(_ x: Int) -> Int {
        return x * x
    }
}

Usage:

Math.square(4) // 16

class methods

Defined using class keyword.
They can be overridden by subclasses.

Example:

class Animal {
    class func species() -> String {
        return "Unknown"
    }
}

class Dog: Animal {
    override class func species() -> String {
        return "Dog"
    }
}

Usage:

Dog.species() // "Dog"

Key Differences

FeaturestaticclassApplicable toClasses, structs, enumsClasses onlyCan be overridden?❌ No✔️ YesUsed forUtility, factory methodsPolymorphic type behavior

Static and class methods are essential for organizing shared logic and supporting type-level polymorphism.

21. Explain access control levels (private, fileprivate, internal, public, open).

Swift provides five access control levels to restrict the visibility and usage of code components (classes, properties, methods, etc.). Access control ensures encapsulation, prevents unwanted usage, and creates cleaner APIs.

1. private

  • Least accessible level.
  • Restricts use to within the same declaration (class, struct, extension).
  • Also accessible inside extensions within the same file when using private(set).

Example:

class Person {
    private var secret = "Hidden"
}

2. fileprivate

  • Visible anywhere in the same file.
  • Not visible outside the file.

Useful when multiple types in a file collaborate closely.

Example:

fileprivate var helperValue = 10

3. internal (default)

  • Visible anywhere within the same module.
  • Not visible across modules.

Ideal for app-level logic that doesn’t need to be public.

4. public

  • Visible across modules.
  • Cannot be subclassed or overridden outside the module.

Example:

public class Logger {}

Suitable for framework APIs that need visibility but not extensibility.

5. open

  • Most permissive level.
  • Can be:
    • Accessed
    • Subclassed
    • Overridden
    from other modules.

Example:

open class BaseViewController {}

Used when you want full extensibility in external frameworks.

Summary Table

LevelAccessible Where?Subclass Outside Module?Override Outside Module?privateSame declaration❌❌fileprivateSame file❌❌internalSame module❌❌publicEverywhere❌❌openEverywhere✔️✔️

22. What is KeyPath in Swift?

A KeyPath is a type-safe reference to a property, allowing properties to be accessed dynamically without using strings (unlike KVC in Objective-C).

Example:

struct Person {
    var name: String
    var age: Int
}

let nameKeyPath = \Person.name

Access value using keyPath:

let p = Person(name: "Aman", age: 25)
let name = p[keyPath: nameKeyPath]  // "Aman"

KeyPaths support:

  • Static type checking
  • Nested paths
  • Writable key paths (WritableKeyPath)
  • Reference-writable key paths for classes (ReferenceWritableKeyPath)

KeyPaths are extremely useful in:

  • SwiftUI bindings
  • Functional programming
  • Custom property access systems
  • Data mapping frameworks

Example in SwiftUI:

TextField("Name", text: $person[keyPath: \.name])

KeyPaths improve safety and eliminate string-based selector errors.

23. What is the difference between map, flatMap, and compactMap?

These are powerful higher-order functions used for data transformation.

map

Transforms each element and returns a new array of the same size.

let numbers = [1, 2, 3]
let result = numbers.map { $0 * 2 } 
// [2, 4, 6]

compactMap

  • Transforms elements
  • Automatically removes nil values
  • Returns a non-optional array

Example:

let values = ["1", "two", "3"]
let numbers = values.compactMap { Int($0) }
// [1, 3]

flatMap

Has two meanings depending on the context:

1. Flatten nested arrays

let nested = [[1, 2], [3, 4]]
let result = nested.flatMap { $0 }
// [1, 2, 3, 4]

2. Transform and flatten

let numbers = [1, 2, 3]
let repeated = numbers.flatMap { Array(repeating: $0, count: $0) }
// [1, 2, 2, 3, 3, 3]

Summary Table

FunctionOutputRemoves nil?Flattens arrays?mapTransformed same-size array❌❌compactMapTransformed array without nil✔️❌flatMapFlattened transformed array❌✔️

24. What is dispatch queue?

A dispatch queue is a lightweight object that manages the execution of tasks serially or concurrently. It is part of GCD (Grand Central Dispatch).

Two main types:

1. Serial Queue

  • Executes tasks one at a time in order.
  • Good for thread safety.
let serialQueue = DispatchQueue(label: "com.example.serial")

2. Concurrent Queue

  • Executes multiple tasks simultaneously.
  • Order of completion is not guaranteed.
let concurrentQueue = DispatchQueue(label: "com.example.concurrent", attributes: .concurrent)

Queues can be:

  • Main queue (updates UI)
  • Global queues (background system queues)
  • Custom queues (for specific tasks)

Dispatch queues simplify concurrency by abstracting thread creation and synchronization.

25. Explain synchronous vs asynchronous tasks.

Tasks on dispatch queues can be submitted synchronously (sync) or asynchronously (async).

sync (Synchronous)

  • Blocks the current thread until task finishes.
  • Used for immediate execution.

Example:

queue.sync {
    print("Task finished before continuing")
}

async (Asynchronous)

  • Returns immediately.
  • Task runs in background.
  • Does NOT block current thread.

Example:

queue.async {
    print("Task running in background")
}

Comparison Table

FeaturesyncasyncBlocks thread?✔️ Yes❌ NoCreates new thread?❌ No✔️ OftenExecution order?SequentialConcurrent possibilitiesUse caseSmall critical tasksBackground operations

Important: Using sync on the main queue causes a deadlock.

26. What is GCD (Grand Central Dispatch)?

GCD is a low-level, high-performance API for managing concurrency in Swift. It optimizes CPU usage by managing threads at the system level.

GCD provides:

  • Dispatch queues (main, global, custom)
  • Delayed execution
  • Background work
  • Thread pooling
  • Synchronization tools

Example:

DispatchQueue.global().async {
    let data = fetchData()
    DispatchQueue.main.async {
        updateUI(data)
    }
}

Why GCD is powerful?

  • Simple concurrency with async queues
  • Efficient thread usage
  • Scalable for heavy workloads
  • Great for networking, image processing, animations

GCD is the foundation for most concurrency work in Swift.

27. What is NSOperationQueue and when to use it?

NSOperationQueue is a higher-level concurrency API built on top of GCD. It manages Operation objects (subclassable).

Example:

let queue = OperationQueue()
queue.addOperation {
    print("Task executing")
}

When to use NSOperationQueue?

Use it when you need:

  • Task dependencies
  • Task cancellation
  • Task priorities
  • Reusability and observability
  • KVO support
  • Better control than raw GCD

Example with dependency:

let op1 = BlockOperation { print("Op1") }
let op2 = BlockOperation { print("Op2") }

op2.addDependency(op1)
queue.addOperations([op1, op2], waitUntilFinished: false)

NSOperationQueue is ideal for complex, controlled task orchestration.

28. What are property wrappers?

Property wrappers provide a clean way to add reusable behavior around property storage. They allow encapsulation of logic such as validation, transformation, caching, etc.

Example:

@propertyWrapper
struct Uppercase {
    private var value: String = ""

    var wrappedValue: String {
        get { value }
        set { value = newValue.uppercased() }
    }
}

Usage:

@Uppercase var name: String
name = "swift"
print(name) // "SWIFT"

Property wrappers reduce repetitive code and centralize logic for property management.

29. What is @propertyWrapper and when is it helpful?

The @propertyWrapper attribute marks a struct, class, or enum as a property wrapper by defining:

  • wrappedValue → actual stored value
  • Optional projectedValue → exposes metadata via $property

Example of built-in property wrapper:

@UserDefault("isLoggedIn", defaultValue: false)
var isLoggedIn: Bool

When is it helpful?

  • Reusable validation logic
  • Lazy storage
  • Thread-safe property access
  • Data persistence (UserDefaults wrapper)
  • Encoding/decoding transformations
  • Input sanitization (e.g., trimming whitespace)

Property wrappers dramatically simplify boilerplate code around property behavior.

30. What is Codable protocol?

Codable is a typealias for:

typealias Codable = Decodable & Encodable

It allows a type to encode to and decode from external formats such as:

  • JSON
  • Plists
  • Custom data formats

Example:

struct User: Codable {
    let name: String
    let age: Int
}

Encoding:

let jsonData = try JSONEncoder().encode(user)

Decoding:

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

Why Codable is important?

  • Strong type safety
  • Automatic JSON parsing
  • Fewer bugs than manual serialization
  • Works seamlessly with nested models
  • Extensively used in networking (REST APIs)

Codable is one of the most widely used protocols in Swift for API-driven applications.

31. What is Decodable vs Encodable?

Swift provides two protocols to support data serialization and deserialization:

Encodable

A type that conforms to Encodable can be encoded into an external representation, such as JSON or a plist.

Example:

struct User: Encodable {
    let name: String
    let age: Int
}

Encoding:

let data = try JSONEncoder().encode(user)

Decodable

A type that conforms to Decodable can be created from external data, such as JSON.

Example:

struct User: Decodable {
    let name: String
    let age: Int
}

Decoding:

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

Codable = Encodable + Decodable

You often need both, so Swift provides:

typealias Codable = Encodable & Decodable

Difference Summary

FeatureEncodableDecodablePurposeConvert model → dataConvert data → modelUsed forSending dataFetching/parsing dataJSONEncoder✔️❌JSONDecoder❌✔️

Most models conform to Codable for two-way conversion.

32. What is JSONSerialization?

JSONSerialization is a Foundation API that converts Swift objects to and from JSON using non-type-safe methods.

Example: JSON to dictionary

let json = """
{ "name": "Aman", "age": 25 }
""".data(using: .utf8)!

let dict = try JSONSerialization.jsonObject(with: json) as? [String: Any]

Example: dictionary to JSON

let data = try JSONSerialization.data(withJSONObject: dict)

Why use JSONSerialization?

  • Useful when JSON structure is dynamic
  • Needed for handling Any-based JSON
  • Required when Codable is too strict

Why prefer Codable instead?

  • Type safe
  • Cleaner and less error-prone
  • Automatic mapping
  • Easier debugging

JSONSerialization is good for low-level or dynamic JSON work, but Codable is better for strongly typed models.

33. What is Result type in Swift?

Result<T, Error> represents either a success with a value or a failure with an error. It simplifies error handling in callbacks and asynchronous APIs.

Example:

enum Result<Success, Failure: Error> {
    case success(Success)
    case failure(Failure)
}

Usage:

func fetchData(completion: (Result<String, Error>) -> Void) {
    completion(.success("Loaded"))
}

Handling the result:

fetchData { result in
    switch result {
    case .success(let value):
        print(value)
    case .failure(let error):
        print(error)
    }
}

Benefits of Result Type

  • Eliminates multiple optional values in callbacks
  • Makes intent explicit
  • Improves readability
  • Encourages structured error handling
  • Works nicely with async/await

Result type is central to modern Swift asynchronous programming.

34. Explain defer statement.

defer allows you to schedule code to run just before exiting the current scope, regardless of how the scope exits (return, error, break, etc.).

Example:

func readFile() {
    let file = openFile()
    defer {
        closeFile(file)
    }
    
    // reading file...
}

Even if an error occurs or the function returns early, the file always closes.

Key rules:

  • defer blocks run in reverse order (LIFO).
  • defer ensures cleanup happens reliably.
  • Useful for resource management.

Common use cases:

  • Closing files
  • Releasing locks
  • Cleaning temporary data
  • Ending database transactions
  • Removing observers

defer improves safety and reduces duplicated cleanup code.

35. What is Singleton pattern and how to implement it?

The Singleton pattern ensures that a class has exactly one instance throughout the app and provides a global point of access to it.

Swift Singleton Implementation:

class Logger {
    static let shared = Logger()
    private init() { }

    func log(_ message: String) {
        print(message)
    }
}

Usage:

Logger.shared.log("App started")

Why use Singleton?

  • Centralized shared resource
  • Global configuration
  • Caching
  • Networking managers
  • Database connections

Why use it carefully?

  • Can lead to tight coupling
  • Hard to test
  • Hidden dependencies

Singletons are powerful but should be used sparingly and thoughtfully.

36. What is memory leak and how can you avoid it?

A memory leak occurs when memory that should be released is not freed, causing the app’s memory usage to grow unnecessarily. In Swift, leaks usually happen due to strong reference cycles.

Example of a leak:

class A {
    var b: B?
}

class B {
    var a: A?
}

A and B hold strong references to each other, so ARC cannot deallocate them.

How to avoid memory leaks:

  • Use weak or unowned references appropriately
  • Break reference cycles in closures using [weak self]
  • Avoid storing strong references inside long-living structures
  • Use profiling tools like Xcode’s Leaks and Instruments

Proper ARC management ensures safe memory and smooth performance.

37. What is retain cycle in closures?

Closures capture variables from their surrounding context. If a closure captures self strongly, and self also strongly retains the closure, a retain cycle occurs.

Example:

class ViewController {
    var completion: (() -> Void)?

    func loadData() {
        completion = {
            print(self) // strong capture
        }
    }
}

Here:

  • self retains completion
  • completion retains self
  • Neither is released → memory leak

Solution: Capture self weakly/unowned

completion = { [weak self] in
    print(self?.description ?? "")
}

Retain cycles are the most common source of memory leaks in Swift closures.

38. What is the use of [weak self] and [unowned self] in closures?

These capture list keywords prevent retain cycles by controlling how closures reference self.

weak self

  • Does not increase reference count
  • Automatically becomes nil when deallocated
  • Must be optional

Example:

completion = { [weak self] in
    self?.updateUI()
}

Use when self may disappear before the closure executes (e.g., network call).

unowned self

  • Does not increase reference count
  • Assumes self will exist during closure execution
  • Crashes if accessed after deallocation

Example:

completion = { [unowned self] in
    updateUI()
}

Use when both objects have the same lifetime (e.g., parent-child relationships).

Comparison Table

FeatureweakunownedReference count❌ doesn’t retain❌ doesn’t retainOptional?YesNoRisk?Very safeUnsafe if self deallocatesWhen to use?Closure may exceed object's lifetimeLifetime of closure < lifetime of object

39. Explain ARC optimization techniques.

ARC automatically manages memory, but you can optimize its usage to prevent leaks and improve performance.

Common optimization practices:

1. Avoid strong reference cycles

  • Use weak or unowned references
  • Break circular dependencies

2. Capture only what you need in closures

Instead of:

{ print(self.largeObject) }

Use:

{ [weak self] in print(self?.largeObject) }

3. Minimize long-living strong references

Especially in:

  • Singletons
  • Caches
  • Delegates

4. Prefer structs (value types)

They don't participate in ARC.

5. Break strong cycles in delegates

Always declare delegates:

weak var delegate: SomeDelegate?

6. Use autoreleasepool for large loops

for i in 0...10000 {
    autoreleasepool {
        // heavy operations
    }
}

7. Use Instruments

Track leaks and retain cycles.

ARC optimization is vital in large-scale Swift apps to maintain performance and avoid crashes.

40. What is copy-on-write (COW) in Swift?

Copy-on-write (COW) is a memory optimization technique used by Swift for value types such as:

  • Arrays
  • Dictionaries
  • Sets
  • Strings

Instead of copying the entire data each time you assign a value type, Swift copies the data only when it is modified.

Example:

var a = [1, 2, 3]
var b = a  // No actual copy yet

b.append(4) // Now copy happens

Before modification:

  • a and b share the same memory buffer.

After modification:

  • Swift creates a new buffer for b
  • a remains unchanged

Benefits of COW

  • High performance
  • Efficient memory usage
  • Preserves value semantics without sacrificing speed

Swift tracks references internally and ensures data isolation on mutation.

Experienced (Q&A)

1. Explain Swift runtime internals.

Swift’s runtime is a sophisticated system responsible for managing:

  • Type metadata
  • Dynamic dispatch
  • Protocol witness tables
  • Memory management (ARC)
  • Generics specialization
  • Reflection
  • Error handling
  • Concurrency runtime (actors, tasks)

Internally, Swift relies on:

Type Metadata

Every Swift type has metadata describing:

  • Size and layout
  • Methods
  • V-table (for classes)
  • Protocol conformances
  • Value witness tables (VWV)—code used to copy, destroy, allocate, etc.

This metadata enables dynamic features such as reflection, casting, and generics.

Dynamic Dispatch Mechanisms

Swift uses several dispatch strategies:

  1. Static Dispatch – for structs, enums, final classes
  2. V-table Dispatch – for classes with overridden methods
  3. Witness Table Dispatch – for protocol methods
  4. Existential Dispatch – for using protocols as types

The compiler chooses the fastest dispatch appropriate for each context.

ARC Runtime

ARC inserts retain/release operations automatically. These are optimized heavily using:

  • Lifetime analysis
  • Redundant retain-elimination
  • Borrowing conventions

Error Handling Runtime

Swift uses a zero-cost exception model similar to C++:

  • Errors propagate using swift-specific metadata
  • Only actual thrown errors incur cost

Concurrency Runtime

Swift 5.5+ added:

  • Work-stealing thread pool
  • Cooperative thread scheduling
  • Actor isolation and message queues
  • Structured concurrency using tasks

Overall, the Swift runtime balances safety, performance, and dynamic capabilities with a relatively small footprint compared to languages like Objective-C.

2. Describe how ARC works at compile time vs runtime.

ARC (Automatic Reference Counting) has two phases of operation:

Compile-Time ARC

During compilation:

  • Swift inserts retain, release, and autorelease calls based on code flow.
  • The compiler performs lifetime analysis, determining exactly where objects are created and destroyed.
  • Optimization passes eliminate redundant ARC calls:
    • Retain-release pairs
    • ARC contraction
    • Move-only optimization
    • Borrow semantics

Example (simplified):

let obj = MyClass()
obj.doSomething()

Compiler inserts:

retain(obj)
invoke doSomething
release(obj)

Runtime ARC

At runtime:

  • The retain count stored on the object is incremented or decremented.
  • When the count reaches zero, the object is deallocated.
  • Deinitializers are invoked.
  • ARC interacts with Swift concurrency to ensure thread-safe memory access.

Runtime ARC manages:

  • Deallocation
  • Weak/unowned reference cleanup
  • Autorelease pools (bridged from Obj-C runtime)

Key differences

PhaseResponsibilityCompile timeInsert ARC operations + optimize themRuntimeExecute retain/release + free memory

ARC is efficient because it pushes complexity to compile-time analysis, not runtime garbage collection.

3. What is Swift ABI stability and why does it matter?

ABI = Application Binary Interface
It defines how compiled Swift code communicates with:

  • The OS
  • Swift runtime
  • Standard library
  • Other binaries

Swift achieved ABI stability in Swift 5.

Why ABI Stability Matters

  1. OS Bundles Swift Runtime Permanently
    iOS, macOS, tvOS, and watchOS include the Swift standard library and runtime embedded in the OS.
  2. Apps Become Smaller
    Before Swift 5, every app shipped its own version of the runtime.
  3. Binary Compatibility
    Swift code compiled with different compilers can now interoperate at runtime.
  4. Enables Long-Term Library Distribution
    Framework vendors can ship binary frameworks without requiring source code.

ABI stability is one of the most crucial milestones for Swift’s maturity as a systems-level and app-level language.

4. What is the difference between value semantics and reference semantics at scale?

At a high level:

  • Value semantics → structs, enums
  • Reference semantics → classes

But at scale, the differences deeply influence architecture.

Value Semantics (Structs)

Characteristics:

  • Copies on assignment (with COW optimization)
  • Predictable ownership
  • No shared mutable state
  • Thread-safe by design
  • Easier reasoning about state transitions

Ideal for:

  • Data models
  • Immutable or transactional logic
  • Concurrency-safe architectures

Reference Semantics (Classes)

Characteristics:

  • Multiple references to the same instance
  • Shared mutable state → harder to reason about
  • Requires manual ARC management
  • Risk of retain cycles

Ideal for:

  • Objects with lifetimes not tied to copying
  • Shared managers
  • Identity-based behavior

At scale, value semantics improve:

  • Testability
  • Concurrency safety
  • Predictability
  • Debugability

Whereas reference semantics:

  • Offer flexibility
  • Enable polymorphism
  • Support shared state

Modern Swift encourages value semantics by default, especially with SwiftUI and concurrency.

5. How does Swift manage memory for structs containing reference types?

Structs are value types, but they may contain properties that are reference types.

Example:

struct Person {
    var name: String
    var pet: Pet // Pet is a class (reference type)
}

Memory model:

  • The struct itself is stored inline (stack or optimized location)
  • The reference type property stores a pointer to heap memory

Copying the struct copies the pointer (not the object)

var p1 = Person(name: "Aman", pet: Pet())
var p2 = p1      // shallow copy
p2.pet.name = "Rex"

Both p1 and p2 now reference the same Pet.

Implications:

  • Struct behaves like value type
  • Embedded class behaves like reference type
  • Memory integrity depends on how reference types are mutated

If deep copy is needed, you must implement it manually.

6. Explain the Swift Concurrency model.

Introduced in Swift 5.5, built around:

1. Async/Await

Structured concurrency replacing nested completion handlers.

2. Actors

Types with isolated mutable state and automatic thread safety.

3. Tasks

Units of asynchronous work executed by the Swift runtime.

4. Task Groups

Structured parallelism: spawning multiple tasks and awaiting their results.

5. Cooperative Thread Pool

Swift runtime manages a work-stealing thread pool for async work.

6. Main Actor

Guarantees UI updates occur on the main thread.

Key Principles

  • Isolation — prevents data races
  • Structured concurrency — tasks have clear lifetimes
  • Automatic scheduling — runtime balances CPU usage
  • Backpressure — avoids thread explosion

Swift’s concurrency model is built for safety, performance, and clarity—far superior to callback-based GCD alone.

7. What are actors in Swift?

Actors are reference types similar to classes but with isolated mutable state. Only one task can access actor state at a time → eliminating data races.

Example:

actor BankAccount {
    var balance = 0

    func deposit(_ amount: Int) {
        balance += amount
    }
}

Key Properties of Actors

  • Enforce data isolation
  • Provide thread-safe mutation
  • Allow async access to internal properties
  • Use cooperative thread pool scheduling

Accessing actor methods:

await account.deposit(100)

Actors solve concurrency problems that traditionally required:

  • Locks
  • Semaphores
  • Serial queues

Actors are foundational to Swift’s new concurrency safety model.

8. Explain async/await in Swift.

async marks a function as asynchronous.
await suspends execution until the async function returns.

Example:

func fetchData() async -> String {
    "Response"
}

let result = await fetchData()

Benefits over completion handlers:

  • Linear, readable code
  • Automatic suspension instead of callback nesting ("callback hell")
  • Structured concurrency ensures task lifetime management
  • Compiler enforces async safety rules

Async functions execute inside the Swift concurrency runtime’s cooperative executor system.

9. What is Task and TaskGroup?

Task

Represents a unit of asynchronous work.

Example:

let task = Task {
    await fetchData()
}

Tasks can be:

  • Detached tasks – independent lifetimes
  • Child tasks – tied to a parent task

You can cancel tasks:

task.cancel()

TaskGroup

Enables structured parallelism:

await withTaskGroup(of: Int.self) { group in
    for i in 1...5 {
        group.addTask {
            return i * 2
        }
    }
    
    for await result in group {
        print(result)
    }
}

Benefits:

  • Concurrent processing of multiple tasks
  • Auto-cancellation when group exits
  • Safe accumulation of results

Task groups are ideal for parallelizing operations like network requests, file processing, image analysis, etc.

10. What is Sendable protocol and why is it required?

Sendable ensures that a type is safe to use across concurrency boundaries (threads, tasks, actors).

Example:

struct User: Sendable {
    var id: Int
}

Swift automatically marks many value types as Sendable.
Classes are not Sendable by default due to shared mutable state.

Why Sendable Matters

It prevents data races by enforcing:

  • Types passed between tasks must be safe
  • Mutability is restricted or isolated
  • Compiler enforces thread-safety rules

Example error:

func process(_ user: User) async { ... }

If User is not Sendable-ready, compiler will warn you.

Unchecked Sendable

final class Manager: @unchecked Sendable {}

This tells the compiler you are manually guaranteeing safety.

11. Explain re-entrancy in actors.

Swift actors ensure data isolation, meaning only one task can access actor-isolated state at a time.
However, actors are re-entrant, which means:

  • When an actor suspends (e.g., awaits an async call),
  • Other tasks waiting for the actor may resume execution in between.

Example:

actor Counter {
    var value = 0

    func increment() async {
        value += 1
        await Task.sleep(1_000_000)
        value += 1
    }
}

Call two increments concurrently:

await withTaskGroup(of: Void.self) { group in
    group.addTask { await counter.increment() }
    group.addTask { await counter.increment() }
}

Because of re-entrancy:

  • First call increments → suspends
  • Second call is allowed to run → increments

Final value becomes not deterministic based on interleaving.

Why does Swift allow re-entrancy?

  • Actors ensure data-race safety, not atomicity.
  • They allow better concurrency performance by letting tasks run while others are suspended.
  • Prevents actor from becoming a bottleneck.

How to avoid re-entrancy issues?

Use non-isolated local copies:

let localValue = value

or use a non-reentrant actor (future Swift may add this via attributes).

Re-entrancy is subtle and critical for designing safe concurrent systems in Swift.

12. What is the difference between @MainActor and DispatchQueue.main?

@MainActor is part of Swift Concurrency, while DispatchQueue.main is part of GCD.

@MainActor

  • Ensures execution happens on the main actor, not necessarily the main thread.
  • Provides data isolation guarantees for UI state.
  • Works with async/await.
@MainActor
func updateUI() { /* safe for UI */ }

Calling:

await updateUI()

DispatchQueue.main

  • A GCD serial queue representing the main thread.
  • Requires manual checks and dispatch calls.
DispatchQueue.main.async {
    updateUI()
}

Differences

Feature@MainActorDispatchQueue.mainThread or Actor?ActorThreadConcurrency modelSwift ConcurrencyGCDIsolation guarantees✔️ Yes❌ NoCompiler enforcement✔️ Yes❌ NoAsync/await integration✔️ NativeRequires bridgingSafety for UI stateExcellentManual discipline

@MainActor is safer, more modern, and integrates deeply with the Swift type system.

13. Explain how Swift handles thread safety.

Swift introduces multiple mechanisms to ensure thread safety across concurrency boundaries.

1. Actor Isolation

Actors guarantee that only one task accesses actor state at a time.

2. Sendable Protocol

Ensures that types passed across concurrency boundaries are safe.

3. Structured Concurrency

Tasks have well-defined life cycles; no orphan threads.

4. Compiler-Enforced Checks

Compiler ensures:

  • Non-Sendable types don’t cross actors
  • Actor-isolated functions are not accessed unsafely
  • UI state mutations occur on main actor

5. Value Semantics

Structs are copied, preventing shared mutable state.

6. Memory Barriers

Swift runtime enforces synchronization when actor state is accessed.

7. Avoiding Data Races

Swift guarantees:

  • No data races on actor-isolated state
  • Safe async access
  • Proper task scheduling

Swift’s concurrency model is designed to eliminate traditional threading problems like locks, semaphores, and manual synchronization.

14. What are advanced generic constraints?

Swift's generics allow powerful constraints on type parameters:

1. Protocol constraints

func printValue<T: CustomStringConvertible>(_ value: T)

2. Class constraints

func process<T: UIView>(_ view: T) {}

3. Associated type constraints

func compare<C1: Collection, C2: Collection>(_ c1: C1, _ c2: C2)
where C1.Element == C2.Element

4. Equality constraints

func identicalTypes<T, U>(_ a: T, _ b: U)
where T == U

5. Nested constraints

extension Array: Equatable where Element: Equatable {}

6. Conditional conformances

extension Optional: Encodable where Wrapped: Encodable {}

Advanced generic constraints allow modeling complex relationships between types, enabling expressive APIs similar to the Swift standard library.

15. Explain opaque types (some keyword).

Opaque types use some to hide the actual return type while preserving compile-time type checking.

Example:

func makeShape() -> some Shape {
    return Circle()
}

Key Properties of Opaque Types

  • Caller doesn’t know the exact type, but the compiler does.
  • The return type is fixed per function.
  • They preserve generic constraints without exposing types.

Why use opaque types?

  • API abstraction
  • Better performance than protocol types
  • Avoids existential overhead
  • Used heavily in SwiftUI

Opaque types enable safer abstraction while maintaining strong static typing.

16. What is the difference between opaque types and generics?

Both opaque types and generics abstract types, but in different ways.

Opaque Types ( some)

  • The function chooses the concrete type.
  • Caller doesn’t know the type but treats it as conforming to a protocol.

Example:

func shape() -> some Shape
  • Type is fixed for a given function.
  • Used for return types only.

Generics

  • The caller chooses the concrete type.

Example:

func draw<T: Shape>(_ shape: T)
  • Used for parameters, return types, structs, classes, enums.

Comparison Table

FeatureOpaque TypesGenericsType chosen byFunctionCallerVisibilityHiddenExposed in signatureFlexibilityLessMoreRuntime overheadLowerLowerUse casesSwiftUI, abstractionReusable algorithms

Opaque types simplify interfaces; generics maximize flexibility.

17. Explain the use of protocols with associated types (PAT) and challenges.

Protocols with associated types allow protocols to express generic relationships.

Example:

protocol Container {
    associatedtype Item
    func add(_ item: Item)
}

Use Cases

  • Build generic abstractions (like Sequence, Collection)
  • Define type relationships

Challenges

Protocols with associated types cannot be used as regular types:

let c: Container  // ❌ error

Because the compiler cannot infer Item type.

Workarounds

  1. Use generics
func process<C: Container>(_ container: C) {}

Use type erasure

struct AnyContainer<T>: Container { ... }

Use opaque return types

func makeContainer() -> some Container

PATs are extremely powerful but require sophisticated type modeling.

18. What is existential type and how is it used?

An existential type is a protocol used as a concrete type, meaning:

let shape: Shape

The value stored must conform to Shape, but the exact type is erased.

Existential types provide:

  • Dynamic dispatch
  • Runtime storage of heterogeneous values
  • Flexibility in modeling abstract behavior

Example:

protocol Drawable {
    func draw()
}

let items: [Drawable] = [Circle(), Square()]

Existential type challenges

  • Runtime overhead due to type erasure
  • Cannot store PATs directly
  • Harder for the optimizer to inline
  • Weak static type guarantees

Existentials are useful but should be avoided in performance-critical paths.

WeCP Team
Team @WeCP
WeCP is a leading talent assessment platform that helps companies streamline their recruitment and L&D process by evaluating candidates' skills through tailored assessments