Back to Articles
Interview PrepiOSInterviewSwift

Senior iOS Developer Interview Questions: A Complete Guide

12 min read

Senior iOS Developer Interview Questions: A Complete Guide

Landing a senior iOS developer role requires deep technical knowledge and practical experience. This guide covers the most important interview topics with detailed explanations and real-world code examples.

1. Memory Management & ARC

Question: Explain retain cycles and how to prevent them.

Answer: Retain cycles occur when two objects hold strong references to each other, preventing ARC from deallocating them. This leads to memory leaks.

swift
// ❌ Retain Cycle Example
class ViewController: UIViewController {
    var onDataLoaded: (() -> Void)?

override func viewDidLoad() { super.viewDidLoad()

// Retain cycle: self captures closure, closure captures self onDataLoaded = { self.updateUI() // Strong reference to self } } }

// ✅ Solution 1: Weak Reference class ViewController: UIViewController { var onDataLoaded: (() -> Void)?

override func viewDidLoad() { super.viewDidLoad()

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

// ✅ Solution 2: Unowned Reference (when you're certain self won't be nil) class DataManager { let updateHandler: () -> Void

init(viewController: ViewController) { updateHandler = { [unowned viewController] in viewController.updateUI() } } }

Key Differences:

  • weak: Creates an optional reference, automatically set to nil when deallocated
  • unowned: Non-optional reference, crashes if accessed after deallocation (use when certain it won't be nil)

    2. Swift Concurrency & async/await

    Question: How does Swift's new concurrency model work? Explain actors and async/await.

    Answer:

Swift's modern concurrency model eliminates data races and callback hell through structured concurrency.

swift
// Actor for thread-safe data access
actor BankAccount {
    private var balance: Double = 0

func deposit(amount: Double) { balance += amount }

func withdraw(amount: Double) throws { guard balance >= amount else { throw BankError.insufficientFunds } balance -= amount }

func getBalance() -> Double { return balance } }

// Using async/await for API calls class UserService { func fetchUser(id: String) async throws -> User { let url = URL(string: "https://api.example.com/users/\(id)")! let (data, _) = try await URLSession.shared.data(from: url) return try JSONDecoder().decode(User.self, from: data) }

// Parallel execution with async let func fetchUserWithDetails(id: String) async throws -> (User, [Post]) { async let user = fetchUser(id: id) async let posts = fetchUserPosts(id: id)

return try await (user, posts) // Wait for both to complete }

// Task groups for dynamic concurrency func fetchMultipleUsers(ids: [String]) async throws -> [User] { try await withThrowingTaskGroup(of: User.self) { group in for id in ids { group.addTask { try await self.fetchUser(id: id) } }

var users: [User] = [] for try await user in group { users.append(user) } return users } } }

Key Concepts:

  • Actors: Provide data isolation, preventing data races
  • async/await: Structured way to write asynchronous code
  • Task Groups: Execute multiple concurrent operations
  • MainActor: Ensures UI updates happen on main thread

    3. Protocol-Oriented Programming

    Question: What are the advantages of protocol-oriented programming over OOP?

    Answer:

Protocol-oriented programming (POP) enables composition over inheritance, value semantics, and better testability.

swift
// Protocol with default implementation
protocol Loggable {
    func log(_ message: String)
}

extension Loggable { func log(_ message: String) { print("[\(type(of: self))] \(message)") } }

// Protocol composition protocol Fetchable { associatedtype DataType func fetch() async throws -> DataType }

protocol Cacheable { associatedtype CacheKey: Hashable func cache(_ data: Any, forKey key: CacheKey) func getCached(forKey key: CacheKey) -> Any? }

// Combine protocols class UserRepository: Fetchable, Cacheable, Loggable { typealias DataType = User typealias CacheKey = String

private var cache: [String: User] = [:]

func fetch() async throws -> User { if let cached = getCached(forKey: "current_user") as? User { log("Returning cached user") return cached }

log("Fetching user from API") let user = try await fetchFromAPI() cache(user, forKey: "current_user") return user }

func cache(_ data: Any, forKey key: String) { if let user = data as? User { cache[key] = user } }

func getCached(forKey key: String) -> Any? { return cache[key] }

