iOS Interview Questions — Middle to Strong Senior

The questions we ask in real vetting interviews — with answers, code, and production nuance. Prepare at the level clients actually hire.

What’s inside

Reviewed by Lemon.io
  • Middle questions Fundamentals under production pressure
  • Senior questions Architecture, performance, Node.js
  • Coding questions System design, security, judgment calls
  • Quick comfort check 3 minutes — see where you stand
  • Time to the first offer

    ~13 days

  • Acceptance rate

    1.2%

  • Recommend us on Glassdoor

    99%

Middle iOS questions

3–5 years of experience. Solid fundamentals under production pressure — this is where Lemon.io screening starts.

01.What is the difference between var and let in Swift?
  • var is used to declare variables whose values can change (mutable).
  • let is used to declare constants whose values cannot be changed once assigned (immutable).Example:
var name = "John" // mutable
name = "Jane" // allowed

let age = 25 // immutable
// age = 26 // not allowed, compile-time error
02.What is optional chaining and how is it used in Swift?

Optional chaining is a process for querying and calling properties, methods, and subscripts on an optional that might currently be nil. If the optional is nil, the call returns nil without causing a runtime error; otherwise, it unwraps and proceeds.

Example:

struct Person {
var dog: Dog?
}
struct Dog {
var name: String
}

let person = Person(dog: Dog(name: "Rex"))
print(person.dog?.name ?? "Dog is nil") // "Rex"

If person.dog was nil, person.dog?.name would simply return nil.

03.What is ARC (Automatic Reference Counting)?

ARC is a memory management mechanism in Swift (and Objective-C) that automatically keeps track of how many references point to each class instance.

When there are no more references (strong references) to an object, ARC frees it from memory. This eliminates the need for manual memory management most of the time.

04.How would you use a completion handler in Swift?

A completion handler is a callback function passed as an argument to another function. It executes after a task completes (e.g., a network request). This helps with asynchronous operations.

Example:

func fetchData(completion: @escaping (Data?) -> Void) {
// Simulate asynchronous task
DispatchQueue.global().async {
let data = Data()
completion(data)
}
}

Here, the completion handler is called once data is fetched.

05.What is the difference between Struct and Class in Swift?
  • Structs are value types; they are copied on assignment or passing.
  • Classes are reference types; multiple variables can refer to the same instance. Classes support inheritance, while structs do not.
06.What is a protocol and how do you define one?

A protocol defines a blueprint of methods, properties, and other requirements that a class, struct, or enum can adopt.

Example:

protocol Greetable {
var greeting: String { get }
func greet()
}

struct Greeter: Greetable {
let greeting: String
func greet() {
print(greeting)
}
}
07.What are extensions in Swift?

Extensions add new functionality to an existing class, struct, enum, or protocol type without modifying the original source code. They can add computed properties, instance methods, type methods, and more.

Example:

extension String {
var reversedString: String {
return String(self.reversed())
}
}
08.What is the difference between synchronous and asynchronous tasks?

Async tasks are added to the queue and execution is immediately continued without waiting till the async task is finished - async task will be executed at some in future.

Sync tasks are added to the queue and execution will be continued after thing tasks is finished.

09.Explain MVC architecture in iOS.

Model-View-Controller (MVC):

  • Model: Manages the data and business logic.
  • View: Handles the UI and presentation.
  • Controller: Mediates between Model and View, handling updates and user input. This helps separate concerns and maintain cleaner code.
10.When would you use guard/guard let vs if/if let in Swift?

Best practices:

  • if/if let statements are used to unwrap optional or checks inside the scope that are not influencing on general execution flow of a current scope
  • guard/guard let statements are used to explicitly check the conditions to understand if the execution should be continued. If condition in guard/guard let statement fails in most cases it means that the current scope should be exited.
11.What is Codable in Swift and how do you use it?

Codable is a protocol that combines Encodable and Decodable. It allows easy encoding/decoding of custom data types into/from formats like JSON.

Example:

struct User: Codable {
let name: String
let age: Int
}

