Senior iOS Developer Interview Questions: A Complete Guide
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
- 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:
- Cell reuse with proper identifiers
- Lazy initialization
- Image caching (NSCache)
- Cancel previous operations
- Background image processing
- Prefetching API
- Avoid layout calculations in cellForRow
- 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
Frequently Asked Questions (FAQ)
Q: How do I prepare for senior iOS interviews in 2-3 weeks?
Focus on: 1) Memory management (ARC, weak/strong, retain cycles - 30% of questions), 2) Concurrency (async/await, actors, GCD - 25%), 3) Architecture patterns (MVVM, Coordinator - 20%), 4) Build one complex side project demonstrating these concepts, 5) Practice system design (design Instagram, Twitter) on whiteboard, 6) Review your past projects and be ready to discuss technical decisions in depth.
Q: What's the difference between junior and senior iOS developer interview questions?
Junior interviews test syntax and basic concepts (optionals, protocols, closures). Senior interviews test: system design (how would you architect X?), performance optimization (why is this slow?), trade-off analysis (MVVM vs VIPER?), debugging complex issues, mentoring ability, and past experience with scalability challenges. Expect 40% system design, 30% advanced technical, 30% behavioral/leadership.
Q: How do I explain memory management in interviews?
Use this framework: 1) Explain ARC basics (automatic reference counting), 2) Show strong vs weak difference with code example, 3) Demonstrate retain cycle problem (closure capturing self), 4) Solution: [weak self] or [unowned self], 5) Mention real-world scenario you encountered (e.g., notification observer causing memory leak), 6) Discuss how you debug memory issues (Instruments, Memory Graph Debugger).
Q: What are the most common mistakes candidates make in senior iOS interviews?
Top mistakes: 1) Over-engineering simple solutions (using VIPER for 5-screen app), 2) Not asking clarifying questions before coding, 3) Ignoring edge cases and error handling, 4) Writing code without explaining thought process, 5) Not discussing trade-offs (X is faster but uses more memory), 6) Poor communication (code silently for 30 minutes), 7) Not testing code mentally before submitting.
Q: How do I demonstrate senior-level expertise beyond coding?
Show: 1) Technical leadership (how you mentored juniors, code review examples), 2) System thinking (discussing app architecture from API to UI), 3) Performance mindset (profiling, optimization decisions), 4) Production awareness (crash reporting, analytics, A/B testing), 5) Business understanding (technical decisions impact user retention/revenue), 6) Ask intelligent questions about their tech stack, team structure, and technical challenges.
Q: Should I mention specific apps I've worked on during interviews?
Yes, but strategically: 1) Prepare 2-3 detailed case studies from past work, 2) Focus on your specific contributions (I designed the caching layer that reduced API calls by 60%), 3) Discuss challenges and how you solved them, 4) Quantify impact (improved app rating from 3.2 to 4.5), 5) Be honest about what you didn't do, 6) Have App Store links ready if apps are public, 7) Respect NDAs (don't share proprietary code).
Q: How deeply should I know SwiftUI vs UIKit for senior roles in 2025?
Depends on role, but generally: SwiftUI (70% of new projects) - know declarative patterns, state management, performance optimization, Combine integration. UIKit (maintenance of legacy code) - understand lifecycle, Auto Layout, UICollectionView, coordinator pattern. Demonstrate: ability to mix both (UIHostingController), migration strategy from UIKit to SwiftUI, when to use each (complex animations favor UIKit), and modern best practices for both.
Q: What salary range should I expect for senior iOS roles in 2025?
Varies by location and experience: US (SF/NYC): $140K-200K + equity. US (other cities): $110K-150K. Europe: €60K-90K (€80K-120K in London/Zurich). Remote: $100K-150K. Factors affecting salary: FAANG vs startup (30-50% difference), years of experience (5+ years for senior), published apps with significant users, contributions to open source, system design expertise, leadership experience. Negotiate based on total comp (base + equity + bonus).
Related Guides
📖 Master iOS Development: [iOS Development Hub 2025: Complete Swift, SwiftUI & UIKit Guide](/en/blog/ios-development-hub-2025) - Want to master all these concepts? Our comprehensive iOS Development Hub covers Swift fundamentals, SwiftUI & UIKit, MVVM architecture, Firebase integration, performance optimization, testing strategies, App Store distribution, and complete iOS developer career guidance with salary insights and growth paths.
Keep practicing, and good luck with your senior iOS interviews! 🚀

Ali Mert Güleç
Mobile-Focused Full Stack Engineer
Passionate about creating exceptional mobile experiences with 7+ years of expertise in iOS, Android, and React Native development. I've helped businesses worldwide transform their ideas into successful applications with millions of active users.