Top 49 Swift & iOS Interview Questions & Answers (2026)
About Swift & iOS
Top 100 Swift and iOS interview questions covering Swift fundamentals, optionals, protocols, UIKit, SwiftUI, Combine, Core Data, concurrency, memory management, and app architecture. Companies hiring for Swift & iOS roles test this knowledge at every stage — from first-round screens that check the basics to final-round conversations about real-world trade-offs.
What to Expect in a Swift & iOS Interview
Expect a mix of conceptual and practical Swift & iOS questions: clear definitions and core-concept checks for junior roles, hands-on scenario questions for mid-level roles, and architecture or trade-off discussions for senior roles. Interviewers usually move from foundational topics toward the kind of problems you'd actually face on the job.
How to Use This Guide
Work through the Swift & iOS questions in order — Beginner, then Intermediate, then Advanced — so each concept builds on the last. Every question has its own page; bookmark the ones that trip you up and revisit them the day before your interview.
Curated by Tech Baithak Editorial Team · Last updated: May 2026
Core concepts every Swift & iOS developer must know.
01
What is Swift?
Swift is a powerful, open-source, general-purpose programming language developed by Apple and announced in 2014. It was designed to replace Objective-C for iOS, macOS, watchOS, tvOS, and server-side development. Key characteristics: (1) Safe: null safety via Optionals eliminates a whole class of null pointer exceptions; type safety prevents type mismatches at compile time; bounds checking on arrays; (2) Fast: compiled to native machine code via LLVM, comparable to C++ performance; (3) Expressive: clean, concise syntax inspired by Python, Ruby, and Rust; type inference reduces verbosity; (4) Modern: closures, generics, protocol-oriented programming, value types, async/await concurrency; (5) Interoperable: works seamlessly with Objective-C code in the same project. Swift is strongly, statically typed — types are known at compile time but often inferred. Since 2015, Swift has been open source (github.com/apple/swift) and runs on Linux and Windows. Swift 5.9+ features macros, parameter packs, and SwiftData. Swift is consistently ranked among the most loved programming languages in developer surveys.
02
What are optionals in Swift?
Optionals are one of Swift's most important features — they represent a value that may or may not be present. A variable of type String? can contain either a String or nil. This forces explicit handling of the absence case, preventing null pointer crashes. Declaring optionals: var name: String? = "Alice" var age: Int? = nil // No value. Unwrapping optionals: (1) Optional binding (if let): safest — if let unwrapped = name { print("Name: \(unwrapped)") } else { print("No name") }; (2) guard let: early return pattern — guard let name = name else { return } // name is non-optional from here; (3) Nil coalescing (??): provide default — let displayName = name ?? "Anonymous"; (4) Optional chaining (?.): access properties/methods that may fail gracefully — let count = name?.count // Returns Int?; (5) Force unwrap (!): dangerous — crashes if nil — let forced = name! // Only if 100% sure it's non-nil. Implicitly unwrapped optionals (!): var label: UILabel! — declared as optional but used as non-optional. Used for properties that are set after init (IBOutlets). Optional map/flatMap: let uppercased = name.map { $0.uppercased() } // "ALICE" or nil.
03
What is the difference between let and var in Swift?
In Swift, let declares a constant and var declares a variable. let name = "Alice" // Constant -- cannot be changed name = "Bob" // Error: cannot assign to value: 'name' is a 'let' constant var age = 30 // Variable age = 31 // Fine. Why prefer let? Swift encourages immutability. Constants: enable compiler optimizations, document intent (this value won't change), prevent accidental mutation, required for thread safety. Use var only when the value genuinely needs to change. Value types vs reference types with let: For value types (struct, enum), let makes the entire value immutable: let point = CGPoint(x: 0, y: 0) point.x = 5 // Error. For reference types (class), let makes the reference constant but the object can still mutate: let person = Person() person.name = "Alice" // OK -- changing object, not reference person = Person() // Error -- can't reassign the reference. Collections: let array = [1, 2, 3]; array.append(4) // Error var array2 = [1, 2, 3]; array2.append(4) // Fine. Xcode warns when a var is never mutated — should be let.
04
What are value types and reference types in Swift?
Swift has two fundamental categories of types that behave differently when assigned or passed: Value types (struct, enum, tuple) — each instance keeps a unique copy. Assigning or passing creates a new copy: struct Point { var x: Int; var y: Int } var p1 = Point(x: 1, y: 2) var p2 = p1 // Independent copy p2.x = 10 print(p1.x) // 1 -- unchanged print(p2.x) // 10. Swift uses Copy-On-Write (COW) for standard library value types (Array, String, Dictionary) — the copy only happens when a mutation occurs, making copies cheap until modification. Reference types (class, actor) — multiple references to the same instance. Assigning copies the reference, not the object: class Counter { var count = 0 } let c1 = Counter() let c2 = c1 // Same object! c2.count += 1 print(c1.count) // 1 -- affected!. When to use each: use struct (value type) for: data models, most custom types in Swift — structs with no inheritance, thread safety by default, value semantics; use class (reference type) when: you need inheritance, identity matters (comparing if two references point to the same object), Objective-C interoperability, shared mutable state. Swift standard library types (Array, String, Dictionary, Int, Bool) are all value types (structs). SwiftUI heavily uses structs for Views.
05
What are Swift closures?
Closures are self-contained blocks of code that can capture and store references to variables and constants from the surrounding context. They're first-class citizens — can be assigned to variables, passed as function parameters, and returned from functions. Closure syntax: { (parameters) -> ReturnType in body }. Examples: // Full syntax: let multiply = { (a: Int, b: Int) -> Int in return a * b } // Trailing closure: let sorted = [3, 1, 4, 1, 5].sorted { $0 < $1 } // Shorthand argument names ($0, $1): let doubled = [1, 2, 3].map { $0 * 2 } // Trailing closure: let filtered = [1, 2, 3, 4].filter { $0 > 2 } // Multiline trailing: URLSession.shared.dataTask(with: url) { data, response, error in guard let data = data else { return } processData(data) }. Capturing values: func makeCounter() -> () -> Int { var count = 0 return { count += 1; return count } // Captures count } let counter = makeCounter() counter() // 1 counter() // 2. @escaping: when a closure is stored and called after the function returns: func fetchData(completion: @escaping (Data) -> Void). @autoclosure: wraps an expression in a closure automatically: func log(_ message: @autoclosure () -> String) { if isDebug { print(message()) } }. Capture list [weak self]: button.action = { [weak self] in self?.doSomething() } — prevents retain cycles.
06
What are Swift protocols?
Protocols define a blueprint of methods, properties, and requirements that conforming types must implement. They're Swift's primary mechanism for abstraction and polymorphism — similar to interfaces in other languages but more powerful. Declaring a protocol: protocol Drawable { var color: UIColor { get } // Read-only property requirement func draw() // Method requirement func resize(to size: CGSize) // Another method }. Conforming to a protocol: struct Circle: Drawable { var color: UIColor = .red func draw() { /* draw circle */ } func resize(to size: CGSize) { /* resize */ } } struct Square: Drawable { var color: UIColor = .blue func draw() { /* draw square */ } func resize(to size: CGSize) { /* resize */ } }. Protocol as a type: func render(shapes: [any Drawable]) { for shape in shapes { shape.draw() } } let shapes: [any Drawable] = [Circle(), Square()] render(shapes: shapes). Protocol extensions: provide default implementations — types get behavior for free: extension Drawable { func drawWithBorder() { draw() // drawBorder } }. Protocol composition: func process(item: Saveable & Printable) { }. Protocols with associated types: protocol Container { associatedtype Item func add(_ item: Item) func get(at index: Int) -> Item }. Protocol-Oriented Programming (POP) is Swift's design philosophy — prefer protocols over inheritance.
07
What is the Swift type system — structs, classes, enums?
Swift has three primary named types: Struct: value type, no inheritance, can conform to protocols. Preferred for most types in Swift: struct User { let id: UUID let name: String var email: String mutating func updateEmail(_ new: String) { email = new } // mutating needed for value types }. Class: reference type, supports inheritance, reference counting (ARC): class Animal { var name: String init(name: String) { self.name = name } func speak() { print("\(name) says something") } } class Dog: Animal { override func speak() { print("\(name) says Woof!") } }. Enum: value type, can have associated values, raw values, methods: enum NetworkError: Error { case timeout case notFound(url: String) case serverError(code: Int) case noConnection } // Usage: throw NetworkError.notFound(url: "https://api.example.com") // Pattern matching: switch error { case .timeout: print("Timed out") case .notFound(let url): print("Not found: \(url)") case .serverError(let code): print("Server error: \(code)") default: break } // Raw values: enum Direction: String { case north = "N", south = "S", east = "E", west = "W" } Direction.north.rawValue // "N" Direction(rawValue: "S") // .south. Choosing: Struct for data models (no inheritance, thread-safe); Class for objects with identity and shared state; Enum for finite set of related values.
08
What is ARC (Automatic Reference Counting) in Swift?
ARC (Automatic Reference Counting) is Swift's memory management system for class instances (reference types). It automatically tracks the number of strong references to each class instance and deallocates the instance when the count reaches zero. How it works: when you create a class instance, ARC allocates memory and sets count = 1. Every new strong reference increments the count. When a reference goes out of scope, ARC decrements the count. When count = 0, the deinitializer runs and memory is freed. Strong reference (default): class Person { var name: String init(name: String) { self.name = name } deinit { print("\(name) deallocated") } } var alice: Person? = Person(name: "Alice") // count = 1 var ref2: Person? = alice // count = 2 alice = nil // count = 1 ref2 = nil // count = 0 -- "Alice deallocated". Retain cycle problem: two objects strongly reference each other — count never reaches zero — memory leak: class Node { var next: Node? // Strong -- can cause cycle }. Weak references (weak var): don't increment count, automatically set to nil when the referenced object is deallocated: class Parent { var child: Child? } class Child { weak var parent: Parent? // Breaks cycle }. Unowned references (unowned): like weak but never become nil — use when the referenced object always outlives the reference: class Owner { let card: CreditCard } class CreditCard { unowned let owner: Owner // Card always has owner }. Always use weak/unowned in closures to prevent retain cycles: { [weak self] in self?.doWork() }.
09
What is UIKit?
UIKit is Apple's foundational UI framework for building iOS and tvOS applications. It provides the core infrastructure for all UI-based apps — the event loop, view hierarchy, user interaction handling, and all built-in UI components. Core concepts: (1) UIApplication: the singleton that manages the app's life cycle and dispatches events; (2) AppDelegate / SceneDelegate: responds to app and scene lifecycle events (launch, background, foreground, termination); (3) UIViewController: manages a view and its lifecycle — the primary building block of an iOS app's UI; (4) UIView: the base class for all visual elements. Views define layout and drawing. Everything visible is a UIView (or subclass); (5) UIWindow: the top-level container that hosts the view hierarchy. Common UIKit components: UILabel, UIButton, UITextField, UITextView, UIImageView, UITableView, UICollectionView, UIScrollView, UIStackView, UINavigationController, UITabBarController, UIAlertController. View hierarchy: UIWindow → UIView → subviews → sub-subviews. Rendering goes from root to leaves. Auto Layout: constraint-based layout system. Constraints define relationships between views: view.topAnchor.constraint(equalTo: parent.topAnchor, constant: 16).isActive = true. Storyboard vs Programmatic UI: Storyboard — visual editor in Xcode, IBOutlets/IBActions; Programmatic — create views entirely in code (more control, better for teams). Many modern apps use programmatic UI or SwiftUI.
10
What is the UIViewController lifecycle?
The UIViewController lifecycle defines the sequence of method calls as a view controller is created, becomes visible, updates, and is dismissed: (1) init(coder:) / init(nibName:bundle:): the view controller is instantiated from a storyboard or programmatically; (2) loadView(): creates the view hierarchy. Override to create the view programmatically (don't call super if you do). Otherwise, loads from nib/storyboard; (3) viewDidLoad(): called once after the view is loaded into memory. Best place for one-time setup: create subviews, configure outlets, set up data structures, subscribe to notifications; (4) viewWillAppear(_ animated: Bool): called every time before the view becomes visible. Refresh UI, re-register observers; (5) viewDidAppear(_ animated: Bool): view is fully visible. Start animations, begin expensive data fetching; (6) viewWillDisappear(_ animated: Bool): about to leave the screen. Save unsaved changes, pause ongoing tasks; (7) viewDidDisappear(_ animated: Bool): view is no longer visible. Stop timers, unregister observers; (8) viewWillLayoutSubviews() / viewDidLayoutSubviews(): called when bounds change. Update frames calculated from bounds; (9) deinit: view controller is deallocated — final cleanup. Memory warning: didReceiveMemoryWarning() — release cached data. Important: always call super in lifecycle methods. viewDidLoad is called once; viewWillAppear/viewDidAppear may be called multiple times.
11
What is SwiftUI?
SwiftUI is Apple's modern declarative UI framework announced in 2019, available on all Apple platforms (iOS 13+, macOS 10.15+, watchOS 6+, tvOS 13+). It replaces UIKit's imperative approach with a functional, declarative style where you describe WHAT the UI should look like based on state, not HOW to update it step by step. Core concepts: (1) View protocol: the building block. Any struct conforming to View with a body property is a view: struct ContentView: View { var body: some View { VStack { Text("Hello, World!").font(.title) Button("Tap me") { print("Tapped") } } } }; (2) State management: @State (local mutable state), @Binding (two-way binding), @StateObject/@ObservedObject/@EnvironmentObject (reference types), @Environment (app-level values); (3) Declarative updates: when state changes, SwiftUI re-renders the view body automatically — no manual UI updates; (4) Previews: Xcode live previews show UI instantly as you type. Common views: Text, Image, Button, TextField, Toggle, Slider, List, ScrollView, NavigationView/NavigationStack, TabView, VStack, HStack, ZStack, Spacer, Divider. Modifiers: chain to customize views — .font(), .foregroundColor(), .padding(), .background(), .cornerRadius(), .shadow(). SwiftUI vs UIKit: SwiftUI = less code, cross-platform, live previews, modern; UIKit = more control, larger API surface, mature, required for some advanced features. Interoperability: UIViewRepresentable / UIViewControllerRepresentable.
12
What are Swift generics?
Generics allow writing flexible, reusable functions and types that work with any type, subject to defined constraints. They enable type-safe code without code duplication. Generic function: func swap<T>(_ a: inout T, _ b: inout T) { let temp = a; a = b; b = temp } var x = 5, y = 10 swap(&x, &y) // x=10, y=5 var s1 = "hello", s2 = "world" swap(&s1, &s2) // Works with any type!. Generic type: struct Stack<Element> { private var items: [Element] = [] mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element? { return items.popLast() } var top: Element? { items.last } } var intStack = Stack<Int>() intStack.push(1); intStack.push(2) var stringStack = Stack<String>(). Type constraints: func findMax<T: Comparable>(_ array: [T]) -> T? { array.max() } // Multiple constraints: func process<T: Hashable & Equatable>(_ items: [T]) { }. Associated types in protocols: protocol Queue { associatedtype Element mutating func enqueue(_ item: Element) mutating func dequeue() -> Element? }. Where clauses: func merge<T: Sequence, U: Sequence>(_ a: T, _ b: U) -> [T.Element] where T.Element == U.Element { }. Opaque types (some): func makeContainer() -> some Container { // Return type is hidden }. Generics + protocols = Swift's powerful type system for expressing abstractions.
13
What is the @State property wrapper in SwiftUI?
@State is a SwiftUI property wrapper that creates mutable state owned by a view. When a @State value changes, SwiftUI automatically re-renders the view body to reflect the new state — this is the core mechanism of SwiftUI's declarative UI updates. Basic usage: struct CounterView: View { @State private var count = 0 // Owned by this view var body: some View { VStack { Text("Count: \(count)") .font(.largeTitle) Button("Increment") { count += 1 // Triggers re-render! } Button("Decrement") { count -= 1 } Button("Reset") { count = 0 } } } }. Rules: use @State for simple value types (Int, String, Bool, struct); mark as private — state is local to the view and should not be exposed; @State instances are stored outside the view struct (by SwiftUI) so they persist across re-renders of the view. @Binding — share state with child views: struct ToggleView: View { @Binding var isOn: Bool // Refers to parent's state var body: some View { Toggle("Enable", isOn: $isOn) } } // Parent: @State private var enabled = false ToggleView(isOn: $enabled) // Pass binding with $. When to use @State vs @StateObject: @State for value types (simple data); @StateObject for reference types (ObservableObject classes). @State is not for complex or shared state — use @ObservedObject, @StateObject, or @EnvironmentObject for that.
14
What is the difference between @ObservedObject, @StateObject, and @EnvironmentObject?
All three work with ObservableObject classes but differ in ownership and scope: @StateObject: the view owns the object — creates it and manages its lifetime. The object is created once and survives view re-renders: class ViewModel: ObservableObject { @Published var count = 0 } struct ParentView: View { @StateObject private var vm = ViewModel() // Owns -- created once var body: some View { Text("\(vm.count)") ChildView(vm: vm) } }. Use @StateObject when the view creates the object. @ObservedObject: the view does NOT own the object — it receives it from outside. The object is not preserved if the view is recreated: struct ChildView: View { @ObservedObject var vm: ViewModel // Doesn't own -- received from parent var body: some View { Button("Increment") { vm.count += 1 } } }. Use @ObservedObject when the object is passed in. @EnvironmentObject: injected into the view hierarchy — any descendant can access it without explicit passing: // At root: ContentView().environmentObject(UserStore()) // Any descendant: struct ProfileView: View { @EnvironmentObject var userStore: UserStore // Accessed from environment }. Crashes at runtime if the object isn't injected. ObservableObject + @Published: class CartStore: ObservableObject { @Published var items: [Item] = [] @Published var total: Double = 0 }. When @Published property changes, all observing views are automatically re-rendered. Summary: @StateObject = view creates it; @ObservedObject = view receives it; @EnvironmentObject = accessed from hierarchy.
15
What are delegates in iOS?
The Delegate pattern is a core iOS design pattern where one object (the delegating object) communicates back to another object (the delegate) through a protocol. Used extensively in UIKit: UITableViewDelegate, UITextFieldDelegate, URLSessionDelegate, etc. Implementation example: // Step 1: Define protocol protocol TaskDelegate: AnyObject { func taskDidStart(_ task: Task) func taskDidComplete(_ task: Task, result: String) func taskDidFail(_ task: Task, error: Error) } // Step 2: Delegating class holds a weak reference class Task { weak var delegate: TaskDelegate? // WEAK to avoid retain cycle var name: String init(name: String) { self.name = name } func start() { delegate?.taskDidStart(self) // Perform work... delegate?.taskDidComplete(self, result: "Done") } } // Step 3: Delegate implements protocol class ViewController: UIViewController, TaskDelegate { let task = Task(name: "Download") override func viewDidLoad() { super.viewDidLoad() task.delegate = self task.start() } func taskDidStart(_ task: Task) { print("Task started: \(task.name)") } func taskDidComplete(_ task: Task, result: String) { print("Task done: \(result)") } func taskDidFail(_ task: Task, error: Error) { print("Task failed: \(error)") } }. Why weak delegate? Prevents retain cycle — delegating object holding the delegate strongly + delegate holding delegating object strongly = leak. UITableView delegation: tableView.dataSource = self tableView.delegate = self. Override protocol methods to provide cell count, cells, heights, selection handling.
16
What is UITableView and how do you implement it?
UITableView displays a list of items in a single column with optional sections. It uses a data source and delegate pattern. It's one of the most commonly used UIKit components. Implementation steps: (1) Add UITableView to view hierarchy; (2) Set dataSource and delegate; (3) Register cell class or nib; (4) Implement required dataSource methods: class UsersViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { var users: [User] = [] let tableView = UITableView() override func viewDidLoad() { super.viewDidLoad() tableView.dataSource = self tableView.delegate = self tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") view.addSubview(tableView) fetchUsers() } // Required: func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return users.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) let user = users[indexPath.row] cell.textLabel?.text = user.name cell.detailTextLabel?.text = user.email return cell } // Optional: func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) let user = users[indexPath.row] navigateToDetail(user) } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 60 } }. Cell reuse (dequeueReusableCell): the key to UITableView's performance — only creates enough cells to fill the screen, reuses off-screen cells as new ones come into view. Custom cells: subclass UITableViewCell, design in Interface Builder or code. Sections: implement numberOfSections and titleForHeaderInSection.
17
What is Auto Layout and how does it work?
Auto Layout is UIKit's constraint-based layout system that allows views to automatically calculate their size and position based on relationships (constraints) between views. It adapts to different screen sizes, orientations, and dynamic content. Constraints define: a view's width, height, position relative to other views or the superview. Each constraint is a linear equation: view1.attribute = multiplier × view2.attribute + constant. Anchors API (programmatic): // Add subview: view.addSubview(label) label.translatesAutoresizingMaskIntoConstraints = false // REQUIRED 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) ]). UIStackView: container that arranges subviews automatically: let stack = UIStackView(arrangedSubviews: [label, button]) stack.axis = .vertical stack.spacing = 8 stack.distribution = .fill. Visual Format Language: string-based constraints (less common now): NSLayoutConstraint.constraints(withVisualFormat: "H:|-16-[view]-16-|", ...). Compression resistance and content hugging: priorities that control how views behave when there's extra or insufficient space. Safe area layout guide: accounts for notch, home indicator, status bar. Use safeAreaLayoutGuide instead of direct superview anchors. Intrinsic content size: some views (UILabel, UIButton) have a natural size based on content — Auto Layout respects this without explicit width/height constraints.
18
What is a closure in Swift and how do you avoid retain cycles?
A retain cycle occurs when two objects strongly reference each other — ARC can never deallocate them because the count never reaches zero. This is most common with closures that capture self: class ViewController: UIViewController { var timer: Timer? override func viewDidLoad() { timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in self.updateUI() // STRONG capture of self! } } // viewController holds timer, timer's closure holds viewController STRONGLY } // Memory leak!. Solution: capture list with [weak self] or [unowned self]: timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in guard let self = self else { return } // self might be nil self.updateUI() } // OR with unowned (when self is guaranteed to exist): timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [unowned self] _ in self.updateUI() // Crashes if self is nil! }. [weak self] vs [unowned self]: weak — self becomes optional (can be nil); safe when the captured object might be deallocated before the closure; unowned — self is non-optional; use only when the captured object is guaranteed to outlive the closure (crashes if deallocated). Prefer [weak self] in general — safer. Instruments — Leaks: use Xcode Instruments > Leaks tool to detect retain cycles at runtime. Memory Graph Debugger in Xcode also shows cycles visually. The pattern { [weak self] in guard let self = self else { return } ... } is idiomatic Swift for safe closure capture.
19
What is the difference between frame and bounds in UIView?
frame and bounds are both CGRect properties of UIView but use different coordinate systems: frame: the view's size and position in its superview's coordinate system. The origin is relative to the superview: view.frame = CGRect(x: 20, y: 50, width: 200, height: 100) // Positioned at (20, 50) in superview. bounds: the view's size and position in its own coordinate system. The origin is usually (0, 0) unless scrolled: view.bounds // Usually CGRect(x: 0, y: 0, width: 200, height: 100). Key difference — when they diverge: If you rotate a view using a transform, the frame changes (it's the smallest bounding rectangle in superview coords) but bounds stays the same: view.transform = CGAffineTransform(rotationAngle: .pi / 4) // 45 degrees print(view.frame) // Larger bounding box around the rotated view print(view.bounds) // Still the view's own rect. UIScrollView bounds: bounds.origin changes as the user scrolls — this is how scrolling works. The contentOffset IS the bounds.origin: scrollView.bounds.origin.y == scrollView.contentOffset.y. Rules: use frame to position a view within its superview; use bounds to draw inside a view (CGContext uses bounds); use frame.size and bounds.size — they should be equal for non-transformed views; frame.origin and bounds.origin differ for transformed views. center: view.center = CGPoint(x: superview.bounds.midX, y: superview.bounds.midY) — centers in superview.
20
What is Grand Central Dispatch (GCD)?
Grand Central Dispatch (GCD) is Apple's low-level C API (also available in Swift via Dispatch) for managing concurrent operations across a thread pool. It abstracts thread management — you submit work to queues, GCD decides which thread to use. Dispatch queues: Main queue: serial, runs on the main thread — all UI updates MUST happen here: DispatchQueue.main.async { self.tableView.reloadData() // UI update on main thread }. Global queues: concurrent, different quality-of-service (QoS) levels: DispatchQueue.global(qos: .userInitiated).async { let data = self.fetchData() // Background work DispatchQueue.main.async { self.updateUI(data) // Back to main } }. Custom queues: let serialQueue = DispatchQueue(label: "com.app.serial") let concurrentQueue = DispatchQueue(label: "com.app.concurrent", attributes: .concurrent). QoS levels (priority): .userInteractive (highest — animations), .userInitiated (user-triggered actions), .default, .utility (background tasks), .background (lowest — indexing, sync). sync vs async: sync blocks the calling thread until work completes; async returns immediately, work runs in background. Never call sync on the main queue from the main thread — deadlock! DispatchGroup: wait for multiple async tasks: let group = DispatchGroup() group.enter(); asyncTask1 { group.leave() } group.enter(); asyncTask2 { group.leave() } group.notify(queue: .main) { self.allDone() }. DispatchWorkItem: encapsulate work that can be cancelled. DispatchBarrier: read/write synchronization on concurrent queues.
21
What is Core Data in iOS?
Core Data is Apple's object graph and persistence framework for managing the model layer of an application. It's NOT a database — it's an ORM-like layer that can persist data to SQLite, binary, or in-memory stores. Core Data stack components: (1) NSManagedObjectModel: the schema — defines entities (like tables), attributes (columns), and relationships; (2) NSPersistentStoreCoordinator: bridges the model with the persistent store (SQLite file); (3) NSManagedObjectContext (MOC): the "working area" where objects are fetched, modified, and saved. Tracks changes; (4) NSPersistentContainer (modern): encapsulates the entire stack in one convenient object. Modern setup: lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "MyApp") container.loadPersistentStores { _, error in if let error = error { fatalError("Core Data error: \(error)") } } return container }() var context: NSManagedObjectContext { return persistentContainer.viewContext }. CRUD operations: // Create: let user = User(context: context) user.name = "Alice"; user.email = "alice@example.com" try! context.save() // Read: let request = User.fetchRequest() request.predicate = NSPredicate(format: "name == %@", "Alice") request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)] let results = try! context.fetch(request) // Update: user.name = "Alice Smith"; try! context.save() // Delete: context.delete(user); try! context.save(). SwiftData (iOS 17+): modern replacement with Swift macros: @Model class User { var name: String; var email: String }. NSFetchedResultsController: drives UITableView/UICollectionView automatically from Core Data changes.
22
What is Combine in Swift?
Combine is Apple's reactive programming framework (Swift-native, iOS 13+) for processing values over time through a Publisher-Subscriber model. It handles asynchronous events (network responses, user input, notifications) in a declarative, composable way. Core concepts: Publisher: produces values over time. Subscriber: receives and reacts to values. Operators: transform, filter, combine publishers. Cancellable: represents a subscription — cancel to stop receiving values. Basic example: import Combine var cancellables = Set<AnyCancellable>() // Publisher from notification: NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification) .compactMap { ($0.object as? UITextField)?.text } .filter { $0.count > 2 } .debounce(for: .milliseconds(300), scheduler: RunLoop.main) .removeDuplicates() .sink { [weak self] text in self?.viewModel.search(text) } .store(in: &cancellables). Common operators: map, flatMap, filter, reduce, merge, zip, combineLatest, debounce, throttle, retry, catch, receive(on:), subscribe(on:), removeDuplicates, assign(to:on:). @Published + Combine: class SearchViewModel: ObservableObject { @Published var searchText = "" @Published var results: [User] = [] init() { $searchText .debounce(for: .milliseconds(300), scheduler: RunLoop.main) .removeDuplicates() .flatMap { text in APIService.search(text) } .receive(on: RunLoop.main) .assign(to: &$results) } }. Combine vs async/await: both are valid; Combine excels at reactive streams with multiple values; async/await is simpler for single-value async operations.
23
What is Swift error handling?
Swift provides a first-class error handling mechanism using the throws, try, catch keywords and the Error protocol. Unlike exceptions in other languages, Swift errors are part of the type system — the compiler ensures you handle them. Defining errors: enum NetworkError: Error { case noConnection case serverError(statusCode: Int) case invalidResponse case unauthorized }. Throwing function: func fetchUser(id: Int) throws -> User { guard isConnected else { throw NetworkError.noConnection } let response = makeRequest(id: id) guard response.statusCode == 200 else { throw NetworkError.serverError(statusCode: response.statusCode) } return parseUser(response.data) }. Handling errors with try/catch: do { let user = try fetchUser(id: 1) print("Found: \(user.name)") } catch NetworkError.noConnection { print("Check your connection") } catch NetworkError.serverError(let code) { print("Server error: \(code)") } catch { print("Unknown error: \(error)") }. try? — convert to optional: let user = try? fetchUser(id: 1) // User? -- nil on error. try! — force, crashes on error: let user = try! fetchUser(id: 1) // Use only if guaranteed to succeed. Propagating errors: func processUser(id: Int) throws { let user = try fetchUser(id: id) // Error propagates up save(user) }. Result type (alternative): func fetch(completion: (Result<User, Error>) -> Void). async throws: func fetchUser(id: Int) async throws -> User { try await networkCall() }.
24
What are Swift access control levels?
Swift has five access control levels that control the visibility of entities (classes, methods, properties) across modules and files: (1) open: most permissive — accessible from any module AND can be subclassed/overridden externally. Only for classes and members: open class BaseViewController: UIViewController { open func setup() {} // Can be overridden in other frameworks }; (2) public: accessible from any module but cannot be subclassed/overridden externally: public struct APIClient { public init() {} public func fetch() {} }; (3) internal: (DEFAULT) accessible within the same module (app or framework). No explicit keyword needed; (4) fileprivate: accessible only within the same source file: fileprivate class HelperClass {} // Only used in this file; (5) private: most restrictive — accessible only within the enclosing declaration (class, struct, extension in same file): class BankAccount { private var balance: Double = 0 // Only BankAccount can touch this private func validate() -> Bool { balance >= 0 } public func deposit(_ amount: Double) { guard amount > 0 else { return } balance += amount } }. Best practices: start with private, relax as needed; properties should usually be private or fileprivate; use internal for types within your app; use public/open when building frameworks. Extensions: extensions in same file can access private members. Extensions in different files can only access fileprivate and above. Getter/setter different access: private(set) var count = 0 // Public read, private write.
25
What is the iOS app lifecycle?
The iOS app lifecycle describes the states an app goes through from launch to termination. Understanding it is essential for proper resource management. App states: (1) Not Running: app hasn't been launched or was terminated by the system; (2) Inactive: app is in foreground but not receiving events (briefly, during transition); (3) Active: app is in the foreground and receiving events — the normal running state; (4) Background: app is not visible but executing code. Limited time (~30 seconds); (5) Suspended: app is in background but not executing — kept in memory; system may terminate without notice. SceneDelegate (iOS 13+) lifecycle methods: func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options: UIScene.ConnectionOptions) // Scene created func sceneDidBecomeActive(_ scene: UIScene) // App became active func sceneWillResignActive(_ scene: UIScene) // About to become inactive func sceneDidEnterBackground(_ scene: UIScene) // Entered background func sceneWillEnterForeground(_ scene: UIScene) // About to enter foreground func sceneDidDisconnect(_ scene: UIScene) // Scene removed. AppDelegate (older / shared): func applicationDidBecomeActive(_ application: UIApplication) func applicationWillResignActive(_ application: UIApplication) func applicationDidEnterBackground(_ application: UIApplication) func applicationWillEnterForeground(_ application: UIApplication) func applicationWillTerminate(_ application: UIApplication). Best practices: save state in sceneDidEnterBackground; release heavy resources in background; restore state in sceneWillEnterForeground; use UIApplication.shared.backgroundTask for background work completion.
26
What is the MVVM architecture pattern in iOS?
MVVM (Model-View-ViewModel) is the most popular architecture for modern iOS apps, especially with SwiftUI and Combine/async. It separates concerns into three layers: (1) Model: data and business logic — plain Swift structs/classes representing domain data: struct User: Codable { let id: Int; let name: String; let email: String }; (2) View: the UI layer — UIViewController/UIView in UIKit, or SwiftUI View structs. Dumb — only displays data and forwards user actions to ViewModel. No business logic; (3) ViewModel: mediator between View and Model. Contains presentation logic, handles user actions, transforms model data for display, communicates with services: class UserListViewModel: ObservableObject { @Published var users: [User] = [] @Published var isLoading = false @Published var errorMessage: String? private let userService: UserServiceProtocol init(userService: UserServiceProtocol = UserService()) { self.userService = userService } func loadUsers() async { isLoading = true do { users = try await userService.fetchUsers() } catch { errorMessage = error.localizedDescription } isLoading = false } func deleteUser(at indexSet: IndexSet) { users.remove(atOffsets: indexSet) } }. SwiftUI View: struct UserListView: View { @StateObject private var viewModel = UserListViewModel() var body: some View { List(viewModel.users) { user in Text(user.name) } .task { await viewModel.loadUsers() } } }. Benefits: testable ViewModels (no UIKit dependency), clear separation, SwiftUI-friendly. Other patterns: MVC (UIKit default — ViewController becomes "Massive ViewController"), MVP, VIPER (complex apps), Clean Architecture, TCA (The Composable Architecture).
Practical knowledge for developers with hands-on experience.
01
What is Swift concurrency (async/await)?
Swift 5.5 introduced structured concurrency with async/await, providing a cleaner alternative to completion handlers and GCD for asynchronous code. async function: func fetchUsers() async throws -> [User] { let url = URL(string: "https://api.example.com/users")! let (data, response) = try await URLSession.shared.data(from: url) guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw NetworkError.badResponse } return try JSONDecoder().decode([User].self, from: data) }. await — suspension point: when the runtime hits await, the current thread is freed to do other work; execution resumes when the awaited value is ready. Calling async functions: // In async context: let users = try await fetchUsers() // In SwiftUI: .task { await viewModel.loadUsers() } // From sync context: Task { await loadData() }. Task: the unit of async work. Task { /* async work */ } Task.detached { /* unstructured */ } // Cancel: let task = Task { await longWork() } task.cancel(). async let — parallel execution: async let users = fetchUsers() async let products = fetchProducts() let (u, p) = try await (users, products) // Both run concurrently. TaskGroup — dynamic parallelism: 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 }. MainActor: ensures code runs on main thread: @MainActor class ViewModel: ObservableObject {} @MainActor func updateUI() {} await MainActor.run { self.tableView.reloadData() }.
02
What are Swift actors?
Actors (Swift 5.5+) are reference types that protect their mutable state from concurrent access — they serialize access to their methods and properties. Actors eliminate data races without manual locking. Declaring an actor: 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 { balance } }. Using an actor — must await: let account = BankAccount() // Actor method calls require await: await account.deposit(100) let balance = await account.getBalance() try await account.withdraw(50). How actors prevent data races: only one piece of code can access an actor's internals at a time. Other callers wait. The actor schedules access, preventing concurrent mutation. MainActor: a global actor that ensures all access happens on the main thread: @MainActor class ViewModel: ObservableObject { @Published var text = "" // Automatically on main thread func update() { text = "Updated" // Safe -- on main actor } } // Can also mark individual functions: @MainActor func updateUI() { label.text = "Hello" }. nonisolated: opt out of actor isolation for non-mutating methods: actor DataStore { nonisolated let id = UUID() // No await needed func fetchData() async -> Data { /* ... */ } }. Sendable protocol: types safe to send across concurrency domains. Value types are Sendable by default. Classes need explicit conformance or @unchecked Sendable. Actor vs class: actor = thread-safe reference type; class = not thread-safe reference type.
03
What is the Coordinator pattern in iOS?
The Coordinator pattern (by Soroush Khanlou) extracts navigation logic from ViewControllers into dedicated Coordinator objects. It solves the "Massive ViewController" problem and makes navigation testable and reusable. Protocol: protocol Coordinator: AnyObject { var childCoordinators: [Coordinator] { get set } var navigationController: UINavigationController { get set } func start() }. Implementation: class AppCoordinator: Coordinator { var childCoordinators: [Coordinator] = [] var navigationController: UINavigationController init(navigationController: UINavigationController) { self.navigationController = navigationController } func start() { let homeCoordinator = HomeCoordinator(navigationController: navigationController) homeCoordinator.delegate = self homeCoordinator.parentCoordinator = self childCoordinators.append(homeCoordinator) homeCoordinator.start() } } class HomeCoordinator: Coordinator { var childCoordinators: [Coordinator] = [] var navigationController: UINavigationController weak var delegate: AppCoordinator? func start() { let vc = HomeViewController() vc.coordinator = self // ViewController knows its coordinator navigationController.pushViewController(vc, animated: false) } func showDetail(for item: Item) { let vc = DetailViewController(item: item) vc.coordinator = self navigationController.pushViewController(vc, animated: true) } func coordinatorDidFinish(_ coordinator: Coordinator) { childCoordinators.removeAll { $0 === coordinator } } }. Benefits: VCs are free of navigation code; navigation logic is testable; coordinators are reusable; clear ownership of child flows. Deinitialization: when a coordinator's flow completes, it removes itself from parent's childCoordinators. SwiftUI + Coordinator: less common — NavigationPath and @NavigationDestination handle navigation declaratively.
04
What is NSURLSession and networking in iOS?
URLSession (NSURLSession in Objective-C) is the modern networking API for iOS apps — handles HTTP/HTTPS, caching, cookies, authentication, background downloads. Basic GET request: struct APIClient { let session = URLSession.shared func fetchUsers() async throws -> [User] { let url = URL(string: "https://api.example.com/users")! var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw URLError(.badServerResponse) } guard (200...299).contains(httpResponse.statusCode) else { throw APIError.httpError(httpResponse.statusCode) } return try JSONDecoder().decode([User].self, from: data) } }. POST with body: request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try JSONEncoder().encode(newUser) let (data, response) = try await session.data(for: request). URLSession configurations: .default (uses disk cache, credentials), .ephemeral (no cache/cookies — private browsing), .background (transfers continue when app suspended). Codable for JSON: struct User: Codable { let id: Int let name: String let email: String } let users = try JSONDecoder().decode([User].self, from: data) let json = try JSONEncoder().encode(user). async/await vs completion handlers: async/await is the modern approach (iOS 15+); completion handlers for older targets. Third-party: Alamofire (feature-rich networking layer), Moya (type-safe networking), Apollo (GraphQL).
05
What is UserDefaults and when should you use it?
UserDefaults is a simple key-value persistent store for small amounts of data — user preferences, app settings, flags. It persists across app launches and is synchronized to disk automatically. Basic usage: // Save: UserDefaults.standard.set(true, forKey: "hasSeenOnboarding") UserDefaults.standard.set("Alice", forKey: "username") UserDefaults.standard.set(42, forKey: "launchCount") UserDefaults.standard.set(Date(), forKey: "lastOpenDate") // Read: let hasSeenOnboarding = UserDefaults.standard.bool(forKey: "hasSeenOnboarding") let username = UserDefaults.standard.string(forKey: "username") // nil if not set let count = UserDefaults.standard.integer(forKey: "launchCount") // Delete: UserDefaults.standard.removeObject(forKey: "username"). Custom objects (Codable): struct Settings: Codable { var theme: String var notificationsEnabled: Bool } let data = try! JSONEncoder().encode(settings) UserDefaults.standard.set(data, forKey: "settings") // Read: if let data = UserDefaults.standard.data(forKey: "settings") { let settings = try! JSONDecoder().decode(Settings.self, from: data) }. @AppStorage (SwiftUI): property wrapper for UserDefaults: @AppStorage("hasSeenOnboarding") var hasSeenOnboarding = false @AppStorage("username") var username = "". Property wrapper for UIKit: @propertyWrapper struct Stored<T> { let key: String; let defaultValue: T var wrappedValue: T { get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue } set { UserDefaults.standard.set(newValue, forKey: key) } } } @Stored(key: "count", defaultValue: 0) var count: Int. When NOT to use: sensitive data (use Keychain), large data (use files/Core Data), complex queries (use Core Data). Limit: best for simple types and small data.
06
What is the Keychain and when do you use it?
The Keychain is iOS's secure, encrypted storage for sensitive data — passwords, authentication tokens, cryptographic keys. Unlike UserDefaults, data is encrypted and persists even after app deletion (unless configured otherwise). When to use Keychain: passwords, API tokens, OAuth tokens, private keys, biometric secrets — anything that must stay secure. Direct Keychain access (complex): import Security func saveToKeychain(key: String, value: String) { let data = value.data(using: .utf8)! let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecValueData as String: data ] SecItemDelete(query as CFDictionary) // Delete existing SecItemAdd(query as CFDictionary, nil) } func readFromKeychain(key: String) -> String? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecReturnData as String: true ] var result: AnyObject? SecItemCopyMatching(query as CFDictionary, &result) if let data = result as? Data { return String(data: data, encoding: .utf8) } return nil }. KeychainWrapper library (simpler): KeychainWrapper.standard.set(token, forKey: "authToken") let token = KeychainWrapper.standard.string(forKey: "authToken"). Sharing across apps (App Group Keychain): kSecAttrAccessGroup: "group.com.company.app". Access control: kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock // Accessible after unlock kSecAttrAccessibleWhenUnlocked // Only when unlocked kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly // Requires passcode. Biometric protection: require Face ID/Touch ID to access: var accessControl = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlocked, .biometryCurrentSet, nil).
07
What is dependency injection in Swift/iOS?
Dependency Injection (DI) provides objects with their dependencies rather than having them create their own — enabling testability, flexibility, and loose coupling. Constructor injection (recommended): protocol NetworkServiceProtocol { func fetchUsers() async throws -> [User] } class NetworkService: NetworkServiceProtocol { func fetchUsers() async throws -> [User] { /* real API call */ } } class MockNetworkService: NetworkServiceProtocol { func fetchUsers() async throws -> [User] { return [User(id: 1, name: "Test")] } } class UserListViewModel { private let networkService: NetworkServiceProtocol init(networkService: NetworkServiceProtocol = NetworkService()) { self.networkService = networkService } func loadUsers() async { let users = try? await networkService.fetchUsers() } } // Production: let vm = UserListViewModel() // Uses real service // Testing: let vm = UserListViewModel(networkService: MockNetworkService()). Property injection: class ViewController { var viewModel: ViewModel! // Set from coordinator }. Method injection: func process(using service: ServiceProtocol) { service.doWork() }. DI containers/frameworks: Swinject — register/resolve dependencies in a container: let container = Container() container.register(NetworkServiceProtocol.self) { _ in NetworkService() } container.register(UserListViewModel.self) { r in UserListViewModel(networkService: r.resolve(NetworkServiceProtocol.self)!) }. Benefits: easily swap implementations (real vs mock), unit test without side effects, clearer dependencies, Single Responsibility Principle. Environment injection (SwiftUI): struct ContentView: View { @EnvironmentObject var userService: UserService }.
08
What is the difference between synchronous and asynchronous operations in iOS?
Synchronous operations block the calling thread until completion. Asynchronous operations return immediately and deliver results later via callbacks, delegates, Combine, or async/await. Why async matters on iOS: the main thread handles all UI updates, touch events, and animations at 60fps. Any work longer than ~16ms on the main thread causes visual stutter ("jank"). Network calls, file I/O, heavy computation MUST be on background threads. Patterns for async work: (1) Completion handlers (old style): func loadImage(url: URL, completion: @escaping (UIImage?) -> Void) { DispatchQueue.global().async { let data = try? Data(contentsOf: url) let image = data.flatMap { UIImage(data: $0) } DispatchQueue.main.async { completion(image) // Back to main for UI } } }; (2) async/await (modern): func loadImage(url: URL) async throws -> UIImage { let (data, _) = try await URLSession.shared.data(from: url) guard let image = UIImage(data: data) else { throw ImageError.invalid } return image } // Usage: Task { let image = try await loadImage(url: url) await MainActor.run { imageView.image = image } }; (3) Combine: URLSession.shared.dataTaskPublisher(for: url) .map { UIImage(data: $0.data) } .receive(on: DispatchQueue.main) .sink(receiveCompletion: { _ in }, receiveValue: { image in imageView.image = image }).store(in: &cancellables). Deadlock prevention: never dispatch sync to the same queue you're on. Never block the main thread with sync calls. Use async APIs for network, disk, and heavy computation.
09
What is UICollectionView?
UICollectionView displays items in customizable layouts — grids, carousels, custom arrangements. It's more flexible than UITableView but requires a layout object. Basic setup: class PhotosViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate { var photos: [UIImage] = [] let collectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.minimumInteritemSpacing = 2 layout.minimumLineSpacing = 2 return UICollectionView(frame: .zero, collectionViewLayout: layout) }() override func viewDidLoad() { super.viewDidLoad() collectionView.dataSource = self collectionView.delegate = self collectionView.register(PhotoCell.self, forCellWithReuseIdentifier: "PhotoCell") } // Required: func collectionView(_ cv: UICollectionView, numberOfItemsInSection section: Int) -> Int { photos.count } func collectionView(_ cv: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = cv.dequeueReusableCell(withReuseIdentifier: "PhotoCell", for: indexPath) as! PhotoCell cell.imageView.image = photos[indexPath.item] return cell } // Layout: func collectionView(_ cv: UICollectionView, layout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let width = (cv.bounds.width - 4) / 3 return CGSize(width: width, height: width) } }. Modern Compositional Layout (iOS 13+): let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/3), heightDimension: .fractionalHeight(1)) let item = NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(1/3)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) let layout = UICollectionViewCompositionalLayout(section: section). DiffableDataSource (iOS 13+): type-safe, animated updates without manual reloadData.
10
What is Codable in Swift?
Codable is a type alias for the combined Encodable & Decodable protocols. It enables automatic JSON serialization and deserialization with minimal boilerplate — the compiler synthesizes the implementation automatically for types with Codable properties. Basic Codable: struct User: Codable { let id: Int let name: String let email: String let joinDate: Date } // Decode JSON: let json = """{"id":1,"name":"Alice","email":"alice@example.com","joinDate":"2024-01-15T10:00:00Z"}""" let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let user = try decoder.decode(User.self, from: json.data(using: .utf8)!) // Encode to JSON: let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 encoder.outputFormatting = .prettyPrinted let data = try encoder.encode(user) let jsonString = String(data: data, encoding: .utf8)!. Custom key mapping (CodingKeys): struct Product: Codable { let productID: Int let productName: String enum CodingKeys: String, CodingKey { case productID = "product_id" // JSON uses snake_case case productName = "product_name" } }. Nested types: struct Order: Codable { let id: Int let items: [OrderItem] // Nested Codable struct let address: Address }. Custom encoding/decoding: implement encode(to:) and init(from:) for complex types. keyDecodingStrategy (snake_case): decoder.keyDecodingStrategy = .convertFromSnakeCase // "user_name" -> userName. Codable replaces hand-written JSON parsing and third-party libraries like SwiftyJSON for most use cases.
11
What are property wrappers in Swift?
Property wrappers (Swift 5.1) add a layer of logic to property get/set operations. They're annotated with @propertyWrapper and used as property attribute. Simple clamping wrapper: @propertyWrapper struct Clamped<T: Comparable> { var wrappedValue: T { didSet { wrappedValue = min(max(wrappedValue, minValue), maxValue) } } let minValue: T; let maxValue: T init(wrappedValue: T, min: T, max: T) { self.minValue = min; self.maxValue = max self.wrappedValue = wrappedValue } } struct Player { @Clamped(min: 0, max: 100) var health = 100 } var player = Player() player.health = 150 // Clamped to 100 player.health = -10 // Clamped to 0. UserDefaults wrapper: @propertyWrapper struct UserDefault<T> { let key: String; let defaultValue: T var wrappedValue: T { get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue } set { UserDefaults.standard.set(newValue, forKey: key) } } } struct Settings { @UserDefault(key: "isDarkMode", defaultValue: false) static var isDarkMode: Bool } Settings.isDarkMode = true. projectedValue ($): expose additional value via $ prefix: @propertyWrapper struct Validated<T> { var wrappedValue: T private(set) var projectedValue: Bool = false // $isValid } @Validated var age: Int @State var count = 0 // $count is Binding<Int>. Built-in property wrappers: @State, @Binding, @Published, @StateObject, @ObservedObject, @EnvironmentObject, @Environment, @AppStorage, @SceneStorage, @Lazy, @atomic patterns. Property wrappers reduce boilerplate and are the mechanism behind SwiftUI's reactive state system.
12
What is the difference between frame-based and Auto Layout?
Two approaches to positioning and sizing UI elements in UIKit: Frame-based layout: set exact position and size manually using CGRect: let label = UILabel() label.frame = CGRect(x: 20, y: 100, width: 200, height: 44) label.text = "Hello". Works predictably, simple to understand, good performance. Problems: must recalculate manually for different screen sizes (iPhone 15 vs iPhone SE), rotations, dynamic type (accessibility text sizes). Fragile — hardcoded values break on different devices. Auto Layout: constraint-based — define relationships between views instead of exact positions: let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false view.addSubview(label) NSLayoutConstraint.activate([ label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20) ]). Adapts automatically to any screen size, orientation, and dynamic type. More complex to debug (ambiguous/conflicting constraints). Slightly more CPU-intensive to calculate. translatesAutoresizingMaskIntoConstraints: MUST be set to false when using Auto Layout programmatically — otherwise UIKit automatically adds constraints based on the view's autoresizingMask, conflicting with your constraints. Choosing: Auto Layout is standard for production apps; frame-based for performance-critical custom drawing or simple one-size scenarios. UIStackView makes common layouts much simpler. SwiftUI uses its own layout system (HStack/VStack/ZStack/Grid).
13
What is WKWebView and how does it differ from UIWebView?
WKWebView is the modern web content display class (WebKit framework, iOS 8+). UIWebView was its predecessor — deprecated in iOS 12 and removed in iOS 15. Key differences: WKWebView runs in a separate process (out-of-process rendering) — more stable and secure; if the web content crashes, it doesn't bring down the app. UIWebView ran in the app process. WKWebView has significantly better JavaScript performance (Nitro JIT engine). WKWebView better memory efficiency — doesn't consume the app's memory budget as aggressively. UIWebView is DEPRECATED — any App Store submission using UIWebView gets rejected. WKWebView implementation: import WebKit class WebViewController: UIViewController, WKNavigationDelegate, WKUIDelegate { var webView: WKWebView! override func loadView() { let config = WKWebViewConfiguration() config.websiteDataStore = .nonPersistent() // Private browsing webView = WKWebView(frame: .zero, configuration: config) webView.navigationDelegate = self view = webView } override func viewDidLoad() { super.viewDidLoad() if let url = URL(string: "https://www.apple.com") { webView.load(URLRequest(url: url)) } } // Navigation delegate: func webView(_ wv: WKWebView, didStartProvisionalNavigation _: WKNavigation!) { print("Loading: \(wv.url?.absoluteString ?? "")") } func webView(_ wv: WKWebView, didFinish _: WKNavigation!) { title = wv.title } } // JS communication: webView.evaluateJavaScript("document.title") { result, _ in print(result) } webView.configuration.userContentController.add(self, name: "native").
14
What is push notifications and APNs in iOS?
Push notifications deliver information to users even when the app isn't active. Apple Push Notification service (APNs) is the gateway between your server and Apple's infrastructure that routes notifications to devices. How it works: app registers for notifications → Apple issues a device token → app sends token to your server → server sends notification payload + device token to APNs → APNs delivers to device. Requesting permission (iOS 10+): import UserNotifications func requestPermission() { UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in if granted { DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } } }. AppDelegate — receiving device token: func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() sendTokenToServer(token) } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { print("Failed: \(error)") }. Handling notification in foreground: extension AppDelegate: UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler handler: @escaping (UNNotificationPresentationOptions) -> Void) { handler([.banner, .badge, .sound]) } func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let userInfo = response.notification.request.content.userInfo handleDeepLink(userInfo) completionHandler() } }. Local notifications: scheduled without a server. Rich notifications: images, actions, custom UI.
15
What is the difference between nil coalescing and optional chaining?
Both deal with optionals but serve different purposes: Optional chaining (?.): safely accesses properties or calls methods on an optional value. If the optional is nil, the entire chain returns nil (of the result type as Optional). Useful for navigating a chain of optional accesses: let user: User? = getUser() let count = user?.name?.count // Int? -- nil if user or name is nil let uppercased = user?.name?.uppercased() // String? // Calling methods: user?.save() // Nothing happens if user is nil // Setting through chain: user?.address?.city = "New York" // No-op if chain breaks. Nil coalescing (??): provides a default value when an optional is nil — always returns a non-optional: let name: String? = nil let displayName = name ?? "Anonymous" // "Anonymous" let count = user?.name?.count ?? 0 // 0 if anything in chain is nil // Chaining: let value = optional1 ?? optional2 ?? defaultValue. Combined: let streetName = user?.address?.street?.name ?? "No Street" // Chains ?.?.?. then provides default with ??. guard let vs if let vs ??: // if let -- use value in a scope: if let name = user?.name { print("Hello \(name)") } // guard let -- exit if nil: guard let name = user?.name else { return } print("Hello \(name)") // ?. -- chain without unwrapping: user?.name?.uppercased() // ?? -- need a non-optional fallback: user?.name ?? "Unknown". Use optional chaining when the nil case is acceptable; nil coalescing when you always need a concrete value.
16
What is SwiftUI's NavigationStack?
NavigationStack (iOS 16+) is the modern replacement for NavigationView in SwiftUI, providing a stack-based navigation model with programmatic control. Basic navigation: struct ContentView: View { var body: some View { NavigationStack { List(items) { item in NavigationLink(item.name, value: item) } .navigationDestination(for: Item.self) { item in DetailView(item: item) } .navigationTitle("Items") } } }. Programmatic navigation with NavigationPath: struct ContentView: View { @State private var path = NavigationPath() var body: some View { NavigationStack(path: $path) { HomeView() .navigationDestination(for: Item.self) { item in DetailView(item: item) } .navigationDestination(for: String.self) { id in ProfileView(id: id) } } } // Programmatically navigate: path.append(selectedItem) // Push path.append("user-123") // Push a String route // Go back: path.removeLast() // Pop one path.removeLast(path.count) // Pop to root // Navigate to root: path = NavigationPath(). NavigationLink types: // Value-based (recommended with NavigationStack): NavigationLink("Go to Detail", value: item) // View-based (works anywhere): NavigationLink("Go to Detail") { DetailView() }. Deep linking with NavigationPath: encode/decode NavigationPath using Codable to restore navigation state. vs NavigationView (deprecated): NavigationView had ambiguous behavior on iPad; NavigationStack is explicitly stack-based, more predictable, and supports programmatic navigation out of the box.
17
What is Core Location in iOS?
Core Location provides device location and heading information — GPS, Wi-Fi positioning, cell towers, and iBeacon. Usage requires explicit user permission. Setup: add to Info.plist: NSLocationWhenInUseUsageDescription — explains why you need location. Implementation: import CoreLocation class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate { private let manager = CLLocationManager() @Published var userLocation: CLLocation? @Published var authorizationStatus: CLAuthorizationStatus override init() { super.init() manager.delegate = self manager.desiredAccuracy = kCLLocationAccuracyBest } func requestPermission() { manager.requestWhenInUseAuthorization() } func startUpdating() { manager.startUpdatingLocation() } func stopUpdating() { manager.stopUpdatingLocation() } // Delegate: func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { userLocation = locations.last } func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { authorizationStatus = manager.authorizationStatus switch manager.authorizationStatus { case .authorizedWhenInUse, .authorizedAlways: startUpdating() case .denied, .restricted: print("Location access denied") default: break } } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { print("Location error: \(error)") } }. Authorization levels: .authorizedWhenInUse (in foreground), .authorizedAlways (foreground + background — justify to App Review), .denied, .restricted, .notDetermined. One-time location: manager.requestLocation() — single update, simpler for one-off needs. Region monitoring: get notified when device enters/exits a circular region (geofencing).
18
What is memory management in Swift collections?
Understanding memory management in Swift collections — especially copy-on-write (COW) — is important for performance. Value type collections (Array, Dictionary, Set): these are value types (structs) and use Copy-On-Write (COW). Assigning creates a logical copy, but actual memory copying happens only when one of the copies is mutated: var array1 = [1, 2, 3, 4, 5] var array2 = array1 // No copy yet -- both share same storage // Now mutate array2: array2.append(6) // NOW it copies -- array1 unaffected print(array1.count) // 5 print(array2.count) // 6. Performance implications: // Efficient: var result = someArray // No copy -- COW result.append(item) // Copy happens here only if shared // Avoid: var copy = originalArray // Shared for i in 0..<copy.count { copy[i] *= 2 // May copy multiple times if shared! } // Better: var copy = originalArray copy.withUnsafeMutableBufferPointer { buffer in for i in buffer.indices { buffer[i] *= 2 } }. Reference type elements in value type collections: var array1 = [NSMutableString("hello")] var array2 = array1 // Copy of the array (new array storage) array2[0].append(" world") // Mutates the SAME NSMutableString! print(array1[0]) // "hello world" -- affected because element is reference type. The array is copied but elements (reference types) are not. Lazy collections: let lazyMapped = (1...1000000).lazy.map { $0 * 2 } // No memory allocated until iterated. Use lazy for large collections to avoid creating intermediate arrays.
Deep expertise questions for senior and lead roles.
01
What is Swift's type system — protocols with associated types and existentials?
Swift's type system includes protocols with associated types (PATs), which are more powerful but less flexible than simple protocols. Understanding them is crucial for advanced Swift. Protocol with associated type: protocol Collection { associatedtype Element // Generic placeholder associatedtype Iterator: IteratorProtocol associatedtype Index: Comparable func makeIterator() -> Iterator subscript(position: Index) -> Element { get } } // Can't use directly as type: // var c: Collection // ERROR -- "Protocol 'Collection' can only be used as a generic constraint". The problem — existential types (any): before Swift 5.7, you couldn't use PATs as types. Since 5.7, use any: var sequences: [any Sequence<Int>] = [] // Heterogeneous sequences of Int var collections: [any Collection] = [] // Heterogeneous but runtime type erasure. Primary associated types (Swift 5.7): protocol Sequence<Element> { // Element is primary associatedtype Element }. Opaque types (some) — concrete but hidden: func makeNumbers() -> some Collection<Int> { return [1, 2, 3] // Specific type hidden but concrete at compile time } // SwiftUI: var body: some View { /* specific type */ }. Type erasure (manual): wrap a PAT in a concrete struct: struct AnySequence<Element>: Sequence { private let _makeIterator: () -> AnyIterator<Element> init<S: Sequence>(_ sequence: S) where S.Element == Element { self._makeIterator = { AnyIterator(sequence.makeIterator()) } } func makeIterator() -> AnyIterator<Element> { _makeIterator() } }. any vs some: some = one specific type (opaque, compile-time); any = any conforming type (existential, runtime type erasure). Use some for performance; any when you need heterogeneity.
02
What is Swift's concurrency model — Sendable and actor isolation?
Swift's strict concurrency model (fully enabled in Swift 6) prevents data races at compile time through Sendable and actor isolation. Sendable protocol: a type is Sendable if it's safe to share across concurrency domains (actors, tasks). Value types (struct, enum with Sendable properties) are implicitly Sendable. Classes must be explicitly marked: // Implicitly Sendable (struct with Sendable properties): struct User: Sendable { let id: Int; let name: String } // Class must be explicitly Sendable (and thread-safe): final class ThreadSafeCounter: @unchecked Sendable { private var value = 0 private let lock = NSLock() func increment() { lock.lock(); defer { lock.unlock() }; value += 1 } } // Actor is Sendable: actor DataStore: Sendable { var data: [String] = [] }. Actor isolation: actor state can only be accessed from within the actor (or via await): actor UserCache { private var cache: [Int: User] = [:] func store(_ user: User) { cache[user.id] = user } func user(id: Int) -> User? { cache[id] } } let cache = UserCache() // From outside actor -- must await: let user = await cache.user(id: 42) // Inside actor (on actor) -- no await: extension UserCache { func clearExpired(before date: Date) { // Direct access -- no await needed cache = cache.filter { _, user in user.expiry > date } } }. Global actors: @globalActor actor NetworkActor { static let shared = NetworkActor() } @NetworkActor class NetworkManager { func fetch() async throws -> Data { /* isolated to NetworkActor */ } }. Swift 6 strict concurrency: the compiler checks for all Sendable violations and actor isolation at compile time — data races become compile errors rather than runtime crashes.
03
What are Swift macros?
Swift Macros (Swift 5.9) are compile-time code generation mechanisms — they expand to produce additional Swift code at compile time, eliminating boilerplate. Unlike C preprocessor macros, Swift macros are type-safe, produce well-formed code, and are debuggable. Macro types: (1) Expression macros (#stringify): transform expressions: let (value, string) = #stringify(42 + 2) // value=44, string="42 + 2" (2) Freestanding macros (#warning, #error, #URL): let url = #URL("https://api.example.com") // Validated at compile time!; (3) Attached macros (@Observable, @Model): enhance declarations. @Observable (Swift 5.9, replacement for ObservableObject): import Observation @Observable class ViewModel { var name = "" // Automatically generates @Published-like behavior var count = 0 // Observation tracking without @Published func increment() { count += 1 } } // In SwiftUI: struct View: View { @State private var vm = ViewModel() // No @StateObject needed! var body: some View { Text(vm.name) } }. SwiftData @Model macro: import SwiftData @Model class User { var name: String var email: String @Relationship(deleteRule: .cascade) var posts: [Post] init(name: String, email: String) { self.name = name; self.email = email } }. Custom macros: implement as a Swift package that conforms to specific macro protocols. Debug with expanded macro view in Xcode. Built-in macros: @discardableResult (suppress warning), @warn_unhandled_result, #file, #line, #function, #column.
04
What is SwiftData?
SwiftData (iOS 17+) is Apple's modern replacement for Core Data, built with Swift macros and full SwiftUI integration. It provides simple, type-safe, expressive persistence. Defining models: import SwiftData @Model class Trip { var name: String var destination: String var startDate: Date var endDate: Date @Relationship(deleteRule: .cascade, inverse: \Trip.accommodation) var accommodation: Accommodation? init(name: String, destination: String, start: Date, end: Date) { self.name = name self.destination = destination self.startDate = start self.endDate = end } } @Model class Accommodation { var name: String var address: String }. Setup in SwiftUI: @main struct TravelApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: [Trip.self, Accommodation.self]) } }. Fetching with @Query: struct TripsView: View { @Query(sort: \Trip.startDate, order: .reverse) var trips: [Trip] // Automatically updates @Environment(\.modelContext) var context var body: some View { List(trips) { trip in Text(trip.name) } } }. CRUD operations: // Create: let trip = Trip(name: "Vacation", destination: "Tokyo", start: .now, end: .now.addingTimeInterval(86400 * 7)) context.insert(trip) // Delete: context.delete(trip) // Update: trip.name = "Summer Vacation" // Auto-saved. Filtering: @Query(filter: #Predicate<Trip> { trip in trip.destination == "Tokyo" }, sort: \Trip.startDate) var tokyoTrips: [Trip]. SwiftData uses the same SQLite backend as Core Data but with a modern Swift-native API.
05
How do you optimize iOS app performance?
iOS performance optimization covers multiple dimensions: 1. Main thread hygiene: all UI on main thread, all heavy work on background. Never block main with network/disk I/O. Profile with Time Profiler in Instruments to find main thread stalls. 2. Image optimization: resize images to display size before setting them (don't display a 4K image in a 44pt thumbnail); use UIGraphicsImageRenderer for resizing; decode images in background before display (decode on main thread causes lag); use ImageIO framework for efficient image loading; AsyncImage (SwiftUI) and SDWebImage/Kingfisher handle this automatically. 3. Table/Collection View: cell reuse (dequeue); lightweight cellForRowAt — do heavy work elsewhere; prefetch with prefetchDataSource; estimate cell heights with estimatedRowHeight; avoid transparent backgrounds. 4. Memory: use weak/unowned to break retain cycles; profile with Memory Graph Debugger and Instruments Allocations; avoid large objects in memory; use lazy loading for expensive properties. 5. Efficient data structures: Dictionary over Array for lookups; Set over Array for membership tests; lazy sequences for large data. 6. Rendering: opaque layers (opaque: true) vs transparent; avoid off-screen rendering (cornerRadius + masksToBounds, shadows); rasterize static complex views (shouldRasterize = true). 7. Launch time: defer non-critical work from applicationDidFinishLaunching; avoid expensive work in +initialize/+load; use dynamic framework loading. 8. Network: cache responses; use background URLSession for large transfers; compression; pagination. 9. Instruments: Time Profiler (CPU), Allocations (memory), Leaks, Core Animation (rendering), Energy (battery).