// Encoding
let user = User(name: "John", age: 25)
if let encoded = try? JSONEncoder().encode(user) {
// Decoding
let decodedUser = try? JSONDecoder().decode(User.self, from: encoded)
}
12.How do you handle errors in Swift?

Swift uses the throw, try, and catch keywords to handle errors. Functions can declare that they throw errors, which must be handled by callers.

Example:

enum DataError: Error {
case noData
}

func loadData() throws -> String {
throw DataError.noData
}

do {
let data = try loadData()
} catch {
print("Error: (error)")
}
13.What is a lazy stored property?

A lazy stored property is not calculated until the first time it’s accessed. It’s useful for delaying costly initializations until they’re actually needed.

Example:

class DataManager {
lazy var data = expensiveDataLoad() // `data` is only loaded when first accessed
func expensiveDataLoad() -> [String] { return ["Item1","Item2"] }
}
14.Explain URLSession and its usage.

URLSession is a framework/API that provides a set of classes and methods for configuring and managing network sessions. It gives a possibility to fetch data from a server, upload and download media and content files, and handles WebSocket communication.

Example:

let url = URL(string: "https://api.example.com/data")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// Handle response
}
task.resume()
15.What is Grand Central Dispatch (GCD)?

GCD is an API in iOS for managing concurrent operations. It allows you to dispatch tasks to different queues (main, global, custom) and handle parallelism easily.

Example:

DispatchQueue.global().async {
// Background task
DispatchQueue.main.async {
// Update UI on main thread
}
}
16.How do you localize an iOS application?

Localization involves providing different versions of your app’s text and resources for different languages and regions.

You use Localizable.strings files, .stringsdict, and setting the development language. In code, you use NSLocalizedString(key, comment: "") or String(localized: "", comment: “”)

17.Explain what Autolayout is.

Autolayout is a system that lets you define rules (constraints) for how views are positioned and sized relative to each other and their container.

It dynamically adjusts layouts for different screen sizes and orientations.

18.What is the difference between frame and bounds of a UIView?
  • frame: The view’s location and size in the coordinate system of its superview.
  • bounds: The view’s location and size in its own coordinate space.
19.How do you trigger layout updates in iOS?

You can call methods like setNeedsLayout() or layoutIfNeeded() on views. setNeedsLayout() marks the view as needing a layout update, and layoutIfNeeded() forces an immediate layout pass if necessary.

20.What is a UITableView and how is it used?

UITableView is a UIKit component that displays a list of cells in a single column, scrolling vertically. You provide data via its dataSource and handle user interaction via its delegate.

Example (basic):

class ViewController: UIViewController, UITableViewDataSource {
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int { return 10 }
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = "Row (indexPath.row)"
return cell
}
}
21.What is a UICollectionView and
how does it differ from UITableView?

A UICollectionView is similar to UITableView but provides a more flexible layout system. Instead of a single column list, it can arrange items in a grid or custom layouts.

It uses a UICollectionViewLayout object to define how items are laid out.

22.What is the purpose of AppDelegate?

AppDelegate (or UIApplicationDelegate) responds to app-level events such as app launching, entering background, receiving notifications, etc. It’s the central point for handling global application states.

23.How do you store user preferences in iOS?

You can use UserDefaults to store small pieces of user-related data like settings or preferences.

Example:

UserDefaults.standard.set(true, forKey: "isLoggedIn")
let loggedIn = UserDefaults.standard.bool(forKey: "isLoggedIn")
24.Explain the concept of Core Data.

Core Data is Apple’s framework for object graph management and persistence. It lets you model data using entities and attributes, and store them in a SQLite database or in-memory behind the scenes.

It simplifies data management, relationships, and querying.

25.What are @IBOutlet and @IBAction?
  • @IBOutlet: A reference to a UI element in a storyboard or XIB file, allowing code to access and manipulate it.
  • @IBAction: A method triggered by a UI element event, like a button press, connecting UI events to code.
26.What is a Navigation Controller?

A UINavigationController manages a stack of view controllers. It provides navigation between screens via a navigation bar and back button.

You push new view controllers onto the stack and pop them to go back.

27.How do you handle memory warnings in iOS?