private func fetchFromAPI() async throws -> User { // API call implementation fatalError("Not implemented") } }

Advantages:

  • Value Types: Protocols work with structs and enums
  • Multiple Conformance: Types can conform to multiple protocols
  • Testability: Easy to create mock implementations
  • Default Implementations: Reduce code duplication

    4. Architecture Patterns

    Question: Compare MVVM, VIPER, and Clean Architecture. When would you use each?

    Answer:

    MVVM (Model-View-ViewModel)

  • Best for: Small to medium apps, SwiftUI apps
  • Pros: Simple, reactive, testable
  • Cons: Can become bloated in large apps

    swift
  • // MVVM Example class LoginViewModel: ObservableObject { @Published var email: String = "" @Published var password: String = "" @Published var isLoading: Bool = false @Published var errorMessage: String?

    private let authService: AuthService

    init(authService: AuthService = AuthService()) { self.authService = authService }

    @MainActor func login() async { isLoading = true errorMessage = nil

    do { try await authService.login(email: email, password: password) } catch { errorMessage = error.localizedDescription }

    isLoading = false } }

    VIPER (View-Interactor-Presenter-Entity-Router)

  • Best for: Large, complex apps with multiple modules
  • Pros: Clear separation, highly testable, scalable
  • Cons: Boilerplate code, steep learning curve

    Clean Architecture

  • Best for: Enterprise apps, long-term maintenance
  • Pros: Independence of frameworks, highly testable
  • Cons: More complex, initial setup overhead

    5. Combine Framework

    Question: How does Combine compare to async/await? When would you use each?

    Answer:

    swift
  • import Combine

    class SearchViewModel: ObservableObject { @Published var searchText: String = "" @Published var results: [SearchResult] = []

    private var cancellables = Set() private let searchService: SearchService

    init(searchService: SearchService = SearchService()) { self.searchService = searchService setupSearchPipeline() }

    private func setupSearchPipeline() { $searchText .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) .removeDuplicates() .filter { $0.count >= 3 } .flatMap { [weak self] query -> AnyPublisher<[SearchResult], Never> in guard let self = self else { return Just([]).eraseToAnyPublisher() }

    return self.searchService.search(query: query) .catch { _ in Just([]) } .eraseToAnyPublisher() } .receive(on: DispatchQueue.main) .assign(to: &$results) } }

    // Async/await version class SearchViewModelAsync: ObservableObject { @Published var searchText: String = "" @Published var results: [SearchResult] = []

    private var searchTask: Task?

    func search() { searchTask?.cancel()

    searchTask = Task { @MainActor in guard searchText.count >= 3 else { results = [] return }

    try? await Task.sleep(nanoseconds: 300_000_000) // Debounce

    guard !Task.isCancelled else { return }

    do { results = try await searchService.search(query: searchText) } catch { results = [] } } } }

    When to Use:

  • Combine: UI bindings, reactive streams, multiple value emissions
  • async/await: One-time operations, API calls, simple async flows

    6. Performance Optimization

    Question: How would you optimize a scrolling UITableView/UICollectionView?

    Answer:

    swift
  • class OptimizedTableViewCell: UITableViewCell { // 1. Reuse identifiers static let reuseIdentifier = "OptimizedCell"

    // 2. Lazy loading private lazy var thumbnailImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true return imageView }()

    // 3. Image caching private let imageCache = NSCache()

    func configure(with item: Item) { // 4. Cancel previous image loads thumbnailImageView.image = nil

    // 5. Check cache first let cacheKey = item.imageURL as NSString if let cachedImage = imageCache.object(forKey: cacheKey) { thumbnailImageView.image = cachedImage return }

    // 6. Load image asynchronously Task { if let image = try? await loadImage(from: item.imageURL) { await MainActor.run { self.thumbnailImageView.image = image self.imageCache.setObject(image, forKey: cacheKey) } } } }

    // 7. Avoid expensive operations in cellForRow override func prepareForReuse() { super.prepareForReuse() thumbnailImageView.image = nil } }

    // 8. Prefetching class MyViewController: UIViewController { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { // Prefetch data for upcoming cells dataSource.prefetchData(at: indexPath) } } }

    Optimization Techniques: 1. Cell reuse with proper identifiers 2. Lazy initialization 3. Image caching (NSCache) 4. Cancel previous operations 5. Background image processing 6. Prefetching API 7. Avoid layout calculations in cellForRow 8. Use estimatedRowHeight for dynamic content

    7. Unit Testing

    Question: How do you write testable code? Show dependency injection and mocking.

    Answer:

    swift
    // Protocol for dependency injection
    protocol NetworkServiceProtocol {
        func fetchUser(id: String) async throws -> User
    }

    // Production implementation class NetworkService: NetworkServiceProtocol { func fetchUser(id: String) 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) } }

    // Mock for testing class MockNetworkService: NetworkServiceProtocol { var shouldFail = false var mockUser: User?

    func fetchUser(id: String) async throws -> User { if shouldFail { throw NetworkError.serverError } return mockUser ?? User(id: id, name: "Test User") } }

    // Testable ViewModel class UserViewModel { private let networkService: NetworkServiceProtocol

    // Dependency injection through initializer init(networkService: NetworkServiceProtocol = NetworkService()) { self.networkService = networkService }

    func loadUser(id: String) async throws -> User { return try await networkService.fetchUser(id: id) } }

    // Unit Tests import XCTest

    class UserViewModelTests: XCTestCase { var sut: UserViewModel! var mockNetworkService: MockNetworkService!

    override func setUp() { super.setUp() mockNetworkService = MockNetworkService() sut = UserViewModel(networkService: mockNetworkService) }

    func testLoadUserSuccess() async throws { // Given let expectedUser = User(id: "123", name: "John Doe") mockNetworkService.mockUser = expectedUser

    // When let user = try await sut.loadUser(id: "123")

    // Then XCTAssertEqual(user.id, expectedUser.id) XCTAssertEqual(user.name, expectedUser.name) }

    func testLoadUserFailure() async { // Given mockNetworkService.shouldFail = true

    // When/Then do { _ = try await sut.loadUser(id: "123") XCTFail("Should throw error") } catch { XCTAssertTrue(error is NetworkError) } } }

    Conclusion

    These senior-level topics form the foundation of expert iOS development. Focus on understanding the "why" behind each concept, not just the "how." In interviews, demonstrate your problem-solving approach and real-world experience with these patterns.

    Key Takeaways:

  • Memory management prevents leaks and crashes
  • Modern concurrency simplifies async code
  • Protocol-oriented programming enables flexible architecture
  • Choose architecture based on project size and complexity
  • Performance optimization is crucial for user experience
  • Write testable code from the start

    Keep practicing, and good luck with your senior iOS interviews! 🚀