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:
For a streamlined assessment process, consider platforms like WeCP, which allow you to:
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 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:
Overall, Swift was created to provide developers with a safer and more modern language while still delivering high performance for Apple platforms.
In Swift, variables and constants are fundamental ways to store data in memory.
var) is a storage location whose value can change during execution.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.
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:
Swift also includes complex and collection types:
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.
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.
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.
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.
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.
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.
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:
if let, guard let)??)?.)Forced unwrapping is mainly used for scenarios like outlets in UIKit where the value is guaranteed to be set before use.
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:
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.
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:
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:
if statements.guard is most commonly used for:
It is considered a best practice in Swift for writing safe, clean, and maintainable code.
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:
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.
In Swift, the terms parameters and arguments are related but distinct:
Example:
func add(a: Int, b: Int) -> Int {
return a + b
}
Here, a and b are parameters.
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.
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:
They are especially useful in functions where optional customization is common.
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:
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.
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:
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.
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:
Example operations:
colors.insert("Yellow")
colors.contains("Blue")
colors.remove("Green")
Sets also support mathematical operations:
Sets are excellent for tasks requiring fast search and uniqueness guarantees.
In Swift, all types belong to one of two categories: value types or reference types.
Examples: Int, Double, Bool, String, Array, Dictionary, Set, Struct, Enum
Example:
var a = 10
var b = a
b = 20
// a still equals 10
Examples: Class, Function references, Actor, NSObjects
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.
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:
Swift encourages using structs by default because they:
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:
=== and !==) to compare instance references.Classes are ideal when:
However, Swift encourages using structs first unless class-specific features are necessary.
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:
Initializers ensure that every object begins life in a valid, predictable state, which is a core part of Swift’s safety model.
Swift classes support two types of initializers:
Example:
class Person {
var name: String
var age: Int
init(name: String, age: Int) { // designated
self.name = name
self.age = age
}
}
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)
}
}
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.
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:
They do not take up stored memory unless they depend on stored properties.
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:
var) or constant (let)Stored properties represent the real state of your data model.
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:
Example:
var score: Int = 0 {
willSet {
print("Score is about to change to \(newValue)")
}
didSet {
print("Score changed from \(oldValue) to \(score)")
}
}
Use cases:
Property observers do not apply to computed properties because they already have logic built-in.
Method overloading is the ability to define multiple methods with the same name but different:
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:
Overloading does NOT depend on return type alone; parameters must differ.
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:
Enums allow you to express intent clearly and safely in your code.
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:
Associated values behave like parameters attached to individual enum cases.
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:
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.
The switch statement in Swift is more powerful than in most languages because:
break after every case unless specified.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:
if statementsSwift’s switch is one of its strongest control-flow features.
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:
person is nil → expression returns niladdress is nil → expression returns nilOptional chaining allows you to traverse complex nested structures without writing multiple if let or guard let statements.
Benefits:
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.
Both break and continue are control-flow statements used inside loops, but they behave differently.
Example:
for i in 1...5 {
if i == 3 {
break
}
print(i)
}
Output: 1 2
The loop stops when i == 3.
Example:
for i in 1...5 {
if i == 3 {
continue
}
print(i)
}
Output: 1 2 4 5
StatementBehaviorbreakExits loop completelycontinueSkips current iteration only
These statements improve control over loop execution flow, making logic more flexible and efficient.
Loops allow you to repeat a block of code multiple times. Swift provides three primary loop types:
Used to iterate over a range, array, dictionary, set, or sequence.
for i in 1...5 {
print(i)
}
Repeats as long as a condition is true.
var count = 0
while count < 5 {
print(count)
count += 1
}
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:
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:
Example with parameters and return value:
let add = { (a: Int, b: Int) -> Int in
return a + b
}
Swift heavily uses closures in:
Closures are a core part of Swift’s functional programming capabilities.
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.
Swift uses two different equality operators depending on value type or reference type.
Example:
5 == 5 // true
"abc" == "abc" // true
For classes, you can overload == to define custom equality.
Example:
class Person {}
let p1 = Person()
let p2 = p1
let p3 = Person()
p1 === p2 // true (same instance)
p1 === p3 // false (different instances)
OperatorMeaningWorks On==Value equalityValue types + classes===Reference identityClasses only
This distinction is essential to understanding how reference types behave in Swift.
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:
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.
These reference types help ARC manage memory and prevent retain cycles.
A strong reference increases an object's reference count.
var person: Person? = Person()
Use strong references for normal ownership.
nil when object deallocatesUsed for preventing retain cycles, typically in parent-child relationships.
Example:
weak var delegate: SomeDelegate?
Used when the lifetime of both objects is tightly connected.
Example:
unowned var owner: Person
Reference TypeIncreases ARC Count?Can be nil?Safe?strongYesNoYesweakNoYesVery safeunownedNoNoUnsafe if object deallocates
Understanding these references is critical for preventing memory leaks 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:
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.
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:
Conformance allows:
Protocol conformance is a key building block of Swift’s flexible and powerful type system.
An extension in Swift allows you to add new functionality to an existing type without modifying its original source code. You can extend:
Extensions can add:
Example:
extension String {
var reversedString: String {
return String(self.reversed())
}
}
Usage:
"Swift".reversedString // "tfiwS"
Extensions embody Swift’s design philosophy of composition over inheritance. They allow modular, cleaner, reusable code without subclassing.
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:
Example:
protocol Vehicle {
func start()
}
extension Vehicle {
func start() {
print("Starting the vehicle…")
}
}
struct Car: Vehicle {}
Usage:
Car().start() // "Starting the vehicle…"
POP is considered the “Swift way” to build clean, modern architectures.
Structs and classes both define custom data types, but they differ in important ways.
Example:
var a = MyStruct()
var b = a // copy
var a = MyClass()
var b = a // reference to same object
mutating keyword to modify propertiesdeinitUse struct when:
Use class when:
Swift encourages preferring structs unless class-specific behavior is necessary.
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:
Classes do not require mutating because they are reference types.
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:
Lazy properties must be var because their value is assigned after initialization.
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:
Typealias is widely used for:
Swift uses a robust error-handling model based on throwing errors from functions and catching them where needed.
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)")
}
try → normal error propagationtry? → converts error into optionaltry! → crash if error occurs (unsafe)Error handling is powerful for:
It ensures clean separation between normal logic and error-handling logic.
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:
do block contains code that may throw errors.catch block can handle specific error types.catch catches all unhandled errors.Benefits:
Higher-order functions are functions that take other functions as parameters or return functions. Swift collections provide built-in higher-order functions.
Transforms each element and returns a new array.
let numbers = [1, 2, 3]
let squares = numbers.map { $0 * 2 } // [2, 4, 6]
Filters elements based on a condition.
let even = numbers.filter { $0 % 2 == 0 } // [2]
Combines all elements into a single value.
let sum = numbers.reduce(0) { $0 + $1 } // 6
Swift’s standard library is heavily optimized for these functions.
Closures passed to a function can be:
(Default behavior)
Example:
func perform(action: () -> Void) {
action() // must run inside function
}
Marked with @escaping
Example:
func getData(completion: @escaping () -> Void) {
DispatchQueue.global().async {
completion()
}
}
Closures that escape must capture self explicitly (e.g., [weak self]) to avoid strong reference cycles.
Escaping closures are used in:
Non-escaping closures are more efficient and safe but limited to synchronous execution.
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" }
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")
}
}
Autoclosures can hide performance-heavy expressions, so use them carefully.
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.
Example generic struct:
struct Stack<T> {
var items: [T] = []
mutating func push(_ item: T) { items.append(item) }
mutating func pop() -> T { items.removeLast() }
}
Generics are a cornerstone of Swift’s powerful type system.
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:
func compare<T: Comparable>(_ a: T, _ b: T) -> Bool {
return a > b
}
Here, T must conform to Comparable.
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)
}
Generic constraints make Swift’s generics extremely expressive and robust.
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 }
}
They are fundamental to protocol-oriented programming and Swift standard library (e.g., Sequence, Collection).
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:
start() (inherited)openDoor() (own requirement)Protocols can inherit from multiple protocols, unlike classes.
Example:
protocol SmartDevice: Camera, GPS, InternetEnabled { }
This improves type composition without deep inheritance chains.
Optional protocol requirements allow methods or properties in a protocol to be optional for conforming types. This is only available for @objc protocols, meaning:
@objcExample:
@objc protocol PrinterDelegate {
@objc optional func didStartPrinting()
@objc optional func didFinishPrinting()
}
Conforming class:
class Printer: PrinterDelegate {
func didStartPrinting() {
print("Started printing")
}
// didFinishPrinting not required
}
Swift prefers protocol extensions over optional requirements for default behavior, but optional requirements remain useful for Objective-C interoperability.
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:
ARC automatically triggers the deinitializer when reference count reaches zero.
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:
& to indicate it's being passed by referenceUse cases:
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:
Swift allows overloading most operators to support mathematical and logical operations for user-defined types.
Swift allows methods associated with the type itself rather than any particular instance. These are called static or class 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
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"
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.
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.
private(set).Example:
class Person {
private var secret = "Hidden"
}
Useful when multiple types in a file collaborate closely.
Example:
fileprivate var helperValue = 10
Ideal for app-level logic that doesn’t need to be public.
Example:
public class Logger {}
Suitable for framework APIs that need visibility but not extensibility.
Example:
open class BaseViewController {}
Used when you want full extensibility in external frameworks.
LevelAccessible Where?Subclass Outside Module?Override Outside Module?privateSame declaration❌❌fileprivateSame file❌❌internalSame module❌❌publicEverywhere❌❌openEverywhere✔️✔️
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:
WritableKeyPath)ReferenceWritableKeyPath)KeyPaths are extremely useful in:
Example in SwiftUI:
TextField("Name", text: $person[keyPath: \.name])
KeyPaths improve safety and eliminate string-based selector errors.
These are powerful higher-order functions used for data transformation.
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]
nil valuesExample:
let values = ["1", "two", "3"]
let numbers = values.compactMap { Int($0) }
// [1, 3]
Has two meanings depending on the context:
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]
FunctionOutputRemoves nil?Flattens arrays?mapTransformed same-size array❌❌compactMapTransformed array without nil✔️❌flatMapFlattened transformed array❌✔️
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:
let serialQueue = DispatchQueue(label: "com.example.serial")
let concurrentQueue = DispatchQueue(label: "com.example.concurrent", attributes: .concurrent)
Queues can be:
Dispatch queues simplify concurrency by abstracting thread creation and synchronization.
Tasks on dispatch queues can be submitted synchronously (sync) or asynchronously (async).
Example:
queue.sync {
print("Task finished before continuing")
}
Example:
queue.async {
print("Task running in background")
}
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.
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:
Example:
DispatchQueue.global().async {
let data = fetchData()
DispatchQueue.main.async {
updateUI(data)
}
}
GCD is the foundation for most concurrency work in Swift.
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")
}
Use it when you need:
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.
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.
The @propertyWrapper attribute marks a struct, class, or enum as a property wrapper by defining:
wrappedValue → actual stored valueprojectedValue → exposes metadata via $propertyExample of built-in property wrapper:
@UserDefault("isLoggedIn", defaultValue: false)
var isLoggedIn: Bool
Property wrappers dramatically simplify boilerplate code around property behavior.
Codable is a typealias for:
typealias Codable = Decodable & Encodable
It allows a type to encode to and decode from external formats such as:
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)
Codable is one of the most widely used protocols in Swift for API-driven applications.
Swift provides two protocols to support data serialization and deserialization:
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)
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)
You often need both, so Swift provides:
typealias Codable = Encodable & Decodable
FeatureEncodableDecodablePurposeConvert model → dataConvert data → modelUsed forSending dataFetching/parsing dataJSONEncoder✔️❌JSONDecoder❌✔️
Most models conform to Codable for two-way conversion.
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)
Any-based JSONJSONSerialization is good for low-level or dynamic JSON work, but Codable is better for strongly typed models.
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)
}
}
Result type is central to modern Swift asynchronous programming.
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.
Common use cases:
defer improves safety and reduces duplicated cleanup code.
The Singleton pattern ensures that a class has exactly one instance throughout the app and provides a global point of access to it.
class Logger {
static let shared = Logger()
private init() { }
func log(_ message: String) {
print(message)
}
}
Usage:
Logger.shared.log("App started")
Singletons are powerful but should be used sparingly and thoughtfully.
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.
[weak self]Proper ARC management ensures safe memory and smooth performance.
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 completioncompletion retains selfcompletion = { [weak self] in
print(self?.description ?? "")
}
Retain cycles are the most common source of memory leaks in Swift closures.
These capture list keywords prevent retain cycles by controlling how closures reference self.
nil when deallocatedExample:
completion = { [weak self] in
self?.updateUI()
}
Use when self may disappear before the closure executes (e.g., network call).
self will exist during closure executionExample:
completion = { [unowned self] in
updateUI()
}
Use when both objects have the same lifetime (e.g., parent-child relationships).
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
ARC automatically manages memory, but you can optimize its usage to prevent leaks and improve performance.
weak or unowned referencesInstead of:
{ print(self.largeObject) }
Use:
{ [weak self] in print(self?.largeObject) }
Especially in:
They don't participate in ARC.
Always declare delegates:
weak var delegate: SomeDelegate?
6. Use autoreleasepool for large loops
for i in 0...10000 {
autoreleasepool {
// heavy operations
}
}
Track leaks and retain cycles.
ARC optimization is vital in large-scale Swift apps to maintain performance and avoid crashes.
Copy-on-write (COW) is a memory optimization technique used by Swift for value types such as:
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:
ba remains unchangedSwift tracks references internally and ensures data isolation on mutation.
Swift’s runtime is a sophisticated system responsible for managing:
Internally, Swift relies on:
Every Swift type has metadata describing:
This metadata enables dynamic features such as reflection, casting, and generics.
Swift uses several dispatch strategies:
The compiler chooses the fastest dispatch appropriate for each context.
ARC inserts retain/release operations automatically. These are optimized heavily using:
Swift uses a zero-cost exception model similar to C++:
Swift 5.5+ added:
Overall, the Swift runtime balances safety, performance, and dynamic capabilities with a relatively small footprint compared to languages like Objective-C.
ARC (Automatic Reference Counting) has two phases of operation:
During compilation:
Example (simplified):
let obj = MyClass()
obj.doSomething()
Compiler inserts:
retain(obj)
invoke doSomething
release(obj)
At runtime:
Runtime ARC manages:
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.
ABI = Application Binary Interface
It defines how compiled Swift code communicates with:
Swift achieved ABI stability in Swift 5.
ABI stability is one of the most crucial milestones for Swift’s maturity as a systems-level and app-level language.
At a high level:
But at scale, the differences deeply influence architecture.
Characteristics:
Ideal for:
Characteristics:
Ideal for:
Whereas reference semantics:
Modern Swift encourages value semantics by default, especially with SwiftUI and concurrency.
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)
}
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.
If deep copy is needed, you must implement it manually.
Introduced in Swift 5.5, built around:
Structured concurrency replacing nested completion handlers.
Types with isolated mutable state and automatic thread safety.
Units of asynchronous work executed by the Swift runtime.
Structured parallelism: spawning multiple tasks and awaiting their results.
Swift runtime manages a work-stealing thread pool for async work.
Guarantees UI updates occur on the main thread.
Swift’s concurrency model is built for safety, performance, and clarity—far superior to callback-based GCD alone.
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
}
}
Accessing actor methods:
await account.deposit(100)
Actors solve concurrency problems that traditionally required:
Actors are foundational to Swift’s new concurrency safety model.
async marks a function as asynchronous.await suspends execution until the async function returns.
Example:
func fetchData() async -> String {
"Response"
}
let result = await fetchData()
Async functions execute inside the Swift concurrency runtime’s cooperative executor system.
Represents a unit of asynchronous work.
Example:
let task = Task {
await fetchData()
}
Tasks can be:
You can cancel tasks:
task.cancel()
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:
Task groups are ideal for parallelizing operations like network requests, file processing, image analysis, etc.
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.
It prevents data races by enforcing:
Example error:
func process(_ user: User) async { ... }
If User is not Sendable-ready, compiler will warn you.
final class Manager: @unchecked Sendable {}
This tells the compiler you are manually guaranteeing safety.
Swift actors ensure data isolation, meaning only one task can access actor-isolated state at a time.
However, actors are re-entrant, which means:
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:
Final value becomes not deterministic based on interleaving.
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.
@MainActor is part of Swift Concurrency, while DispatchQueue.main is part of GCD.
@MainActor
func updateUI() { /* safe for UI */ }
Calling:
await updateUI()
DispatchQueue.main.async {
updateUI()
}
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.
Swift introduces multiple mechanisms to ensure thread safety across concurrency boundaries.
Actors guarantee that only one task accesses actor state at a time.
Ensures that types passed across concurrency boundaries are safe.
Tasks have well-defined life cycles; no orphan threads.
Compiler ensures:
Structs are copied, preventing shared mutable state.
Swift runtime enforces synchronization when actor state is accessed.
Swift guarantees:
Swift’s concurrency model is designed to eliminate traditional threading problems like locks, semaphores, and manual synchronization.
Swift's generics allow powerful constraints on type parameters:
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.
Opaque types use some to hide the actual return type while preserving compile-time type checking.
Example:
func makeShape() -> some Shape {
return Circle()
}
Opaque types enable safer abstraction while maintaining strong static typing.
Both opaque types and generics abstract types, but in different ways.
some)Example:
func shape() -> some Shape
Example:
func draw<T: Shape>(_ shape: T)
FeatureOpaque TypesGenericsType chosen byFunctionCallerVisibilityHiddenExposed in signatureFlexibilityLessMoreRuntime overheadLowerLowerUse casesSwiftUI, abstractionReusable algorithms
Opaque types simplify interfaces; generics maximize flexibility.
Protocols with associated types allow protocols to express generic relationships.
Example:
protocol Container {
associatedtype Item
func add(_ item: Item)
}
Sequence, Collection)Protocols with associated types cannot be used as regular types:
let c: Container // ❌ error
Because the compiler cannot infer Item type.
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.
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:
Example:
protocol Drawable {
func draw()
}
let items: [Drawable] = [Circle(), Square()]
Existentials are useful but should be avoided in performance-critical paths.