In modern iOS development, explicit memory warnings are less common. When they occur, UIApplicationDelegate’s applicationDidReceiveMemoryWarning() or a view controller’s didReceiveMemoryWarning() might be called. You should release any unneeded resources or cached data.

28.What is the difference between init() and convenience init() in Swift?
  • init(): A designated initializer that fully initializes all properties of a class.
  • convenience init(): A secondary initializer that must call another designated initializer. Convenience initializers often provide additional configuration options or simplified initialization paths.
29.Discuss handling of concurrency with async/await in Swift.

async/await provides a structured concurrency model. async functions allow suspending execution until async tasks finish, making code more readable:

func fetchData() async -> Data {
let (data, _) = try! await URLSession.shared.data(from: URL(string: "https://example.com")!)
return data
}

This avoids callback pyramids and makes asynchronous code more synchronous-looking.

30.How to prevent retain cycles in closures and delegates?
  • Use [weak self] in closure capture lists to avoid strong reference cycles.
  • Delegates should be weak references.Example:
class MyClass {
var callback: (() -> Void)?
func setup() {
callback = { [weak self] in
self?.doSomething()
}
}
}
31.Explain the difference between synchronous, asynchronous, and concurrent operations.
  • Synchronous: Tasks run one after another, waiting for each to finish.
  • Asynchronous: Tasks can begin and return immediately, freeing the caller to do other work. The task finishes later.
  • Concurrent: Multiple tasks run at the same time (in parallel if hardware allows).

Comfortable at this level? You already clear our first screening bar.

Middle iOS devs on Lemon.io work long-term with companies in the US and Western Europe — no bidding, first offer in ~13 days.

Apply now

Senior iOS questions

5–8 years. Architecture, performance, and Node — the level most Lemon.io clients hire at.

01.Explain the concept and use of DispatchWorkItem in GCD.

DispatchWorkItem allows you to create a block of code you can dispatch to a queue, cancel before execution, or notify when complete. This provides more control over asynchronous tasks.

Example:

let workItem = DispatchWorkItem {
// Heavy computation
}
DispatchQueue.global().async(execute: workItem)
// Later you can cancel if needed
workItem.cancel()
02.How does Swift’s memory management differ from Objective-C’s?
  • Swift uses ARC for both reference types and provides automatic memory management for most scenarios.
  • Swift enforces safe memory practices (e.g., no dangling pointers) and provides options/guarding to prevent nil references.
  • Objective-C uses ARC but still relies on pointers and has more legacy memory management patterns (weak references with special qualifiers, etc.).
03.What are the performance considerations when using Core Data?
  • Use NSFetchedResultsController for efficiently managing large data sets.
  • Avoid fetching all data at once; use predicates and fetch limits.
  • Batch updates and batch deletes reduce memory usage and improve performance.
  • Consider indexing frequently queried attributes.
04.Explain how Operations and OperationQueues differ from GCD.

OperationQueues/Operation: Higher-level abstraction on top of GCD. Operations can be paused, canceled, have dependencies, and provide more control over concurrency and execution states.

05.What are escaping and non-escaping closures in Swift?
  • Non-escaping closures: The closure is guaranteed to be executed before the function returns. No need for @escaping.
  • Escaping closures: The closure may be stored and executed later after the function returns. Must be marked with @escaping.
06.Explain what the resultBuilder feature in Swift does.

A result builder (e.g., @resultBuilder or @ViewBuilder in SwiftUI) is a feature that lets you build complex data structures from a sequence of statements. It transforms imperative-looking code into a declarative structure. Commonly used in SwiftUI to build UI hierarchies.

07.Describe the difference between Main and Global queues in GCD.
  • Main queue: Serialized queue that runs on the main thread, used for UI updates.
  • Global queues: Concurrent queues for background work. They come in different priorities: .userInteractive, .userInitiated, .utility, and .background.
08.How would you implement dependency injection in Swift?

Use initializers or property injection to provide dependencies. Protocols can define what a dependency looks like. Constructor injection is common:

Example:

protocol DataService {
func fetchData() -> [String]
}

