# Top 25 Swift Interview Questions for iOS Developers > Prepare for your iOS interviews with the 25 most common Swift questions: optionals, closures, ARC, protocols, async/await and advanced patterns. - Published: 2026-01-29 - Updated: 2026-03-28 - Author: SharpSkill - Tags: swift, ios, interview, career, apple - Reading time: 15 min --- iOS technical interviews thoroughly test Swift fundamentals and advanced concepts. These 25 questions cover the topics most frequently asked by hiring managers, from language basics to modern concurrency patterns. > **How to use this guide** > > Each question includes a detailed answer with code examples. Questions are organized by increasing difficulty, from fundamentals to advanced concepts. ## Swift Fundamentals ### 1. What is the difference between `let` and `var`? `let` declares a constant whose value cannot be changed after initialization, while `var` declares a mutable variable. For reference types (classes), `let` prevents reassigning the reference but not modifying the object itself. ```swift // Constants.swift // let = constant, immutable value let maximumAttempts = 3 // maximumAttempts = 5 // ❌ Compile error // var = variable, mutable value var currentAttempt = 0 currentAttempt += 1 // ✅ OK // Careful with reference types class User { var name: String init(name: String) { self.name = name } } let user = User(name: "Alice") user.name = "Bob" // ✅ OK - modifying object, not reference // user = User(name: "Charlie") // ❌ Error - reassignment forbidden ``` **Best practice**: use `let` by default and only switch to `var` when mutation is required. This makes code more predictable and easier to reason about. ### 2. Explain optionals in Swift Optionals represent the possible absence of a value. An optional can contain either a value of the specified type or `nil`. Swift uses optionals to guarantee compile-time safety by forcing explicit handling of cases where a value might be missing. ```swift // Optionals.swift // Declaring an optional with ? var username: String? = nil // Can hold a String or nil // Optional binding with if let (safe unwrapping) if let name = username { print("Hello, \(name)") // Only executes if username != nil } else { print("Anonymous user") } // Guard let for early return func greet(user: String?) { guard let name = user else { print("No user provided") return // Early exit if nil } print("Hello, \(name)") // name is guaranteed non-nil here } // Nil-coalescing operator (??) for default value let displayName = username ?? "Anonymous" // Force unwrapping (!) - DANGEROUS, avoid this // let forced = username! // Crashes if nil ``` ### 3. What is the difference between `struct` and `class`? `struct` are value types (copied on assignment) while `class` are reference types (share the same instance). This fundamental distinction impacts performance, memory management, and code behavior. ```swift // ValueVsReference.swift // Struct = value type (copy) struct Point { var x: Int var y: Int } var p1 = Point(x: 10, y: 20) var p2 = p1 // Independent copy p2.x = 100 // Only modifies p2 print(p1.x) // 10 - p1 unchanged // Class = reference type (shared) class Rectangle { var width: Int var height: Int init(width: Int, height: Int) { self.width = width self.height = height } } let r1 = Rectangle(width: 10, height: 20) let r2 = r1 // Same shared instance r2.width = 100 // Modifies the shared instance print(r1.width) // 100 - r1 also modified! ``` **When to use which**: - **Struct**: simple data, immutable values, no inheritance needed - **Class**: identity matters, inheritance required, shared behavior ### 4. How does pattern matching work with `switch`? Swift's `switch` is powerful and exhaustive: it must cover all possible cases. It supports pattern matching on types, ranges, tuples, and additional conditions with `where`. ```swift // PatternMatching.swift enum NetworkError: Error { case timeout case serverError(code: Int) case noConnection } func handleError(_ error: NetworkError) { switch error { case .timeout: print("Request timed out") case .serverError(let code) where code >= 500: print("Critical server error: \(code)") case .serverError(let code): print("Server error: \(code)") case .noConnection: print("No connection") } // No default needed: all cases covered } // Pattern matching on ranges and tuples let point = (x: 5, y: 10) switch point { case (0, 0): print("Origin") case (let x, 0): print("On X axis at \(x)") case (0...10, 0...10): print("In the 10x10 square") default: print("Elsewhere") } ``` ### 5. Explain closures and their syntax Closures are self-contained blocks of code that capture and store references to variables from their surrounding context. They are the equivalent of lambdas or anonymous functions in other languages. ```swift // Closures.swift // Full syntax let add: (Int, Int) -> Int = { (a: Int, b: Int) -> Int in return a + b } // Shorthand syntax (inferred types, implicit return) let multiply: (Int, Int) -> Int = { $0 * $1 } // Trailing closure syntax let numbers = [3, 1, 4, 1, 5] let sorted = numbers.sorted { $0 > $1 } // [5, 4, 3, 1, 1] // Closure capturing a variable func makeCounter() -> () -> Int { var count = 0 // Captured variable return { count += 1 // Closure "closes over" count return count } } let counter = makeCounter() print(counter()) // 1 print(counter()) // 2 - count remembered between calls ``` > **Capture semantics** > > Closures capture variables by reference by default. To capture by value, use a capture list: `{ [count] in ... }`. ## Memory Management and ARC ### 6. How does ARC (Automatic Reference Counting) work? ARC automatically manages memory by counting strong references to each class instance. When the count drops to zero, the instance is deallocated. Unlike garbage collection, ARC is deterministic and predictable. ```swift // ARC.swift class Person { let name: String init(name: String) { self.name = name print("\(name) is initialized") } deinit { print("\(name) is deallocated") } } // Demonstrating lifecycle var person1: Person? = Person(name: "Alice") // refCount = 1 var person2 = person1 // refCount = 2 person1 = nil // refCount = 1 (not deallocated) person2 = nil // refCount = 0 → deinit called // Output: "Alice is deallocated" ``` ### 7. What is a retain cycle and how to prevent it? A retain cycle occurs when two objects hold strong references to each other, preventing their deallocation. The `weak` and `unowned` keywords break these cycles. ```swift // RetainCycle.swift class Department { let name: String var manager: Employee? // Strong reference init(name: String) { self.name = name } deinit { print("Department \(name) deallocated") } } class Employee { let name: String // weak prevents retain cycle - can become nil weak var department: Department? init(name: String) { self.name = name } deinit { print("Employee \(name) deallocated") } } // Without weak: retain cycle → memory leak // With weak: proper deallocation var dept: Department? = Department(name: "Engineering") var emp: Employee? = Employee(name: "Bob") dept?.manager = emp emp?.department = dept dept = nil // ✅ Deallocated thanks to weak emp = nil // ✅ Deallocated ``` ### 8. What is the difference between `weak` and `unowned`? Both break retain cycles, but with different guarantees. `weak` is optional and becomes `nil` if the referenced object is deallocated. `unowned` assumes the object always exists and crashes if accessed after deallocation. ```swift // WeakVsUnowned.swift class Customer { let name: String var card: CreditCard? init(name: String) { self.name = name } } class CreditCard { let number: String // unowned because a card always exists with its customer unowned let customer: Customer init(number: String, customer: Customer) { self.number = number self.customer = customer } } // Card cannot exist without customer let customer = Customer(name: "Alice") customer.card = CreditCard(number: "1234", customer: customer) // If customer is deallocated, accessing card.customer would crash ``` **Rule**: use `weak` by default. Only use `unowned` when the referenced object's lifetime is guaranteed to be equal or longer. ## Protocols and Generics ### 9. Explain protocols in Swift Protocols define a contract (required properties and methods) that conforming types must implement. They enable polymorphism and are the foundation of Protocol-Oriented Programming (POP) in Swift. ```swift // Protocols.swift // Protocol definition protocol Drawable { var color: String { get set } // Required property (read/write) func draw() // Required method } // Protocol extension with default implementation extension Drawable { func draw() { print("Default drawing in \(color)") } } // Protocol conformance struct Circle: Drawable { var color: String var radius: Double // draw() inherits default implementation } struct Square: Drawable { var color: String var side: Double // Override default implementation func draw() { print("Square \(color) with side \(side)") } } // Polymorphic usage let shapes: [Drawable] = [Circle(color: "red", radius: 5), Square(color: "blue", side: 10)] shapes.forEach { $0.draw() } ``` ### 10. What is an associated type? Associated types allow protocols to define generic types that will be specified by conforming types. This mechanism makes protocols like `Collection` so flexible. ```swift // AssociatedTypes.swift // Protocol with associated type protocol Container { associatedtype Item // Type defined by conformant var items: [Item] { get set } mutating func add(_ item: Item) func count() -> Int } // Implementation with Item = String struct StringBox: Container { typealias Item = String // Optional, Swift can infer var items: [String] = [] mutating func add(_ item: String) { items.append(item) } func count() -> Int { items.count } } // Implementation with Item = Int struct IntStack: Container { var items: [Int] = [] mutating func add(_ item: Int) { items.append(item) } func count() -> Int { items.count } } ``` ### 11. How do generics work? Generics allow writing flexible, reusable code that works with any type. They avoid code duplication while preserving type safety at compile time. ```swift // Generics.swift // Generic function func swap(_ a: inout T, _ b: inout T) { let temp = a a = b b = temp } // Type constraint with where func findIndex(of item: T, in array: [T]) -> Int? { for (index, element) in array.enumerated() { if element == item { return index } // Equatable required for == } return nil } // Generic struct struct Queue { private var elements: [Element] = [] mutating func enqueue(_ element: Element) { elements.append(element) } mutating func dequeue() -> Element? { guard !elements.isEmpty else { return nil } return elements.removeFirst() } } var intQueue = Queue() intQueue.enqueue(1) intQueue.enqueue(2) print(intQueue.dequeue()) // Optional(1) ``` ### 12. Explain the `Codable` protocol `Codable` (an alias for `Encodable & Decodable`) enables automatic serialization of Swift types to and from formats like JSON. The compiler generates the implementation if all properties are themselves Codable. ```swift // Codable.swift struct User: Codable { let id: Int let name: String let email: String let createdAt: Date // CodingKeys to map different JSON names enum CodingKeys: String, CodingKey { case id case name case email case createdAt = "created_at" // snake_case → camelCase } } // JSON decoding let json = """ { "id": 1, "name": "Alice", "email": "alice@example.com", "created_at": "2026-01-15T10:30:00Z" } """.data(using: .utf8)! let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 do { let user = try decoder.decode(User.self, from: json) print(user.name) // "Alice" } catch { print("Decoding error: \(error)") } // Encoding to JSON let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted let data = try encoder.encode(user) ``` ## Concurrency and async/await ### 13. How does async/await work in Swift? `async/await` simplifies asynchronous code by allowing non-blocking operations to be written sequentially. An `async` function can be suspended without blocking the thread, allowing other tasks to run. ```swift // AsyncAwait.swift // Asynchronous function func fetchUser(id: Int) async throws -> User { let url = URL(string: "https://api.example.com/users/\(id)")! // await suspends execution until response let (data, response) = try await URLSession.shared.data(from: url) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw NetworkError.invalidResponse } return try JSONDecoder().decode(User.self, from: data) } // Calling from async context func loadUserProfile() async { do { let user = try await fetchUser(id: 42) print("User: \(user.name)") } catch { print("Error: \(error)") } } // Calling from synchronous context with Task func buttonTapped() { Task { await loadUserProfile() } } ``` ### 14. What is an Actor? Actors are reference types that protect their internal state from concurrent access. They guarantee that only one task at a time can access their mutable properties, eliminating data races. ```swift // Actors.swift // Actor protects its state automatically actor BankAccount { private var balance: Double = 0 func deposit(_ amount: Double) { balance += amount // Automatic thread-safe access } func withdraw(_ amount: Double) -> Bool { guard balance >= amount else { return false } balance -= amount return true } func getBalance() -> Double { return balance } } // Usage - await required to access actor let account = BankAccount() Task { await account.deposit(100) let success = await account.withdraw(30) let balance = await account.getBalance() print("Balance: \(balance)") // 70 } ``` ### 15. Explain `Task` and `TaskGroup` `Task` creates a unit of asynchronous work. `TaskGroup` allows running multiple tasks in parallel and collecting their results. ```swift // TaskGroup.swift // Simple Task let task = Task { return await fetchUser(id: 1) } let user = try await task.value // TaskGroup for parallelization func fetchMultipleUsers(ids: [Int]) async throws -> [User] { try await withThrowingTaskGroup(of: User.self) { group in // Launch all requests in parallel for id in ids { group.addTask { try await fetchUser(id: id) } } // Collect results as they complete var users: [User] = [] for try await user in group { users.append(user) } return users } } // All 3 requests run in parallel let users = try await fetchMultipleUsers(ids: [1, 2, 3]) ``` ### 16. How does `@MainActor` work? `@MainActor` ensures code runs on the main thread. This is essential for UI updates that must always execute on the main thread. ```swift // MainActor.swift // UI class annotated with @MainActor @MainActor class UserViewModel: ObservableObject { @Published var user: User? @Published var isLoading = false @Published var error: String? func loadUser() async { isLoading = true // ✅ On main thread automatically do { // Network operation on background thread user = try await fetchUser(id: 42) } catch { self.error = error.localizedDescription } isLoading = false // ✅ Automatic return to main thread } } // Or for a specific function func updateUI() async { await MainActor.run { // This block runs on main thread label.text = "Updated" } } ``` ## Patterns and Architecture ### 17. Explain the Delegate pattern The Delegate pattern allows an object to delegate certain responsibilities to another object. It is ubiquitous in UIKit (UITableViewDelegate, UITextFieldDelegate, etc.). ```swift // DelegatePattern.swift // 1. Define the delegate protocol protocol DownloadManagerDelegate: AnyObject { func downloadDidStart() func downloadDidProgress(_ progress: Double) func downloadDidComplete(data: Data) func downloadDidFail(error: Error) } // 2. Class that uses the delegate class DownloadManager { // weak to avoid retain cycles weak var delegate: DownloadManagerDelegate? func startDownload(url: URL) { delegate?.downloadDidStart() // Simulated download Task { for progress in stride(from: 0.0, to: 1.0, by: 0.1) { try await Task.sleep(nanoseconds: 100_000_000) delegate?.downloadDidProgress(progress) } delegate?.downloadDidComplete(data: Data()) } } } // 3. Class that implements the delegate class ViewController: UIViewController, DownloadManagerDelegate { let manager = DownloadManager() override func viewDidLoad() { super.viewDidLoad() manager.delegate = self // Register as delegate } func downloadDidStart() { print("Started") } func downloadDidProgress(_ progress: Double) { print("\(progress * 100)%") } func downloadDidComplete(data: Data) { print("Complete") } func downloadDidFail(error: Error) { print("Error: \(error)") } } ``` ### 18. What is the MVVM pattern? MVVM (Model-View-ViewModel) separates presentation logic from the view. The ViewModel exposes observable data that the View displays, without knowing View details. ```swift // MVVM.swift // Model struct Article: Identifiable { let id: UUID let title: String let content: String let publishedAt: Date } // ViewModel @MainActor class ArticleListViewModel: ObservableObject { @Published private(set) var articles: [Article] = [] @Published private(set) var isLoading = false @Published var errorMessage: String? private let repository: ArticleRepository init(repository: ArticleRepository = .shared) { self.repository = repository } func loadArticles() async { isLoading = true errorMessage = nil do { articles = try await repository.fetchArticles() } catch { errorMessage = "Failed to load articles" } isLoading = false } } // View (SwiftUI) struct ArticleListView: View { @StateObject private var viewModel = ArticleListViewModel() var body: some View { Group { if viewModel.isLoading { ProgressView() } else { List(viewModel.articles) { article in Text(article.title) } } } .task { await viewModel.loadArticles() } } } ``` ### 19. Explain dependency injection Dependency injection involves providing an object's dependencies from the outside rather than creating them internally. This improves testability and decoupling. ```swift // DependencyInjection.swift // Protocol for abstraction protocol UserServiceProtocol { func fetchUser(id: Int) async throws -> User } // Real implementation class UserService: UserServiceProtocol { func fetchUser(id: Int) async throws -> User { // Real API call let url = URL(string: "https://api.example.com/users/\(id)")! let (data, _) = try await URLSession.shared.data(from: url) return try JSONDecoder().decode(User.self, from: data) } } // ViewModel with injection class ProfileViewModel: ObservableObject { private let userService: UserServiceProtocol // Constructor injection init(userService: UserServiceProtocol = UserService()) { self.userService = userService } func loadProfile(id: Int) async { // Uses injected service } } // Mock for testing class MockUserService: UserServiceProtocol { func fetchUser(id: Int) async throws -> User { return User(id: id, name: "Test User", email: "test@test.com") } } // In tests let viewModel = ProfileViewModel(userService: MockUserService()) ``` ### 20. How to implement the Singleton pattern? Singleton ensures a class has only one globally accessible instance. In Swift, a static property with a private initializer is used. ```swift // Singleton.swift class NetworkManager { // Single globally accessible instance static let shared = NetworkManager() // Private initializer prevents creating other instances private init() { // Initial configuration } private let session = URLSession.shared func request(_ url: URL) async throws -> T { let (data, _) = try await session.data(from: url) return try JSONDecoder().decode(T.self, from: data) } } // Usage let user: User = try await NetworkManager.shared.request(url) ``` > **Caution with Singletons** > > Singletons create global state that complicates testing and decoupling. Prefer dependency injection when possible. ## Advanced Concepts ### 21. Explain `@escaping` for closures A closure is `@escaping` when it can be called after the function that receives it returns. This is common for async callbacks and closure storage. ```swift // Escaping.swift class DataLoader { // Storage of completion handlers private var completionHandlers: [() -> Void] = [] // @escaping because closure is stored and called later func loadData(completion: @escaping () -> Void) { completionHandlers.append(completion) DispatchQueue.global().async { // Async work... Thread.sleep(forTimeInterval: 1) DispatchQueue.main.async { // Closure called after loadData returns completion() } } } // Non-escaping by default: closure called before return func transform(data: Data, using transformer: (Data) -> String) -> String { return transformer(data) // Called immediately } } // With @escaping, watch for retain cycles class ViewController { var loader = DataLoader() var data: String? func load() { loader.loadData { [weak self] in // [weak self] avoids retain cycle self?.data = "Loaded" } } } ``` ### 22. What is `@propertyWrapper`? Property wrappers encapsulate the storage and access logic of a property. They allow reusing patterns like validation, logging, or persistence. ```swift // PropertyWrapper.swift // Property wrapper for positive values only @propertyWrapper struct Positive { private var value: Int = 0 var wrappedValue: Int { get { value } set { value = max(0, newValue) } // Force positive } // Projected value accessible via $ var projectedValue: Bool { value > 0 } init(wrappedValue: Int) { self.wrappedValue = wrappedValue } } // Usage struct Player { @Positive var score: Int = 0 @Positive var health: Int = 100 } var player = Player() player.score = -50 // Becomes 0 (clamped) print(player.score) // 0 print(player.$score) // false (projectedValue) player.score = 100 print(player.$score) // true ``` ### 23. Explain result builders Result builders allow building complex values with declarative syntax. This is the mechanism behind SwiftUI's DSL syntax. ```swift // ResultBuilder.swift // Result builder definition @resultBuilder struct StringBuilder { static func buildBlock(_ components: String...) -> String { components.joined(separator: " ") } static func buildOptional(_ component: String?) -> String { component ?? "" } static func buildEither(first component: String) -> String { component } static func buildEither(second component: String) -> String { component } } // Function using the builder func buildGreeting(@StringBuilder _ content: () -> String) -> String { content() } // Usage with declarative syntax let greeting = buildGreeting { "Hello" "and" "welcome" if Bool.random() { "!" } else { "." } } print(greeting) // "Hello and welcome !" or "Hello and welcome ." ``` ### 24. How does `some` and opaque types work? `some` declares an opaque type: the exact type is known to the compiler but hidden from the caller. This is essential for protocols with associated types and enables optimizations. ```swift // OpaqueTypes.swift // Without some: error because Collection has associated type // func makeCollection() -> Collection { ... } // ❌ Error // With some: exact type is hidden but consistent func makeArray() -> some Collection { return [1, 2, 3] // Always returns the same concrete type } // Used in SwiftUI for body struct ContentView: View { var body: some View { // Exact type inferred but hidden VStack { Text("Hello") Text("World") } } } // Difference with any (existential) func processAny(_ collection: any Collection) { // Can accept different types, runtime overhead } func processSome(_ collection: some Collection) { // Type fixed at compile time, no overhead } ``` ### 25. Explain Swift macros Macros (Swift 5.9+) generate code at compile time. They reduce boilerplate while remaining type-safe and debuggable. ```swift // Macros.swift // Freestanding macro: generates an expression let (x, y) = #unwrap(optionalX, optionalY) // Expands to: guard let x = optionalX, let y = optionalY else { ... } // Attached macro: modifies a declaration @Observable // Macro that generates Observable boilerplate class UserModel { var name: String = "" var email: String = "" } // Automatically generates @ObservationTracked, ObservationRegistrar, etc. // Macro for Codable with customization @Codable struct Product { let id: Int @CodableKey("product_name") let name: String // Renames JSON key @CodableIgnored var cache: Data? // Excludes from coding } // Creating a custom macro @attached(member, names: named(init)) public macro AutoInit() = #externalMacro(module: "MyMacros", type: "AutoInitMacro") @AutoInit struct Point { let x: Int let y: Int // init(x: Int, y: Int) generated automatically } ``` > **Debugging macros** > > In Xcode, right-click on a macro then "Expand Macro" to see generated code. Useful for understanding and debugging. ## Conclusion These 25 questions cover the fundamentals every Swift developer must master to succeed in iOS interviews. From memory management with ARC to modern concurrency patterns, each concept integrates into the Swift ecosystem. ### Review Checklist - ✅ Master optionals and their various unwrapping methods - ✅ Understand the difference between value and reference types - ✅ Know how to identify and resolve retain cycles with weak/unowned - ✅ Use async/await and actors for concurrency - ✅ Implement Delegate, MVVM, and Dependency Injection patterns - ✅ Know protocols, generics, and Codable - ✅ Understand advanced concepts: property wrappers, result builders, macros ### Additional Resources For deeper learning, the official Swift documentation remains the reference. Regular practice with personal projects and coding exercises helps solidify these concepts. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/ios/top-25-swift-interview-questions