iOS interviews test Swift proficiency, UIKit and SwiftUI knowledge, memory management, concurrency, architecture patterns, and platform-specific APIs. This guide covers the 50 most common questions with clear answers and code examples.
Quick reference
| Topic | Most asked questions |
|---|---|
| Swift | Optionals, closures, protocols, generics, struct vs class |
| Memory | ARC, retain cycles, weak vs unowned |
| UIKit | ViewController lifecycle, Auto Layout, delegation pattern |
| SwiftUI | @State, @Binding, @ObservableObject, view lifecycle |
| Concurrency | async/await, GCD, actors, Task |
| Architecture | MVVM, MVC, Clean Architecture, Coordinator |
| Networking | URLSession, Codable, error handling |
| Persistence | Core Data, UserDefaults, Keychain, SwiftData |
Swift fundamentals
1. What is an optional in Swift and how do you safely unwrap it?
An optional represents a value that may be absent — it is either some(Value) or none. Swift forces you to handle the absence explicitly before using the value.
var name: String? = "Alice"
// 1. Optional binding (preferred)
if let name = name {
print("Hello, \(name)")
}
// 2. Guard let (early exit)
func greet(_ name: String?) {
guard let name = name else { return }
print("Hello, \(name)")
}
// 3. Nil coalescing
let display = name ?? "Unknown"
// 4. Optional chaining
let length = name?.count // Int? — nil if name is nil
// 5. Forced unwrap — only when nil is impossible
let forced = name! // crashes if nil — avoid in production
Never use ! unless you are 100% certain the value exists (e.g., IBOutlets after viewDidLoad).
2. What is the difference between struct and class in Swift?
| Feature | struct (value type) |
class (reference type) |
|---|---|---|
| Memory | Stack (usually) | Heap |
| Copying | Copy on assignment | Shared reference |
| Inheritance | No | Yes (single) |
| Deinitialiser | No | Yes (deinit) |
| Mutability | mutating methods |
Methods mutate freely |
| ARC | Not managed by ARC | Managed by ARC |
| Thread safety | Copies are independent | Shared state — needs sync |
struct Point { var x: Int; var y: Int }
class Counter { var count = 0 }
var p1 = Point(x: 0, y: 0)
var p2 = p1
p2.x = 10
print(p1.x) // 0 — independent copy
var c1 = Counter()
var c2 = c1
c2.count = 10
print(c1.count) // 10 — same object
Rule of thumb: prefer struct by default. Use class for identity semantics, inheritance, or Objective-C interop.
3. What is ARC (Automatic Reference Counting)?
ARC tracks the number of strong references to each class instance. When the count drops to zero, ARC deallocates the instance.
class Dog {
let name: String
init(name: String) { self.name = name; print("\(name) created") }
deinit { print("\(name) deallocated") }
}
var dog1: Dog? = Dog(name: "Rex") // count = 1
var dog2 = dog1 // count = 2
dog1 = nil // count = 1
dog2 = nil // count = 0 → deinit called
ARC is compile-time — there is no runtime garbage collector pause.
4. What is a retain cycle and how do you break it?
A retain cycle occurs when two objects hold strong references to each other, preventing ARC from deallocating either.
// Retain cycle
class Person {
var pet: Pet?
deinit { print("Person deallocated") }
}
class Pet {
var owner: Person? // strong — creates cycle!
deinit { print("Pet deallocated") }
}
var person: Person? = Person()
var pet: Pet? = Pet()
person?.pet = pet
pet?.owner = person
person = nil
pet = nil
// Neither deinit is called — memory leak!
Fix with weak or unowned:
class Pet {
weak var owner: Person? // weak — allows deallocation
}
| Reference | Value type | Use when |
|---|---|---|
strong |
Default | You own the object |
weak |
Optional<T> |
Reference may become nil (delegate, parent VC) |
unowned |
Non-optional | Reference will never be nil during object lifetime (e.g., child → parent that always outlives it) |
5. What are closures in Swift? How do you avoid capture cycles?
Closures are self-contained blocks that capture values from their surrounding scope.
// Closure capturing self — creates retain cycle if self holds the closure
class ViewModel {
var name = "Alice"
var onComplete: (() -> Void)?
func setup() {
onComplete = { [weak self] in // capture list
guard let self = self else { return }
print(self.name)
}
}
}
Capture list rules:
[weak self]— self becomes optional; use when the closure may outlive self[unowned self]— non-optional; use when the closure is guaranteed to not outlive self (crashes if self is nil)
6. What is a protocol in Swift? How does it differ from an abstract class?
A protocol defines a blueprint of methods, properties, and requirements. Unlike abstract classes in other languages, any type (class, struct, enum) can conform.
protocol Drawable {
var color: String { get }
func draw()
func resize(by factor: Double) // required
}
extension Drawable {
func resize(by factor: Double) { // default implementation
print("Default resize \(factor)x")
}
}
struct Circle: Drawable {
var color = "red"
func draw() { print("Drawing circle") }
}
Protocol vs abstract class:
| Protocol | Abstract class | |
|---|---|---|
| Multiple conformance | Yes | No (single inheritance) |
| Value types | Yes (struct/enum) | No |
| Stored properties | No | Yes |
| Default implementation | Yes (extension) | Yes |
7. What are generics in Swift?
Generics let you write flexible, reusable functions and types that work with any type satisfying given constraints.
// Generic function
func swap<T>(_ a: inout T, _ b: inout T) {
let temp = a; a = b; b = temp
}
// Generic type with constraint
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) { items.append(item) }
mutating func pop() -> Element? { items.popLast() }
var top: Element? { items.last }
}
// Protocol constraint
func largest<T: Comparable>(_ array: [T]) -> T? {
array.max()
}
8. What is the difference between map, filter, and flatMap?
let numbers = [1, 2, 3, 4, 5]
// map — transform each element
let doubled = numbers.map { $0 * 2 } // [2, 4, 6, 8, 10]
// filter — keep elements satisfying predicate
let evens = numbers.filter { $0 % 2 == 0 } // [2, 4]
// reduce — aggregate
let sum = numbers.reduce(0, +) // 15
// flatMap — transform + flatten one level
let nested = [[1, 2], [3, 4]]
let flat = nested.flatMap { $0 } // [1, 2, 3, 4]
// compactMap — transform + remove nils
let strings = ["1", "two", "3"]
let ints = strings.compactMap { Int($0) } // [1, 3]
9. What is @escaping closure?
A closure marked @escaping can outlive the function it was passed to — it is stored and called later.
var completionHandlers: [() -> Void] = []
func addHandler(_ handler: @escaping () -> Void) {
completionHandlers.append(handler) // stored — outlives function
}
func fetchData(completion: @escaping (Data?) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, _ in
completion(data) // called after function returns
}.resume()
}
Non-escaping (default) closures are called synchronously within the function body.
10. What are enum associated values and how are they used?
enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
enum NetworkError: Error {
case notFound
case serverError(statusCode: Int, message: String)
case timeout
}
func handle(_ error: NetworkError) {
switch error {
case .notFound:
print("404")
case .serverError(let code, let msg):
print("Server \(code): \(msg)")
case .timeout:
print("Timed out")
}
}
UIKit
11. What is the UIViewController lifecycle?
init(coder:) / init(nibName:bundle:)
↓
loadView() — creates view hierarchy (don't call super if overriding)
↓
viewDidLoad() — view loaded; set up subviews, bind data, register cells
↓
viewWillAppear(_:) — about to become visible (may fire multiple times)
↓
viewDidAppear(_:) — visible; start animations, timers
↓
viewWillDisappear(_:)
↓
viewDidDisappear(_:) — stop timers, save state
↓
deinit — remove observers
| Method | Common use |
|---|---|
viewDidLoad() |
One-time setup, add subviews |
viewWillAppear(_:) |
Refresh data, show/hide elements |
viewDidAppear(_:) |
Start animations, analytics events |
viewWillDisappear(_:) |
Save unsaved data |
viewDidDisappear(_:) |
Stop timers, pause video |
12. What is Auto Layout? Explain constraint priority.
Auto Layout resolves ambiguous or conflicting constraint sets by assigning a constraint to each view's position and size.
Priorities (1–1000):
required= 1000 — must be satisfied (crash if not)defaultHigh= 750 — used for content compression resistancedefaultLow= 250 — used for content hugging
// Programmatic constraints
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16)
])
// Content hugging — resist expanding beyond intrinsic size
label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
// Compression resistance — resist shrinking below intrinsic size
label.setContentCompressionResistancePriority(.required, for: .horizontal)
13. What is the delegation pattern in UIKit?
Delegation lets one object act on behalf of another. The delegating object holds a weak reference to its delegate to avoid retain cycles.
protocol ImagePickerDelegate: AnyObject {
func imagePicker(_ picker: ImagePickerController, didSelect image: UIImage)
}
class ImagePickerController: UIViewController {
weak var delegate: ImagePickerDelegate? // weak!
func userSelectedImage(_ image: UIImage) {
delegate?.imagePicker(self, didSelect: image)
}
}
class ProfileViewController: UIViewController, ImagePickerDelegate {
func imagePicker(_ picker: ImagePickerController, didSelect image: UIImage) {
profileImageView.image = image
}
}
14. What is the difference between frame and bounds?
| Property | Coordinate space | What it describes |
|---|---|---|
frame |
Superview's coordinate system | Position and size relative to parent |
bounds |
View's own coordinate system | Internal origin (usually 0,0) and size |
center |
Superview's coordinate system | Center point |
let view = UIView(frame: CGRect(x: 50, y: 100, width: 200, height: 100))
print(view.frame.origin) // (50, 100)
print(view.bounds.origin) // (0, 0) — own coordinate space
Scroll views manipulate bounds.origin to achieve scrolling.
15. How does UITableView work? What is cell reuse?
UITableView uses a reuse queue to avoid creating a new cell for every row. Cells that scroll off-screen are placed in the queue and recycled.
class MyCell: UITableViewCell {
static let reuseID = "MyCell"
// subviews...
}
// Register
tableView.register(MyCell.self, forCellReuseIdentifier: MyCell.reuseID)
// Dequeue
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: MyCell.reuseID, for: indexPath) as! MyCell
let item = items[indexPath.row]
cell.configure(with: item) // always reset all properties — cell may be reused
return cell
}
Gotcha: Never set properties on a cell without resetting them — the cell may carry state from a previous row.
16. What is prepareForReuse() used for?
Called by the table/collection view before dequeueing a cell for reuse. Reset any visual state that is set in cellForRowAt:
override func prepareForReuse() {
super.prepareForReuse()
imageView?.image = nil // cancel image load
titleLabel.text = nil
badgeView.isHidden = true
// cancel any async tasks started in configure()
}
SwiftUI
17. What are the main property wrappers in SwiftUI?
| Wrapper | Scope | Purpose |
|---|---|---|
@State |
Local to view | Simple value owned by view |
@Binding |
Passed from parent | Two-way connection to parent state |
@ObservableObject / @StateObject |
External class | Reference type with @Published properties |
@EnvironmentObject |
Environment | Shared across view hierarchy |
@Environment |
Built-in values | colorScheme, dismiss, locale |
@AppStorage |
UserDefaults | Persist simple values |
@FetchRequest |
Core Data | Fetch managed objects |
struct CounterView: View {
@State private var count = 0 // local state
var body: some View {
VStack {
Text("Count: \(count)")
Button("+") { count += 1 }
}
}
}
18. What is the difference between @StateObject and @ObservedObject?
| Wrapper | Owns the object | Use when |
|---|---|---|
@StateObject |
Yes — view creates and owns it | Creating the object in the view |
@ObservedObject |
No — receives from outside | Object passed in as parameter |
class ViewModel: ObservableObject {
@Published var title = "Hello"
}
// Parent creates and owns — use @StateObject
struct ParentView: View {
@StateObject private var vm = ViewModel()
var body: some View {
ChildView(vm: vm)
}
}
// Child receives — use @ObservedObject
struct ChildView: View {
@ObservedObject var vm: ViewModel
var body: some View { Text(vm.title) }
}
Never use @ObservedObject to create an object — it will be recreated on every re-render.
19. What causes a SwiftUI view to re-render?
A view re-renders when:
@Stateproperty changes@ObservableObject's@Publishedproperty emits@EnvironmentObjectchanges- Parent view re-renders and passes new values
SwiftUI diffs the view tree and only re-renders views whose inputs changed. Use equatable() or split views to minimise re-renders.
// Avoid expensive recomputation on every render
struct ExpensiveView: View {
let items: [Item]
var body: some View {
List(items) { item in
ItemRow(item: item) // only re-renders if item changes
}
}
}
20. What is @Binding and when do you use it?
@Binding creates a two-way connection between a child view and its parent's state without transferring ownership.
struct ToggleRow: View {
@Binding var isOn: Bool // receives reference to parent state
var body: some View {
Toggle("Enable", isOn: $isOn)
}
}
struct SettingsView: View {
@State private var notificationsEnabled = false
var body: some View {
ToggleRow(isOn: $notificationsEnabled) // pass binding with $
}
}
21. What are LazyVStack and LazyHStack?
Lazy stacks create child views only as they become visible on screen, making them suitable for large data sets.
ScrollView {
LazyVStack {
ForEach(0..<1_000) { index in
Text("Row \(index)") // created only when visible
}
}
}
Use LazyVStack/LazyHStack inside ScrollView for long lists. Prefer List for standard table-like content as it handles separators, swipe actions, and selection.
22. What is SwiftUI's Task and onAppear?
Task launches a Swift concurrency task tied to the view's lifecycle. .onAppear runs code when the view appears.
struct UserView: View {
@State private var user: User?
var body: some View {
Group {
if let user = user {
Text(user.name)
} else {
ProgressView()
}
}
.task { // preferred for async work
user = await fetchUser()
}
// .onAppear { } — for synchronous setup
}
}
.task automatically cancels when the view disappears, unlike .onAppear.
Concurrency
23. What is GCD (Grand Central Dispatch)?
GCD dispatches work on thread pools without you managing threads directly.
// Main queue — UI updates
DispatchQueue.main.async {
self.label.text = "Done"
}
// Background queue — heavy work
DispatchQueue.global(qos: .userInitiated).async {
let data = processLargeFile()
DispatchQueue.main.async {
self.display(data) // back to main for UI
}
}
// After delay
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
showBanner()
}
QoS levels (high → low priority): .userInteractive > .userInitiated > .default > .utility > .background.
24. What is async/await in Swift?
Swift concurrency (Swift 5.5+) lets you write asynchronous code in a linear style without callbacks.
// Mark function as async
func fetchUser(id: Int) 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)
}
// Call with await
func loadProfile() async {
do {
let user = try await fetchUser(id: 42)
print(user.name)
} catch {
print("Error:", error)
}
}
// Run from synchronous context
Task {
await loadProfile()
}
25. What is an Actor in Swift?
An actor protects mutable state from concurrent access. All access to an actor's properties is serialised, preventing data races.
actor BankAccount {
private var balance: Double = 0
func deposit(_ amount: Double) {
balance += amount
}
func withdraw(_ amount: Double) throws {
guard balance >= amount else { throw BankError.insufficient }
balance -= amount
}
var currentBalance: Double { balance }
}
let account = BankAccount()
Task { await account.deposit(100) }
Task { try? await account.withdraw(30) }
@MainActor ensures code runs on the main thread — apply to ViewModels updating UI.
@MainActor
class ProfileViewModel: ObservableObject {
@Published var user: User?
func load() async {
user = await fetchUser() // UI update guaranteed on main thread
}
}
26. What is async let and TaskGroup?
async let runs multiple async tasks concurrently:
async let user = fetchUser(id: 1)
async let posts = fetchPosts(userId: 1)
let (u, p) = try await (user, posts) // both run concurrently
TaskGroup handles dynamic numbers of concurrent tasks:
let results = try await withThrowingTaskGroup(of: User.self) { group in
for id in ids {
group.addTask { try await fetchUser(id: id) }
}
var users: [User] = []
for try await user in group {
users.append(user)
}
return users
}
Networking
27. How do you make a network request with URLSession?
struct User: Codable {
let id: Int
let name: String
let email: String
}
func fetchUser(id: Int) async throws -> User {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/users/\(id)") else {
throw URLError(.badURL)
}
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(User.self, from: data)
}
28. What is Codable in Swift?
Codable is a type alias for Encodable & Decodable. Swift synthesises the implementation when property names match JSON keys.
struct Article: Codable {
let id: Int
let title: String
let publishedAt: Date // needs dateDecodingStrategy
enum CodingKeys: String, CodingKey {
case id
case title
case publishedAt = "published_at" // custom key mapping
}
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
decoder.keyDecodingStrategy = .convertFromSnakeCase // auto snake_case mapping
let article = try decoder.decode(Article.self, from: jsonData)
// Encode back to JSON
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try encoder.encode(article)
29. How do you handle errors from a network request?
enum APIError: Error, LocalizedError {
case invalidURL
case httpError(statusCode: Int)
case decodingError(Error)
case networkError(Error)
var errorDescription: String? {
switch self {
case .invalidURL: return "Invalid URL"
case .httpError(let code): return "HTTP \(code)"
case .decodingError(let e): return "Decoding: \(e.localizedDescription)"
case .networkError(let e): return e.localizedDescription
}
}
}
func fetch<T: Decodable>(_ type: T.Type, from url: URL) async -> Result<T, APIError> {
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard let http = response as? HTTPURLResponse else {
return .failure(.networkError(URLError(.badServerResponse)))
}
guard (200...299).contains(http.statusCode) else {
return .failure(.httpError(statusCode: http.statusCode))
}
do {
let decoded = try JSONDecoder().decode(T.self, from: data)
return .success(decoded)
} catch {
return .failure(.decodingError(error))
}
} catch {
return .failure(.networkError(error))
}
}
Architecture
30. What is the MVC pattern in iOS?
| Component | Responsibility | Example |
|---|---|---|
| Model | Data and business logic | User, APIService |
| View | Display and user interaction | UITableViewCell, Storyboard |
| Controller | Mediates between Model and View | UIViewController |
Problem: ViewControllers tend to become "Massive ViewControllers" because UIKit blends controller and view concerns. MVVM addresses this.
31. What is MVVM in iOS?
| Component | Responsibility |
|---|---|
| Model | Data structures and business logic |
| View | UI — renders ViewModel output, sends user actions |
| ViewModel | Transforms model data for display; does not import UIKit |
// Model
struct User: Codable {
let id: Int
let name: String
}
// ViewModel
@MainActor
class UserViewModel: ObservableObject {
@Published var displayName = ""
@Published var isLoading = false
@Published var errorMessage: String?
private let service: UserService
init(service: UserService = .shared) {
self.service = service
}
func load(id: Int) async {
isLoading = true
defer { isLoading = false }
do {
let user = try await service.fetchUser(id: id)
displayName = user.name.uppercased()
} catch {
errorMessage = error.localizedDescription
}
}
}
// View (SwiftUI)
struct UserView: View {
@StateObject private var vm = UserViewModel()
var body: some View {
Group {
if vm.isLoading { ProgressView() }
else { Text(vm.displayName) }
}
.task { await vm.load(id: 1) }
}
}
32. What is the Coordinator pattern?
Coordinators handle navigation logic, keeping ViewControllers free of routing code.
protocol Coordinator: AnyObject {
var childCoordinators: [Coordinator] { get set }
var navigationController: UINavigationController { get }
func start()
}
class AppCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
let navigationController: UINavigationController
init(nav: UINavigationController) {
navigationController = nav
}
func start() {
let vc = HomeViewController()
vc.coordinator = self
navigationController.pushViewController(vc, animated: false)
}
func showDetail(for item: Item) {
let vc = DetailViewController(item: item)
navigationController.pushViewController(vc, animated: true)
}
}
33. What is dependency injection in iOS?
Dependency injection passes dependencies into an object rather than having it create them internally.
// Without DI — hard to test
class LoginViewModel {
let service = AuthService() // hard dependency
}
// With DI — testable
class LoginViewModel {
let service: AuthServiceProtocol
init(service: AuthServiceProtocol = AuthService()) {
self.service = service
}
}
// Test
class MockAuthService: AuthServiceProtocol {
var loginResult: Result<User, Error> = .success(User.mock)
func login(email: String, password: String) async throws -> User {
return try loginResult.get()
}
}
let vm = LoginViewModel(service: MockAuthService())
Data Persistence
34. When do you use UserDefaults vs Keychain vs Core Data?
| Storage | Use for | Encrypted | Max size |
|---|---|---|---|
UserDefaults |
Preferences, feature flags, small primitives | No (plist) | Small |
Keychain |
Passwords, tokens, biometric keys | Yes | Small |
Core Data |
Structured relational data, large datasets | Optional (NSFileProtection) | Large |
| Files | Images, documents, audio | Optional | Large |
SwiftData |
Core Data replacement (iOS 17+) | Optional | Large |
// UserDefaults
UserDefaults.standard.set("dark", forKey: "theme")
let theme = UserDefaults.standard.string(forKey: "theme") ?? "light"
// Keychain (using Security framework or KeychainAccess library)
import Security
let data = token.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "userToken",
kSecValueData as String: data
]
SecItemAdd(query as CFDictionary, nil)
35. What is Core Data?
Core Data is Apple's object graph and persistence framework. It provides:
- SQLite, binary, or in-memory storage
- Managed object context (MOC) as a scratchpad
- NSFetchRequest for querying
- Undo/redo support
// Fetch
let request: NSFetchRequest<Article> = Article.fetchRequest()
request.predicate = NSPredicate(format: "isRead == false")
request.sortDescriptors = [NSSortDescriptor(keyPath: \Article.date, ascending: false)]
let articles = try context.fetch(request)
// Create and save
let article = Article(context: context)
article.title = "New Article"
article.date = Date()
try context.save()
36. What is SwiftData (iOS 17+)?
SwiftData is a Swift-native replacement for Core Data, using macros instead of NSManagedObject subclasses.
import SwiftData
@Model
class Article {
var title: String
var date: Date
var isRead: Bool
init(title: String, date: Date = .now, isRead: Bool = false) {
self.title = title
self.date = date
self.isRead = isRead
}
}
// SwiftUI integration
struct ArticleListView: View {
@Query(sort: \Article.date, order: .reverse)
var articles: [Article]
@Environment(\.modelContext) private var context
var body: some View {
List(articles) { article in
Text(article.title)
}
}
}
iOS-specific APIs
37. What is the iOS App lifecycle?
UIKit (UIApplicationDelegate):
Not Running
↓ launch
Inactive (foreground, not receiving events)
↓
Active (foreground, receiving events)
↓ home button / lock
Background (limited time to finish work)
↓ system suspends
Suspended (in memory, not executing)
↓ memory pressure
Not Running
SwiftUI / UISceneDelegate lifecycle phases:
| Phase | When |
|---|---|
.background |
App launched but not visible |
.inactive |
Transitioning or interrupted |
.active |
Foreground and receiving events |
// SwiftUI
struct MyApp: App {
@Environment(\.scenePhase) var phase
var body: some Scene {
WindowGroup { ContentView() }
.onChange(of: phase) { newPhase in
switch newPhase {
case .active: startTimers()
case .background: saveState()
default: break
}
}
}
}
38. How do push notifications work on iOS?
App → request permission
↓
User grants → APNs registers → device token
↓
App sends token to your server
↓
Server → APNs → device
// Request permission
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in
if granted {
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}
// Receive token
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
sendToServer(token: token)
}
// Handle foreground notification
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler handler: @escaping (UNNotificationPresentationOptions) -> Void) {
handler([.banner, .sound]) // show even when app is active
}
}
39. What is background execution in iOS?
iOS limits background execution by default. Options:
| Mode | Plist key | Use case |
|---|---|---|
| Background Fetch | fetch |
Periodic data refresh |
| Remote Notifications | remote-notification |
Silent push to trigger fetch |
| Background Processing | processing |
Long-running tasks via BGProcessingTask |
| Audio, AirPlay | audio |
Music playback |
| Location updates | location |
Navigation apps |
| VoIP | voip |
Calling apps |
// BGTaskScheduler (iOS 13+)
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.app.refresh", using: nil) { task in
handleAppRefresh(task: task as! BGAppRefreshTask)
}
func scheduleRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.app.refresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
try? BGTaskScheduler.shared.submit(request)
}
40. What is Combine framework?
Combine is Apple's reactive framework for processing values over time.
import Combine
class SearchViewModel: ObservableObject {
@Published var query = ""
@Published var results: [String] = []
private var cancellables = Set<AnyCancellable>()
init() {
$query
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.filter { !$0.isEmpty }
.sink { [weak self] query in
self?.search(query: query)
}
.store(in: &cancellables)
}
func search(query: String) {
// perform search
}
}
Key operators: map, filter, flatMap, combineLatest, merge, zip, debounce, throttle, catch, retry.
Testing & Performance
41. How do you write unit tests in iOS?
import XCTest
@testable import MyApp
final class LoginViewModelTests: XCTestCase {
var sut: LoginViewModel!
var mockService: MockAuthService!
override func setUp() {
super.setUp()
mockService = MockAuthService()
sut = LoginViewModel(service: mockService)
}
override func tearDown() {
sut = nil
mockService = nil
super.tearDown()
}
func test_login_success_updatesUser() async throws {
// Given
mockService.loginResult = .success(.init(id: 1, name: "Alice"))
// When
await sut.login(email: "a@a.com", password: "pass")
// Then
XCTAssertEqual(sut.user?.name, "Alice")
XCTAssertFalse(sut.isLoading)
XCTAssertNil(sut.errorMessage)
}
func test_login_failure_setsErrorMessage() async {
// Given
mockService.loginResult = .failure(AuthError.invalidCredentials)
// When
await sut.login(email: "a@a.com", password: "wrong")
// Then
XCTAssertNil(sut.user)
XCTAssertNotNil(sut.errorMessage)
}
}
42. How do you profile memory leaks in iOS?
- Instruments → Leaks: runs the app, detects allocations that are no longer reachable
- Instruments → Allocations: heap growth, retain counts
- Memory Graph Debugger (Xcode): pause app → capture memory graph → see reference cycles
// Common leak: strong self in closure stored on self
class VC: UIViewController {
var onComplete: (() -> Void)?
func setup() {
onComplete = {
self.doSomething() // strong capture — potential leak
}
// Fix:
onComplete = { [weak self] in
self?.doSomething()
}
}
}
43. What are Swift concurrency structured tasks?
Structured concurrency means child tasks are scoped to their parent — they are cancelled when the parent is cancelled.
func loadDashboard() async throws -> Dashboard {
async let profile = fetchProfile()
async let feed = fetchFeed()
async let notifications = fetchNotifications()
// All three run concurrently; any failure cancels others
return Dashboard(
profile: try await profile,
feed: try await feed,
notifications: try await notifications
)
}
// Task cancellation
let task = Task {
await heavyWork()
}
task.cancel() // sets task's isCancelled; cooperative cancellation
Advanced topics
44. What is the Responder Chain?
UIKit routes events (touches, motion, remote control) through a chain of responders: view → superview → ViewController → UIWindow → UIApplication → AppDelegate. The first responder that handles the event stops propagation.
// Custom responder action
class MyButton: UIButton {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// handle or call super to pass up the chain
super.touchesBegan(touches, with: event)
}
}
// Send action up the responder chain
UIApplication.shared.sendAction(#selector(MyAction.doSomething), to: nil, from: self, for: nil)
45. What is NSOperation vs GCD?
| GCD | NSOperation | |
|---|---|---|
| Level | Low-level C API | Higher-level Obj-C/Swift |
| Dependencies | Manual | addDependency(_:) |
| Cancellation | Not directly | cancel() |
| KVO observation | No | Yes |
| Reuse | No | Yes (subclass) |
| Concurrency limit | DispatchSemaphore | maxConcurrentOperationCount |
Use GCD for simple async tasks. Use NSOperation when you need dependencies, cancellation, or priority management.
46. What is @objc and dynamic in Swift?
@objc— exposes Swift declarations to Objective-C runtime (required for selectors, delegate methods in ObjC protocols)dynamic— forces dispatch through the ObjC runtime (enables method swizzling, KVO)
class MyView: UIView {
@objc dynamic var progress: Float = 0 // KVO observable
@objc func handleTap(_ gesture: UITapGestureRecognizer) {
// selector-based target/action requires @objc
}
}
// KVO observation
myView.observe(\.progress, options: [.new]) { view, change in
print("Progress: \(change.newValue ?? 0)")
}
47. What is the difference between synchronous and asynchronous URLSession tasks?
URLSession's data tasks are inherently asynchronous. With async/await:
// Async — preferred (iOS 15+)
let (data, response) = try await URLSession.shared.data(from: url)
// Completion handler — older pattern (iOS < 15 or Objective-C)
URLSession.shared.dataTask(with: url) { data, response, error in
// called on background queue — dispatch to main for UI
DispatchQueue.main.async {
self.updateUI(data: data)
}
}.resume()
48. What is Sendable in Swift concurrency?
Sendable marks types that are safe to pass across actor and task boundaries without risk of data races.
// Implicitly Sendable: value types, immutable classes
struct Config: Sendable { // safe — all properties are Sendable
let host: String
let port: Int
}
// Non-Sendable: class with mutable state
class Cache {
var data: [String: Data] = [:] // not Sendable by default
}
// Mark as @unchecked Sendable only when you manually ensure thread safety
class SafeCache: @unchecked Sendable {
private var data: [String: Data] = [:]
private let lock = NSLock()
func get(_ key: String) -> Data? {
lock.lock(); defer { lock.unlock() }
return data[key]
}
}
49. What is the Hashable and Identifiable protocol?
// Hashable — for use in Set and as Dictionary keys
struct Point: Hashable {
var x: Int
var y: Int
// Swift synthesises hash(into:) and == when all properties are Hashable
}
var visited: Set<Point> = []
visited.insert(Point(x: 0, y: 0))
// Identifiable — required for ForEach and List in SwiftUI
struct Article: Identifiable {
let id: UUID // must be unique and stable
var title: String
}
ForEach(articles) { article in // uses article.id for diffing
Text(article.title)
}
50. What are common iOS interview anti-patterns?
| Anti-pattern | Problem | Fix |
|---|---|---|
| Massive ViewController | UI, business logic, networking mixed | MVVM, Coordinator |
| Strong delegate | Retain cycle | weak var delegate: Protocol? |
DispatchQueue.main.sync on main |
Deadlock | Use async |
Force unwrap ! everywhere |
Crashes in production | Optional binding, guard |
UserDefaults for sensitive data |
Plain-text plist | Keychain |
UIImage(named:) in tight loop |
Memory spike | Cache images |
Creating URLSession per request |
Performance | Shared instance or pool |
Blocking main thread with Thread.sleep |
ANR-like freeze | Async/await |
iOS vs cross-platform comparison
| Native iOS (Swift) | React Native | Flutter | |
|---|---|---|---|
| Language | Swift / Obj-C | JavaScript / TypeScript | Dart |
| UI | Native UIKit/SwiftUI | Native components via bridge | Custom Skia rendering |
| Performance | Best | Good (JSI bridge) | Very good |
| Ecosystem | Apple-first | npm / large community | pub.dev |
| Code reuse | iOS only | iOS + Android + Web | iOS + Android + Web + Desktop |
| Apple features | All | Partial | Partial |
| App Store approval | Easiest | Slightly harder | Slightly harder |
Common mistakes
| Mistake | Impact | Fix |
|---|---|---|
| Updating UI from background thread | Random crashes | Always dispatch to DispatchQueue.main or use @MainActor |
Missing [weak self] in closures |
Memory leak | Capture list in stored closures |
Using @ObservedObject to create objects |
Reinitialised on re-render | Use @StateObject |
| Not cancelling Combine subscriptions | Memory leak | .store(in: &cancellables) |
Storing data in UserDefaults then clearing keychain |
Data inconsistency | Single source of truth |
Ignoring Task cancellation |
Wasted work, race conditions | Check Task.isCancelled |
Tight coupling to UIApplication.shared |
Untestable | Inject via protocol |
| Large images not scaled down before display | Memory pressure | UIGraphicsImageRenderer to resize |
Frequently asked questions
Q: Should I use UIKit or SwiftUI for a new project? SwiftUI for all new projects targeting iOS 16+. UIKit for projects supporting older iOS versions or requiring features not yet in SwiftUI.
Q: Is async/await compatible with Combine?
Yes — use AsyncPublisher or .values property on a Combine publisher to bridge to async/await:
for await value in publisher.values {
print(value)
}
Q: How do you handle the "purple warning" (main thread checker) in Xcode?
Ensure all UIKit access happens on the main thread. Use @MainActor, DispatchQueue.main.async, or the Main Thread Checker in the Scheme diagnostics.
Q: What is the difference between Task.detached and Task {}?
Task {} inherits the actor context and priority of its parent. Task.detached creates an unstructured task with no inherited context — use sparingly.
Q: How does iOS handle memory warnings?
The system calls applicationDidReceiveMemoryWarning and didReceiveMemoryWarning on all active ViewControllers. Respond by freeing caches and non-visible resources.
Q: What is SF Symbols?
A library of 5,000+ system icons provided by Apple, designed to work with the San Francisco font at any weight and size:
Image(systemName: "heart.fill")
.symbolRenderingMode(.multicolor)
.font(.title)