class MyViewModel {
private let service: DataService
init(service: DataService) {
self.service = service
}
}
09.Compare MVP, MVVM, and VIPER architectures.
  • MVP: Model-View-Presenter separates business logic from UI. Presenter updates the View.
  • MVVM: Model-View-ViewModel introduces a ViewModel that binds data to the View, often using data binding.
  • VIPER: View-Interactor-Presenter-Entity-Router splits responsibilities into layers, aiming for a very modular and testable architecture.
10.How to optimize table view performance?
  • Use reuse identifiers and dequeue reusable cells.
  • Avoid complex layouts; consider pre-sizing or using Auto Layout efficiently.
  • Use cell prefetching.
  • Offload heavy tasks off the main thread, such as image loading.
11.Explain URLSession configuration and how to use it for caching and custom headers.

URLSession can be configured with URLSessionConfiguration. For caching:

let config = URLSessionConfiguration.default
config.requestCachePolicy = .returnCacheDataElseLoad
// custom headers
config.httpAdditionalHeaders = ["Authorization": "Bearer xyz"]
let session = URLSession(configuration: config)

This allows customizing timeouts, caching policies, headers, and more.

12.How do you implement offline caching for network requests in iOS?
  • Use URLCache with appropriate cache policies.
  • Store JSON responses or other data in Core Data or local database.
  • Use NSKeyedArchiver/Codable to save objects to disk.
  • Implement a strategy to determine when to serve cached data vs. fresh data.
13.What are property wrappers in Swift and give an example?

Property wrappers allow you to define reusable logic for getter/setter and apply it to properties.

Example:

@propertyWrapper
struct Capitalized {
private var value: String
init(wrappedValue: String) {
self.value = wrappedValue.capitalized
}
var wrappedValue: String {
get { value }
set { value = newValue.capitalized }
}
}

struct User {
@Capitalized var name: String
}

let user = User(name: "john")
print(user.name) // "John"
14.What is the difference between NSLock, DispatchSemaphore,
and DispatchGroup?
  • NSLock: A mutex lock for critical sections.
  • DispatchSemaphore: Controls access to a resource with a given number of permits.
  • DispatchGroup: Allows grouping of multiple tasks and notifying when all have completed.
15.Discuss testing strategies for iOS apps
  • Unit Testing: Test individual components (business logic) using XCTest.
  • UI Testing: Automate UI interactions and verify results using XCUITest.
  • Snapshot Testing: Compare rendered UI to a reference image.
  • Mocking & Dependency Injection: Helps isolate code and test behavior under controlled conditions.
16.How do you handle migration in Core Data?

Use Core Data’s migration system to move from one data model version to another. Lightweight migrations can be done automatically by setting the correct options when loading the persistent store.

For complex migrations, create a mapping model or use a custom migration policy.

17.Explain KVC and KVO in Swift.
  • Key-Value Coding (KVC): Accessing object properties using strings (keys), typically value(forKey:) and setValue(_:forKey:).
  • Key-Value Observing (KVO): Observing changes to a property value. Allows objects to be notified when a specific property changes.
18.When would you consider using SwiftUI over UIKit?

SwiftUI is used for building declarative UIs, providing a more modern approach to UI development, better code previews, and seamless integration with new Apple platforms.

Consider SwiftUI for new projects or rapid UI development. UIKit still excels in complex, legacy projects or when needing lower-level control.

19.How do you implement push notifications and handle background updates?
  • Register for notifications with UNUserNotificationCenter.
  • Implement UNUserNotificationCenterDelegate methods to handle incoming notifications.
  • Use background modes in the app’s capabilities to update data silently.
  • Implement application(_:didReceiveRemoteNotification:fetchCompletionHandler:) to handle background updates.
20.Describe how you would use Instruments to profile an iOS app.
  • Time Profiler: Measure CPU usage and identify performance bottlenecks.
  • Allocations: Track memory usage and detect leaks.
  • Leaks & Zombies: Identify memory leaks and over-released objects.
  • Network: Monitor network requests. Run the app through Instruments, record a session, and analyze the data to optimize performance.
21.What are Combine publishers and subscribers?

Combine is Apple’s reactive framework.

  • Publishers: Emit values over time. Examples: URLSession.DataTaskPublisher, Just, PassthroughSubject.
  • Subscribers: Receive and handle emitted values. You chain publishers and operators to transform data, and subscribers handle the final output.
