Are you preparing for your next iOS interview and feeling overwhelmed by Swift questions? You’re not alone. From closures and optionals to protocols and design patterns, Swift interviews can get tricky. But don’t worry — this blog is not just a list of questions and answers. It’s a learning guide built to help you deeply understand every core concept that interviewers love to ask.
Whether you’re a beginner trying to break into iOS development or an experienced dev aiming for top tech companies, this blog will:
Classes vs Structures
In Swift, Structs and Classes are both used to create custom data types, but they behave very differently:
A Struct is a value type: every time it is assigned or passed, a new copy is created.
A Class is a reference type: when assigned or passed, it shares the same reference in memory.
Struct is like a Tiffin Box Copy:
Imagine you bring a lunchbox (tiffin) to school. Your friend likes it so much, he makes a photocopy of the menu. Now, if you add extra spice (mirchi) to your original tiffin, his copied version stays the same.
Each person has their own separate version.
This is how structs work — they create independent copies.
Class is like a Google Doc:
Now think about a Google Document shared with your team. If anyone edits the document, the changes are visible to everyone in real-time.
All of you are working on the same shared file.
This is how classes work — they use shared references in memory.
Memory Difference: Stack vs Heap
Struct (Value Type)
Stored on the Stack, which is faster and automatically managed.
New copy is created on assignment.
Class (Reference Type)
Stored on the Heap, and uses ARC (Automatic Reference Counting) to manage memory.
Reference is shared — changes reflect everywhere.
Mutability Rules
If a struct is declared with let
, none of its properties can be changed — even if those properties are declared with var
.
If a class instance is declared with let
, its properties can still be changed as long as they’re var
.
Why? Because let
for structs freezes the whole value, but for classes, it freezes the reference, not the content.
Inheritance
Class supports inheritance — one class can inherit from another.
Struct does not support inheritance at all.
ARC — Automatic Reference Counting
Class instances are stored on the heap and managed with ARC.
Struct instances are stored on the stack, no ARC involved.
Performance
Structs are faster, especially when small and frequently created/destroyed.
Classes are heavier, because ARC has overhead, and shared memory requires more tracking.
When to Use Class vs Struct
Struct
- You’re modeling simple data (like
User
,Post
,Product
) - You want to keep things immutable or copy-safe
- You don’t need inheritance or identity
Class
- You need inheritance, polymorphism, or reference-sharing
- You’re working with UIKit components
- You’re managing shared logic (like singleton managers, services)
Example
Struct:
Let’s say you’re making a cricket prediction app and need to represent a Player
model:
struct Player {
var name: String
var team: String
var runs: Int
}
var player1 = Player(name: "Virat Kohli", team: "India", runs: 50)
var player2 = player1 // Creates a copy
player2.runs = 75
print(player1.runs) // Output: 50
print(player2.runs) // Output: 75
Class :
Now, you need to create a Data Manager to track the app’s network calls or store a single user session:
class NetworkManager {
var isConnected: Bool = false
}
let manager1 = NetworkManager()
let manager2 = manager1 // Shares the same instance
manager2.isConnected = true
print(manager1.isConnected) // Output: true
print(manager2.isConnected) // Output: true
Closure
A Closure in Swift is a self-contained block of code that can be passed around and used later.
You can think of it like a function without a name, which you can assign to variables, pass to other functions, or even return from a function.
Closures can:
- Capture values from their surrounding context
- Be stored as variables
- Be used for callback logic
let greet: (String) -> String = { (name) in
return "Hello, \(name)"
}
print(greet("Swift")) // Output: Hello, Swift
Why Use Closures?
- To pass behavior as a value (functional programming)
- To write completion handlers (network requests, animations)
- To make code more flexible and reusable
Key Closure Concepts
Trailing Closures
If the last parameter of a function is a closure, you can use trailing closure syntax.
func doSomething(task: () -> Void) {
task()
}
// Call using trailing closure
doSomething {
print("Task done")
}
This is commonly used in SwiftUI, GCD, and completion handlers.
Capturing Values
Closures capture and store references to variables from the context in which they were created.
func makeCounter() -> () -> Int {
var count = 0
return {
count += 1
return count
}
}
let counter = makeCounter()
print(counter()) // 1
print(counter()) // 2
Here, the closure remembers the count
variable — even after the function has finished!
Interview Tip: This is called a closure capturing its environment.
Escaping vs Non-Escaping Closures
Non-Escaping (default):
Closure is called before the function returns.
func doWork(closure: () -> Void) {
closure() // called inside
}
Escaping:
Closure is stored and called after the function returns.
func fetchData(completion: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
completion()
}
}
Use @escaping
when you store the closure to be used later, like in async calls or network APIs.
Autoclosures:
Used to delay execution of a closure until it’s actually used.
func logIfNeeded(_ message: @autoclosure () -> String) {
print("LOG: \(message())")
}
logIfNeeded("App started") // looks like a normal string, but it's a closure
Closures and Memory Leaks
When closures capture self
strongly, it can cause retain cycles (especially in classes). To avoid this, use capture lists:
class ViewModel {
var name = "Swift"
func loadData(completion: @escaping () -> Void) {
DispatchQueue.main.async { [weak self] in
print(self?.name ?? "No name")
completion()
}
}
}
Always use [weak self]
or [unowned self]
in escaping closures to prevent memory leaks.
OPTIONALS
An Optional in Swift is a type that can hold a value or no value (nil).
It’s Swift’s way of safely handling the absence of a value, without crashing your app like in other languages.
Why Use Optionals?
Imagine you’re fetching user data from a server. The user’s address might or might not be there. Instead of using a placeholder like "N/A"
or crashing, you use:
var address: String? // might be nil
Optionals allow safer coding in unpredictable situations — like APIs, inputs, CoreData, etc.
Unwrapping Optionals
Since Optionals can be nil
, you can’t use them directly. You need to unwrap them.
Force Unwrapping (!
)
Use when you’re sure there’s a value. Risky! If name
is nil
, the app crashes.
let name: String? = "Rahul"
print(name!) // Output: Rahul
Optional Binding (if let
/ guard let
)
if let
if let safeName = name {
print("Hello, \(safeName)")
} else {
print("Name is nil")
}
guard let
func greet(_ name: String?) {
guard let unwrapped = name else {
print("No name provided")
return
}
print("Hello, \(unwrapped)")
}
Nil Coalescing Operator (??
)
Provide a default value if nil
.
let userCity: String? = nil
let finalCity = userCity ?? "Mumbai"
print(finalCity) // Output: Mumbai
Use this to avoid writing too much if let
.
Optional Chaining
Use ?
to safely access properties or methods.
let user: User? = getUser()
let street = user?.address?.street
If any part is nil
, the entire expression becomes nil
.
Interview Tip:
“Optional is a wrapper around your actual type. It helps avoid runtime crashes by forcing the developer to handle missing values safely.”
Generics
Generics allow you to write flexible, reusable code that works with any type, without sacrificing type safety.
Instead of writing duplicate code for every data type, you write it once — using a placeholder type (like T
) — and Swift handles the rest.
Think of Generics as a reusable lunch box 🥡. Whether you store pasta (String), sandwiches (Int), or fruit (CustomType) — the container stays the same.
You’re only changing what goes inside.
func swapValues<T>(a: inout T, b: inout T) {
let temp = a
a = b
b = temp
}
Works for any type (Int
, String
, Double
, etc.) using just one function.
Generics in Functions
func printArray<T>(items: [T]) {
for item in items {
print(item)
}
}
printArray(items: [1, 2, 3])
printArray(items: ["a", "b", "c"])
Generics in Structs and Classes
struct Box<T> {
var value: T
}
let intBox = Box(value: 123)
let stringBox = Box(value: "Swift")
Type Constraints with Generics
Sometimes, you want to restrict a generic type to certain protocols (e.g., Comparable
, Equatable
, etc.)
func findLargest<T: Comparable>(a: T, b: T) -> T {
return a > b ? a : b
}
findLargest(a: 7, b: 3) // Works
findLargest(a: "cat", b: "dog") // Works
T: Comparable
ensures you can use >
and <
.
Leave a Reply