22.How would you implement Secure Coding and Keychain storage?
  • Use Keychain for sensitive data (passwords, tokens). Apple provides Keychain Services API.
  • Use SecItemAdd, SecItemCopyMatching, SecItemUpdate, and SecItemDelete.
  • Implement Codable for secure coding and ensure data at rest is encrypted if needed.
23.Explain diffable data sources.

Diffable data sources were introduced to simplify UI updates. They use a snapshot of data to smoothly animate changes in collection/table views.

Instead of manually inserting/deleting rows, you apply a snapshot that calculates changes automatically.

24.Discuss the role of SceneDelegate.

Introduced in iOS 13, UISceneDelegate manages UI scenes in multi-window environments (e.g., iPad). Each scene can have its own lifecycle, state restoration, and UI independent of others.

25.How do you ensure thread-safety in Swift code?
  • Use serial queues for tasks that must not run concurrently.
  • Use locks (NSLock), semaphores, or dispatch barriers for critical sections.
  • Make mutable state isolated to a single queue.
  • Use atomics or @Sendable closures with Swift’s concurrency features.
26.How do you implement custom animations using
UIViewPropertyAnimator?

UIViewPropertyAnimator provides fine-grained control over animations, allowing you to pause, resume, and scrub through animations.

Example:

let animator = UIViewPropertyAnimator(duration: 1.0, curve: .easeInOut) {
view.transform = CGAffineTransform(scaleX: 2.0, y: 2.0)
}
animator.startAnimation()
// Later
animator.pauseAnimation()
animator.fractionComplete = 0.5 // jump to half
animator.continueAnimation(withTimingParameters: nil, durationFactor: 1.0)
27.Explain the difference between weak and unowned references in Swift.
  • Both weak and unowned references are not increasing the retain count
  • weak references are used when the referenced object can become nil at some point. weak references are always optional and automatically set to nil when the object is deallocated.
  • unowned references are used when the referenced object is never expected to be nil once initialized. Using unowned is a promise that the reference will always point to a valid object. If the object is deallocated and accessed, it leads to a runtime crash.

If this section felt routine — you’re senior. Land projects that prove it.

Full-time or part-time roles with companies in the US and Western Europe. You write the code, we own the admin: contracts, invoicing, all of it.

Apply now

iOS Coding Interview Questions

8+ years / lead level. System design, security, and the judgment calls that separate strong seniors — what our clients pay top rates for.

01.Create a SwiftUI View with a Button and Label

Possible solution:

import SwiftUI

struct ContentView: View {
@State private var message = "Hello, Lemon!"

var body: some View {
VStack {
Text(message)
.font(.title)
Button("Tap Me") {
message = "Button Pressed!"
}
.padding()
}
}
}
02.Fetch and Decode JSON Using Async/Await

Possible solution:

import Foundation

struct User: Codable {
let id: Int
let name: String
}

func fetchUsers() async throws -> [User] {
let url = URL(string: "https://jsonplaceholder.typicode.com/users")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([User].self, from: data)
}

do {
let users = try await fetchUsers()
print(users)
} catch {
print("Failed to fetch users:", error)
}
03.Filter a List Using Combine

Possible solution:

import Combine

let names = ["Alice", "Bob", "Charlie", "David", "Eve"]
let searchPublisher = PassthroughSubject<String, Never>()

var cancellable = searchPublisher
.map { query in
names.filter { $0.lowercased().contains(query.lowercased()) }
}
.sink { filteredNames in
print("Filtered Names:", filteredNames)
}

searchPublisher.send("a") // Filters names containing "a"
searchPublisher.send("e") // Filters names containing "e"
04.Store and Retrieve Data Using UserDefaults

Possible solution:

import Foundation

UserDefaults.standard.set("Hello, Lemon!", forKey: "savedMessage")
let message = UserDefaults.standard.string(forKey: "savedMessage") ?? "No message found"
print(message)
05.Create and Use a Timer

Possible solution:

import Foundation


var counter = 0
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
    counter += 1
    print("Counter:", counter)
    
    if counter == 5 {
    timer.invalidate() // Stop after 5 seconds
}
}
06.Detect Device Orientation Changes (Simulated)

Possible solution:

import UIKit


func checkOrientation() {
    let orientation = UIDevice.current.orientation
    switch orientation {
    case .portrait:
    print("Portrait")
    case .landscapeLeft, .landscapeRight:
    print("Landscape")
    default:
    print("Unknown orientation") // Fallback if execution is in Playground
    }
}


checkOrientation()
07.Implement a generic stack data structure with push, pop, and peek operations.

Possible solution:

struct LemonStack {
private var elements: [T] = []

mutating func push(_ item: T) {
elements.append(item)
}

mutating func pop() -> T? {
return elements.popLast()
}

func peek() -> T? {
return elements.last
}
}

var stack = LemonStack()
stack.push(1)
stack.push(2)
print(stack.pop() ?? "Stack is empty") // Prints 2
print(stack.peek() ?? "Stack is empty") // Prints 1
08.Implement a Simple Observer Pattern

Possible solution:

import Foundation

class LemonObservable {
private var observers = [() -> Void]()

func addObserver(_ observer: @escaping () -> Void) {
observers.append(observer)
}

func notifyObservers() {
observers.forEach { $0() }
}
}

let observable = LemonObservable()
observable.addObserver {
print("Observer 1 Notified")
}
observable.addObserver {
print("Observer 2 Notified")
}

observable.notifyObservers()
09.Create a Simple Async Function with Delayed Execution

Possible solution:

import Foundation

let delayTime: UInt64 = 3_000_000_000 // In nanoseconds, equals 3 seconds

func delayedTask() async {
print("Task started")
try? await Task.sleep(nanoseconds: delayTime)
print("Task completed after delay")
}

Task {
await delayedTask()
}
10.Use Gesture Recognizers in a UIView (UIKit)

Possible solution:

import UIKit

class LemonGestureView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)

self.backgroundColor = .white
let tap = UITapGestureRecognizer(target: self,
action: #selector(handleTap))
self.addGestureRecognizer(tap)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

@objc func handleTap() {
self.backgroundColor = self.backgroundColor == .white ? .blue : .white
print("Tapped! Background changed")
}
}

let view = LemonGestureView(frame: CGRect(x: 0.0,
y: 0.0,
width: 300.0,
height: 300.0))

Cruising through these? Companies on Lemon.io are looking for exactly you.

Own projects across AI, fintech, healthcare and more — with companies you want on your resume.

Apply now

See how your interview answers compare with top candidates

Answer six real questions, compare your approach, and find out where you stand.

Start the self-check

Interview questions quiz

Tough vetting by us —
real projects for you

Pre-screening

CV and LinkedIn review, a screening call with our AI assistant, and a role-specific technical task.

~37%

Recruiter interview

Assesses interpersonal skills, pressure handling, ambiguity, and client expectations.

~17%

Technical interview

A live problem-solving session with a trusted senior engineer from our community. Peer cross-reviewed.

~7%

Listed on Lemon.io

Roughly 1 in 100 applicants makes it to the platform, with every skill and stack validated.

~1%

Hear from our developers

Rated 5 out of 5 on Trustpilot

I’ve been able to work from the Philippines, all over Europe, and Brazil without missing a single project, learning a ton of different technologies.

Ivan PratsIvan PratsSenior Full-Stack Developer
Rated 5 out of 5 on Trustpilot

One of the best things about Lemon is the opportunities you get. They have the connections, the clients, new companies who are constantly looking for engineers in different stacks.

Sam OykeyeSam OykeyeSenior Full-Stack Developer
Rated 5 out of 5 on Trustpilot

I’ve been working with Lemon since 2021, building projects across healthcare, travel, ecommerce, and fintech. I really appreciate the team’s support and truly believe this company is unique.

Viktoria BohomazViktoria BohomazFull-Stack Developer

No bidding, no negotiating, no late payments. Just the work you want exists. Let's match you to it.

Companies in the US & Western Europe · full-time or part-time · payments via bank transfer, Payoneer, or Wise.

Apply now