Resources

iOS Study Guide

iOS & Swift — curated questions and answers to help you prepare.

Swift Fundamentals

Value vs Reference Types

What's the difference between struct and class?

Easy

Struct is a value type (copied on assignment), class is a reference type (shared instance). Structs live on the stack, classes live on the heap.

When would you use a struct over a class?

Easy

Prefer struct by default. Use when no shared mutable state is needed — structs have safer concurrency and better performance due to stack allocation and copy-on-write.

What is Copy-on-Write (CoW)?

Easy

A performance optimization where a value type shares its underlying storage until a mutation occurs. Only then is a copy made. Used in Array, String, and Dictionary.

What is the difference between identity (===) and equality (==)?

Medium

=== checks if two class references point to the exact same object in memory (reference identity). == checks value equality as defined by the Equatable protocol. Only classes support ===.

Can a struct conform to a class-only protocol?

Medium

No. A struct cannot conform to a protocol constrained with AnyObject or marked with the class keyword, since those protocols require reference-type conformers.

What happens when you pass a struct into a closure?

Medium

The struct is captured by value at the time of capture. Mutations inside the closure don't affect the original, and vice versa. To mutate the original you'd need an inout parameter or a class.

How do you implement custom Copy-on-Write for your own type?

Hard

Use isKnownUniquelyReferenced() before any mutation. If the reference isn't unique, copy the storage first.

Swift
final class Storage { var value: Int = 0 }

struct MyStruct {
  private var _storage = Storage()

  var value: Int {
    get { _storage.value }
    set {
      if !isKnownUniquelyReferenced(&_storage) {
        _storage = Storage()
      }
      _storage.value = newValue
    }
  }
}

What is the difference between an existential type and a generic constraint?

Hard

An existential (any Protocol) is a type-erased box resolved at runtime via dynamic dispatch — flexible but with overhead. A generic constraint (<T: Protocol>) resolves the concrete type at compile time, enabling static dispatch and specialisation. Prefer generics for performance; use existentials when you need heterogeneous collections.

Swift Fundamentals

Optionals

What is an optional?

Easy

A type that wraps another type and can hold either a value or nil. Defined as Optional<T>, written as T?.

Difference between if let and guard let?

Easy

if let → scoped unwrapping; the bound value is only available inside the if block.
guard let → early exit; the bound value stays available for the rest of the scope. Prefer guard let to reduce nesting.

What is force unwrapping and why is it dangerous?

Easy

Crashes with a fatal error if the optional is nil. Only use it when you can guarantee a value exists, or when a crash is the intentional failure mode.

What are implicitly unwrapped optionals?

Easy

Declared with ! instead of ?. They behave as non-optionals — no unwrapping syntax needed — but crash at runtime if nil. Mostly used for IBOutlets and properties set shortly after init.

What is optional chaining?

Medium

A way to call properties and methods on an optional that returns nil gracefully if any link is nil, instead of crashing.

Swift
let city = user?.address?.city  // nil if any part is nil

What does the nil-coalescing operator (??) do?

Medium

Returns the unwrapped optional if it has a value, otherwise returns a default.

Swift
let name = user.name ?? "Anonymous"

What is the difference between map and flatMap on an optional?

Medium

map transforms the wrapped value and re-wraps it: Optional<T> → Optional<U>.
flatMap also transforms but the closure returns an optional, preventing double-wrapping.

Swift
let str: String? = "42"
let num = str.flatMap { Int($0) }  // Int?, not Int??

How does Swift handle Optional in a generic context?

Hard

Optional<T> is itself a generic enum with cases .some(T) and .none. One common pitfall: T? where T is already optional produces Optional<Optional<T>>, so flatMap is critical.

Swift
func firstInt(_ values: [String?]) -> Int? {
  values.lazy.compactMap { $0 }.compactMap { Int($0) }.first
}
Swift Fundamentals

Closures

What is a closure?

Easy

A self-contained block of code that can capture and store references to variables from its surrounding context.

Swift
let add = { (a: Int, b: Int) -> Int in
  a + b
}

What is a capture list?

Easy

A list in square brackets at the start of a closure that declares how external values are captured.

Swift
{ [weak self, count] in
  self?.update(count)
}

Escaping vs non-escaping closures?

Medium

Non-escaping (default): executed synchronously within the function body.
Escaping (@escaping): stored or called after the function returns — required for async callbacks.

Swift
func fetch(completion: @escaping (Data) -> Void) {
  URLSession.shared.dataTask(with: url) { data, _, _ in
    completion(data!)
  }.resume()
}

How do retain cycles occur in closures and how do you prevent them?

Medium

A retain cycle forms when a closure captures self strongly and self holds a strong reference to that closure. Neither can be deallocated.

Swift
// Retain cycle
self.onComplete = { self.dismiss() }

// Fixed
self.onComplete = { [weak self] in self?.dismiss() }

What is @autoclosure?

Medium

Wraps an expression in a closure automatically, delaying evaluation until the closure is called. Used for short-circuit operators and conditional logging.

Swift
func log(_ msg: @autoclosure () -> String) {
  if isDebug { print(msg()) }  // msg() only evaluated if isDebug
}

log("Value: \(expensiveComputation())")

What is the difference between [weak self] and [unowned self]?

Hard

[weak self]: self becomes Optional inside the closure — safe if self may be deallocated before the closure runs.
[unowned self]: non-optional, crashes if self has been deallocated. Use only when the closure's lifetime is strictly shorter than self's.

Swift
// weak — self might be gone
timer.handler = { [weak self] in
  self?.refresh()
}

// unowned — self guaranteed to outlive the closure
child.onDone = { [unowned self] in
  self.childFinished()
}

What are multiple trailing closures?

Hard

Swift 5.3+ lets you write multiple closure arguments after the closing parenthesis, with labeled syntax for all but the first.

Swift
UIView.animate(withDuration: 0.3) {
  view.alpha = 0
} completion: { finished in
  view.removeFromSuperview()
}
Swift Fundamentals

Protocols

What is a protocol in Swift?

Easy

A blueprint of methods, properties, and requirements that a conforming type must implement. Similar to interfaces in other languages, but more powerful — protocols can have default implementations via extensions.

Can a struct conform to a protocol?

Easy

Yes. Structs, classes, and enums can all conform to protocols. Unlike class inheritance, protocol conformance is the primary composition mechanism in Swift.

What are protocol extensions?

Medium

Extensions on a protocol that provide default method implementations. Conforming types get these for free but can override them.

Swift
protocol Greetable {
  var name: String { get }
}

extension Greetable {
  func greet() { print("Hello, \(name)!") }
}

What are associated types?

Medium

A placeholder type defined in a protocol, resolved concretely by the conforming type.

Swift
protocol Container {
  associatedtype Item
  var items: [Item] { get }
  mutating func add(_ item: Item)
}

struct Box: Container {
  var items: [Int] = []
  mutating func add(_ item: Int) { items.append(item) }
}

What is protocol-oriented programming (POP)?

Medium

A Swift paradigm favouring protocol composition and default implementations over class inheritance. Benefits: works with value types, avoids deep hierarchies, and enables easy testing by swapping conformers.

What is the difference between 'some Protocol' and 'any Protocol'?

Hard

some Protocol (opaque type): concrete type is fixed at compile time, enabling static dispatch.
any Protocol (existential): type-erased box holding any conformer, resolved at runtime.

Swift
// some — single concrete type, efficient
func makeShape() -> some Shape { Circle() }

// any — heterogeneous collection, flexible
var shapes: [any Shape] = [Circle(), Rectangle()]

What are primary associated types?

Hard

Introduced in Swift 5.7 — lets you constrain associated types in angle brackets on existentials, similar to generics.

Swift
// Before Swift 5.7
func process<C: Collection>(_ items: C) where C.Element == Int { }

// Swift 5.7+
func process(_ items: any Collection<Int>) { }
Swift Fundamentals

Generics

What are generics in Swift?

Easy

A way to write flexible, reusable code that works with any type while maintaining type safety. The compiler generates specialised versions at compile time.

Swift
func swap<T>(_ a: inout T, _ b: inout T) {
  let tmp = a
  a = b
  b = tmp
}

What is a generic constraint?

Medium

A restriction on a type parameter requiring protocol conformance or a specific superclass.

Swift
func max<T: Comparable>(_ a: T, _ b: T) -> T {
  return a > b ? a : b
}

// where clause for multiple constraints
func equal<T>(_ a: T, _ b: T) -> Bool where T: Equatable, T: Hashable {
  return a == b
}

What is type erasure and when would you use it?

Medium

A pattern that wraps a generic or protocol-with-associated-type behind a concrete type, hiding the underlying implementation. Example: AnyPublisher<Output, Failure> erases the concrete Publisher. Use it when you need to store or return heterogeneous values conforming to the same protocol.

What are parameter packs (variadic generics) in Swift 5.9?

Hard

Parameter packs let functions and types accept a variable number of generic type parameters, eliminating combinatorial overloads.

Swift
func printAll<each T>(_ values: repeat each T) {
  repeat print(each values)
}

printAll(1, "hello", true)  // works with any number of args
Swift Fundamentals

Error Handling

How does error handling work in Swift?

Easy

Functions that can fail are marked throws. Callers use try inside a do-catch block. Errors conform to the Error protocol.

Swift
enum FileError: Error {
  case notFound, permissionDenied
}

func loadFile(at path: String) throws -> Data {
  guard fileExists(path) else { throw FileError.notFound }
  return try Data(contentsOf: URL(fileURLWithPath: path))
}

do {
  let data = try loadFile(at: "/tmp/file.txt")
} catch FileError.notFound {
  print("File not found")
} catch {
  print("Unexpected error:", error)
}

What is the difference between try, try?, and try!?

Easy

try: propagates the error to the caller — must be inside a do-catch or a throws function.
try?: converts the result to an optional — nil on failure, no crash.
try!: force-unwraps; crashes at runtime on failure.

Swift
let a = try  parseJSON(data)   // throws on failure
let b = try? parseJSON(data)   // nil on failure
let c = try! parseJSON(data)   // crash on failure

What is the Result type?

Medium

An enum with .success(Value) and .failure(Error) cases for representing outcomes without throwing. Useful for async callbacks.

Swift
func fetch(_ url: URL, completion: (Result<Data, Error>) -> Void) {
  URLSession.shared.dataTask(with: url) { data, _, error in
    if let error { completion(.failure(error)); return }
    completion(.success(data!))
  }.resume()
}

// Caller
fetch(url) { result in
  switch result {
  case .success(let data): handle(data)
  case .failure(let e):    showError(e)
  }
}

When would you use Result over throws?

Medium

Use throws for synchronous code — it's cleaner and the compiler enforces handling. Use Result when passing outcomes across async boundaries (callbacks, Combine), storing them, or when you want to defer error handling.

What are typed throws introduced in Swift 6?

Hard

throws(SpecificError) lets a function declare the exact error type it throws, enabling exhaustive catch blocks without casting.

Swift
enum ParseError: Error { case invalidToken, unexpectedEOF }

func parse(_ input: String) throws(ParseError) -> AST {
  guard !input.isEmpty else { throw .unexpectedEOF }
  // ...
}

do {
  let ast = try parse(source)
} catch .invalidToken {
  // exhaustive — no default needed
} catch .unexpectedEOF {
  // compiler verifies all cases
}
Swift Fundamentals

Enums

What is an enum in Swift?

Easy

A type that defines a finite set of related values. Swift enums are first-class types — they can have methods, computed properties, and conform to protocols.

What are raw values in enums?

Easy

A pre-set primitive value associated with each case.

Swift
enum Planet: Int {
  case mercury = 1, venus, earth
}

Planet(rawValue: 3)  // Optional(.earth)
Planet.venus.rawValue  // 2

What are associated values?

Medium

Data attached to a specific case, allowing each case to carry different types of data.

Swift
enum Barcode {
  case qr(String)
  case upc(Int, Int, Int)
}

let code = Barcode.qr("https://example.com")

if case .qr(let url) = code {
  print(url)
}

What is an indirect enum?

Medium

An enum where cases hold a value of the same enum type. indirect enables recursive data structures by using a reference internally.

Swift
indirect enum Tree<T> {
  case leaf(T)
  case node(Tree<T>, Tree<T>)
}

let tree = Tree.node(.leaf(1), .leaf(2))

What does CaseIterable provide?

Medium

Automatic synthesis of an allCases static property containing all cases in declaration order.

Swift
enum Direction: CaseIterable {
  case north, south, east, west
}

Direction.allCases.count  // 4
Direction.allCases.map { $0 }  // [.north, .south, .east, .west]

How does pattern matching work with enums?

Hard

Swift uses switch, if case, and guard case to destructure enum cases and bind associated values. where clauses add extra conditions.

Swift
switch result {
case .success(let data) where data.count > 0:
  handle(data)
case .success:
  handleEmpty()
case .failure(let e as NetworkError):
  retry()
case .failure(let e):
  showError(e)
}
Swift Fundamentals

Collections & Functional Programming

What are the main collection types in Swift?

Easy

Array: ordered, allows duplicates.
Dictionary: key-value pairs, unordered.
Set: unordered, unique elements.

All conform to the Collection protocol and share higher-order functions like map, filter, and reduce.

What is the difference between map, filter, and reduce?

Medium

map: transforms each element, same count.
filter: keeps elements matching a predicate.
reduce: folds all elements into a single value.

Swift
let nums = [1, 2, 3, 4, 5]

nums.map    { $0 * 2 }          // [2, 4, 6, 8, 10]
nums.filter { $0.isMultiple(of: 2) }  // [2, 4]
nums.reduce(0, +)               // 15

What is a lazy sequence?

Medium

A sequence that defers element computation until actually consumed — no intermediate arrays are allocated.

Swift
let firstFiveEvens = (1...1_000_000)
  .lazy
  .filter { $0.isMultiple(of: 2) }
  .prefix(5)  // Only 5 elements ever computed

// vs non-lazy: allocates 1_000_000-element array first

What does compactMap do?

Medium

Transforms each element and removes nils in a single pass — more efficient than map + filter.

Swift
let strings = ["1", "two", "3", "four"]
let ints = strings.compactMap { Int($0) }  // [1, 3]

How would you implement a custom Sequence?

Hard

Conform to Sequence by implementing makeIterator(), returning a type conforming to IteratorProtocol with a mutating next() -> Element? method.

Swift
struct Countdown: Sequence {
  let from: Int
  func makeIterator() -> Iterator { Iterator(current: from) }

  struct Iterator: IteratorProtocol {
    var current: Int
    mutating func next() -> Int? {
      guard current > 0 else { return nil }
      defer { current -= 1 }
      return current
    }
  }
}

for n in Countdown(from: 3) { print(n) }  // 3, 2, 1
Swift Fundamentals

Access Control

What are Swift's access control levels?

Easy

From most to least accessible:

open — any module; classes can be subclassed/overridden externally
public — any module; no external subclassing
internal — same module (default)
fileprivate — same source file
private — enclosing declaration only

What is the difference between private and fileprivate?

Medium

private restricts access to the enclosing type and extensions of that type in the same file.
fileprivate allows access from anywhere in the same file, including unrelated types.

What is the difference between public and open?

Medium

Both allow access from outside the module. open additionally allows subclassing and overriding externally.

Swift
// Can be used externally, but not subclassed
public class NetworkClient { }

// Can be subclassed and overridden by external modules
open class BaseViewController: UIViewController {
  open func setup() { }  // overridable
}

How does access control interact with extensions?

Hard

Extensions in the same file as the type can access private members. You can set a default access level for all members of an extension.

Swift
private extension MyType {
  func internalHelper() { }  // all members are private
}

// Extensions can access private members if in the same file
extension MyType {
  func anotherMethod() {
    internalHelper()  // OK — same file
  }
}
Swift Fundamentals

Property Wrappers

What is a property wrapper?

Easy

A type annotated with @propertyWrapper that adds custom storage and behaviour to a property via a required wrappedValue. Common examples: @State, @Published, @AppStorage.

How do you implement a custom property wrapper?

Medium

Define a struct/class with @propertyWrapper and implement wrappedValue.

Swift
@propertyWrapper
struct Clamped {
  private var value: Int
  let range: ClosedRange<Int>

  var wrappedValue: Int {
    get { value }
    set { value = min(max(newValue, range.lowerBound), range.upperBound) }
  }

  init(wrappedValue: Int, _ range: ClosedRange<Int>) {
    self.range = range
    self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
  }
}

struct Speaker {
  @Clamped(0...100) var volume = 50
}

var s = Speaker()
s.volume = 150  // clamped to 100

What is projectedValue in a property wrapper?

Medium

An optional secondary value exposed via the $ prefix on the property.

Swift
@propertyWrapper
struct Logged<T> {
  private var value: T
  var wrappedValue: T {
    get { value }
    set { print("Changed to \(newValue)"); value = newValue }
  }
  var projectedValue: String { "Current: \(value)" }
  init(wrappedValue: T) { value = wrappedValue }
}

struct Model {
  @Logged var name = "Adrian"
}

var m = Model()
print(m.$name)  // "Current: Adrian"

What are the limitations of property wrappers?

Hard

Cannot be applied to computed properties or protocol requirements.
Cannot be used on stored properties in extensions.
Composing multiple wrappers on one property isn't directly supported — you'd need to nest wrapper types.
The wrappedValue access level must be at least as accessible as the wrapper type itself.

UIKit

View Hierarchy & Layout

What is the difference between a view's frame and its bounds?

Easy

frame: the view's position and size in its parent's coordinate system.
bounds: the view's position and size in its own coordinate system — origin is usually (0,0).

Swift
view.frame  // CGRect(x: 50, y: 100, width: 200, height: 100) — parent coords
view.bounds // CGRect(x: 0, y: 0, width: 200, height: 100)  — own coords

Rotating a view changes frame (bounding box grows) but not bounds.

What is the UIView hierarchy?

Easy

UIView has a tree structure: every view has one superview and zero or more subviews. Drawing happens recursively from the window root. Views are added with addSubview(_:) and removed with removeFromSuperview(). The window is the root of the tree.

What is Auto Layout?

Easy

A constraint-based layout system that expresses the position and size of views as mathematical relationships. The layout engine solves all constraints simultaneously to determine frames at runtime — supporting any screen size or orientation.

What is the difference between intrinsic content size and Auto Layout constraints?

Medium

Intrinsic content size is the natural size a view prefers based on its content (e.g., a UILabel's text). Auto Layout uses it when explicit width/height constraints are absent, via content hugging and compression resistance priorities.

Content hugging: resistance to growing beyond intrinsic size (default 250).
Compression resistance: resistance to shrinking below intrinsic size (default 750).

What is the Auto Layout update cycle?

Medium

UIKit processes layout in three passes per run-loop iteration:

1. updateConstraints() — update constraint constants
2. layoutSubviews() — calculate and apply frames
3. draw(_:) — render pixels

Call setNeedsLayout() to schedule a layout pass. Call layoutIfNeeded() to force it immediately (useful inside animation blocks).

When would you use setNeedsLayout() vs layoutIfNeeded()?

Medium

setNeedsLayout() marks the view as needing layout but defers it to the next run-loop cycle — prefer this for normal updates to batch changes efficiently.

layoutIfNeeded() forces an immediate layout pass — required inside UIView.animate blocks so constraints animate.

Swift
UIView.animate(withDuration: 0.3) {
  self.heightConstraint.constant = 200
  self.view.layoutIfNeeded() // drives the animation
}

What is the difference between translatesAutoresizingMaskIntoConstraints and Auto Layout constraints?

Hard

When translatesAutoresizingMaskIntoConstraints is true (default for views created in code), UIKit auto-generates constraints from the autoresizing mask. Adding your own constraints on top creates conflicts. Always set it to false on views you're constraining manually.

Swift
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
NSLayoutConstraint.activate([
  label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
  label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])

How would you debug a broken Auto Layout constraint?

Hard

1. Check the Xcode console for 'Unable to simultaneously satisfy constraints' — it prints all conflicting constraints.
2. Use po UIView.UIViewAutolayoutNotification or Debug > View Debugging > Capture View Hierarchy in Xcode.
3. Give views and constraints identifiers for readable logs:

Swift
let c = label.topAnchor.constraint(equalTo: view.topAnchor, constant: 20)
c.identifier = "label-top"
c.isActive = true

4. Break on UIViewAlertForUnsatisfiableConstraints in the debugger.

UIKit

View Controller Lifecycle

What are the main UIViewController lifecycle methods?

Easy

viewDidLoad() — called once after the view is loaded into memory. Set up UI here.
viewWillAppear(_:) — called before the view becomes visible. Refresh data here.
viewDidAppear(_:) — view is on screen. Start animations or tracking.
viewWillDisappear(_:) — about to leave screen. Pause activity.
viewDidDisappear(_:) — no longer visible. Stop timers, save state.
deinit — view controller being deallocated.

What is the difference between viewDidLoad and viewWillAppear?

Easy

viewDidLoad fires once in the view controller's lifetime (after the view hierarchy is loaded). viewWillAppear fires every time the view is about to appear — including when returning from a pushed view controller. Use viewDidLoad for one-time setup and viewWillAppear for data that may change between appearances.

When is viewDidLoad called relative to the view being on screen?

Medium

viewDidLoad is called after the view is loaded into memory but before it's displayed. The view exists in memory but has no window yet — frame may still reflect placeholder values from Interface Builder. Do not rely on final frames in viewDidLoad; use viewDidLayoutSubviews instead.

What is viewDidLayoutSubviews and when should you use it?

Medium

Called after the view controller's view lays out its subviews. This is where final frames are known. Use it for layout calculations that depend on actual view sizes, but avoid expensive operations since it can be called many times.

Swift
override func viewDidLayoutSubviews() {
  super.viewDidLayoutSubviews()
  gradientLayer.frame = view.bounds // safe — bounds are final
}

What is the container view controller pattern and how does it work?

Hard

A parent view controller manages child view controllers, composing complex UIs from smaller units. You must use the proper containment API — not just addSubview — so lifecycle events propagate correctly.

Swift
func add(_ child: UIViewController) {
  addChild(child)                         // 1. notify
  view.addSubview(child.view)             // 2. add view
  child.view.frame = view.bounds
  child.didMove(toParent: self)           // 3. signal complete
}

func remove(_ child: UIViewController) {
  child.willMove(toParent: nil)           // 1. signal leaving
  child.view.removeFromSuperview()        // 2. remove view
  child.removeFromParent()               // 3. clean up
}
UIKit

Table Views & Collection Views

How does UITableView cell reuse work?

Easy

Cells are expensive to create. UITableView maintains a reuse pool: as cells scroll off screen they're enqueued. dequeueReusableCell(withIdentifier:for:) returns a recycled cell (or creates one) — always configure it fully since it may contain stale data.

Swift
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
  cell.textLabel?.text = items[indexPath.row]
  return cell
}

What is the difference between UITableView and UICollectionView?

Easy

UITableView: single-column, vertical-only list. Simpler API, built-in sections and headers.
UICollectionView: fully customisable layout engine. Supports grids, carousels, waterfalls — any layout via UICollectionViewLayout or UICollectionViewCompositionalLayout. Prefer UICollectionView for anything beyond a simple list.

What is NSIndexPath in the context of table views?

Medium

A lightweight struct identifying a specific item by section and row (table) or section and item (collection). Used throughout the data source and delegate APIs to pinpoint cells.

Swift
let path = IndexPath(row: 2, section: 0)
tableView.scrollToRow(at: path, at: .middle, animated: true)

What is UICollectionViewDiffableDataSource and why is it preferred?

Medium

Introduced in iOS 13, it drives a collection (or table) view with a snapshot of identifiable items — eliminating manual performBatchUpdates and the crashes that come from mismatched insert/delete counts.

Swift
var dataSource: UICollectionViewDiffableDataSource<Section, Item>!

// Apply new state
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(newItems)
dataSource.apply(snapshot, animatingDifferences: true)

What is UICollectionViewCompositionalLayout?

Medium

A declarative layout API (iOS 13+) built from three nested primitives:
NSCollectionLayoutItem → NSCollectionLayoutGroup → NSCollectionLayoutSection

Swift
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5),
                                      heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)

let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                       heightDimension: .absolute(200))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

let section = NSCollectionLayoutSection(group: group)
return UICollectionViewCompositionalLayout(section: section)

How do you handle dynamic cell heights in a UITableView?

Hard

Set rowHeight to UITableView.automaticDimension and provide an estimatedRowHeight. The table view will use Auto Layout constraints inside the cell to compute the real height.

Swift
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 80 // close estimate avoids jumpy scroll

The cell's contentView subviews must have an unbroken constraint chain from top to bottom so the layout engine can derive a height.

What is the difference between performBatchUpdates and reloadData in UICollectionView?

Hard

reloadData: tears down and rebuilds all cells. Fast but no animation, loses scroll position, causes flicker.
performBatchUpdates: applies inserts, deletes, moves, and reloads in a single animated transaction. The item counts before + operations must reconcile with counts after — a mismatch causes a crash.

Prefer DiffableDataSource (apply snapshot) which handles this automatically and is crash-safe.

UIKit

Navigation & Presentation

What is the difference between push and modal presentation?

Easy

Push (UINavigationController): slides new VC onto a navigation stack. Back button returns automatically. Used for hierarchical flows.
Modal presentation: new VC covers the current context, detached from the navigation stack. Used for self-contained tasks (login, settings, confirmation).

Swift
// Push
navigationController?.pushViewController(detailVC, animated: true)

// Modal
present(settingsVC, animated: true)

What is UINavigationController?

Easy

A container view controller that manages a stack of child view controllers. It provides the navigation bar, back button, and push/pop transitions. The root is set at init; subsequent screens are pushed/popped.

What are the UIModalPresentationStyle options and when would you use each?

Medium

.fullScreen — covers entire screen; presenting VC is not visible.
.pageSheet — card-style sheet (default iOS 13+); presenting VC visible behind.
.formSheet — compact centred rectangle on iPad.
.overFullScreen — fullscreen but presenting VC stays in the hierarchy (transparent backgrounds show through).
.custom — use UIViewControllerTransitioningDelegate for fully custom transitions.

How does unwind segue work?

Medium

An unwind segue lets you navigate back to a previous view controller in the stack, even skipping several levels, without holding a reference to it.

1. Declare an @IBAction in the destination VC (where you want to return to).
2. Control-drag to the Exit icon in the source VC's storyboard scene.

Swift
// In DestinationViewController
@IBAction func unwindToHome(_ segue: UIStoryboardSegue) {
  // receive data from source if needed
}

How would you implement a custom view controller transition?

Hard

1. Set the presented VC's transitioningDelegate.
2. Return a UIViewControllerAnimatedTransitioning object from animationController(forPresented:presenting:source:).
3. Implement animateTransition(using:) to drive the animation.

Swift
class FadeTransition: NSObject, UIViewControllerAnimatedTransitioning {
  func transitionDuration(using ctx: UIViewControllerContextTransitioning?) -> TimeInterval { 0.35 }

  func animateTransition(using ctx: UIViewControllerContextTransitioning) {
    guard let toView = ctx.view(forKey: .to) else { return }
    ctx.containerView.addSubview(toView)
    toView.alpha = 0
    UIView.animate(withDuration: 0.35) {
      toView.alpha = 1
    } completion: { _ in
      ctx.completeTransition(!ctx.transitionWasCancelled)
    }
  }
}
UIKit

Responder Chain & Gestures

What is the responder chain?

Easy

A chain of UIResponder objects (UIView → UIViewController → UIWindow → UIApplication → AppDelegate) that passes unhandled events up until one responds. If no responder handles it, the event is discarded.

How do UIGestureRecognizers work?

Medium

A gesture recogniser monitors touch events and fires its action when a specific pattern is detected. Multiple recognisers can coexist on one view; you configure priority with requireGestureRecognizerToFail(_:) and delegate methods.

Swift
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
view.addGestureRecognizer(tap)

@objc func handleTap(_ sender: UITapGestureRecognizer) {
  print(sender.location(in: view))
}

What is the difference between hitTest(_:with:) and pointInside(_:with:)?

Medium

hitTest walks the view hierarchy depth-first to find the deepest view that wants to handle a touch. pointInside checks whether a point lies within a view's bounds. Override hitTest to expand/shrink the touchable area or redirect touches.

Swift
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
  // Expand tap target beyond bounds
  let expanded = bounds.insetBy(dx: -20, dy: -20)
  return expanded.contains(point) ? self : nil
}

How would you handle simultaneous gesture recognisers?

Hard

By default, only one gesture recogniser succeeds at a time. Implement UIGestureRecognizerDelegate to allow simultaneous recognition.

Swift
class VC: UIViewController, UIGestureRecognizerDelegate {
  override func viewDidLoad() {
    super.viewDidLoad()
    let pan  = UIPanGestureRecognizer(target: self, action: #selector(pan))
    let swipe = UISwipeGestureRecognizer(target: self, action: #selector(swipe))
    pan.delegate  = self
    swipe.delegate = self
    view.addGestureRecognizer(pan)
    view.addGestureRecognizer(swipe)
  }

  func gestureRecognizer(
    _ g: UIGestureRecognizer,
    shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer
  ) -> Bool { true }
}
UIKit

Animations

How do you perform a basic UIView animation?

Easy

UIView.animate provides a block-based API. Any animatable property changed inside the animations block is interpolated over the specified duration.

Swift
UIView.animate(withDuration: 0.3) {
  self.label.alpha = 0
  self.label.transform = CGAffineTransform(translationX: 0, y: -20)
}

What animatable properties does UIView support?

Medium

frame / bounds / center
transform (CGAffineTransform)
alpha
backgroundColor

Note: frame should be avoided when using Auto Layout — animate constraint constants and call layoutIfNeeded() instead.

What is UIViewPropertyAnimator and when would you use it over UIView.animate?

Medium

UIViewPropertyAnimator is interruptible and scrubbable — you can pause, reverse, or seek to any point during the animation. Use it for interactive transitions (finger-driven progress), spring animations with fine control, or when you need to add animations mid-flight.

Swift
let animator = UIViewPropertyAnimator(duration: 0.5, curve: .easeInOut) {
  self.cardView.transform = .identity
}
animator.addCompletion { _ in print("done") }
animator.startAnimation()

What is Core Animation and how does it relate to UIView animations?

Hard

UIView animations are a high-level wrapper over Core Animation (CALayer). Every UIView is backed by a CALayer — the actual rendering happens on a dedicated render thread, off the main thread.

To access Core Animation directly:

Swift
let anim = CABasicAnimation(keyPath: "shadowOpacity")
anim.fromValue = 0
anim.toValue   = 1
anim.duration  = 0.4
view.layer.add(anim, forKey: "shadow")
view.layer.shadowOpacity = 1 // set final value on model layer

CALayer properties not exposed by UIView (shadow, borderRadius, transform3D) require Core Animation directly.

UIKit

Custom Views & Drawing

How do you create a custom UIView subclass?

Easy

Subclass UIView, override draw(_:) for custom rendering, or add subviews in init. Always provide both initializers.

Swift
class BadgeView: UIView {
  var count = 0 { didSet { setNeedsDisplay() } }

  override init(frame: CGRect) {
    super.init(frame: frame)
    backgroundColor = .clear
  }

  required init?(coder: NSCoder) {
    super.init(coder: coder)
    backgroundColor = .clear
  }

  override func draw(_ rect: CGRect) {
    let path = UIBezierPath(ovalIn: rect)
    UIColor.red.setFill()
    path.fill()
  }
}

What is the difference between draw(_:) and layoutSubviews()?

Medium

draw(_:) is for custom Core Graphics rendering into the view's backing bitmap. It should be pure drawing — no layout, no subview management.

layoutSubviews() is for positioning and sizing subviews. Called after constraint solving. Never call draw(_:) directly; call setNeedsDisplay() to schedule it.

What is CALayer and how is it related to UIView?

Medium

Every UIView is backed by a CALayer. The view manages event handling and accessibility; the layer does the actual compositing and rendering on the GPU. You can access visual properties — corner radius, shadow, border, transforms — via view.layer.

Swift
view.layer.cornerRadius = 12
view.layer.shadowColor  = UIColor.black.cgColor
view.layer.shadowRadius = 8
view.layer.shadowOpacity = 0.2
view.clipsToBounds = false // needed for shadow to show outside bounds

How do you achieve smooth 60fps custom drawing?

Hard

1. Keep draw(_:) fast — avoid allocating objects, precompute paths.
2. Set clearsContextBeforeDrawing = false if you redraw the whole rect.
3. Only invalidate the dirty region: setNeedsDisplay(_:) with a rect.
4. Move heavy work to a background thread and render into an image, then display it on the main thread.
5. Use CAShapeLayer or CATextLayer instead of draw(_:) — they render on the GPU without redrawing on CPU.

UIKit

Auto Layout in Code

What are NSLayoutAnchor APIs?

Easy

A type-safe API for creating Auto Layout constraints in code. Anchors correspond to edges, centers, and dimensions of views.

Swift
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),
])

What is the Safe Area Layout Guide?

Medium

Represents the portion of the view not obscured by system chrome (navigation bar, tab bar, home indicator, notch). Use safeAreaLayoutGuide anchors instead of the view's edges to avoid content being hidden.

Swift
label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8)

What is the difference between NSLayoutConstraint.activate and constraint.isActive = true?

Medium

Both activate constraints, but NSLayoutConstraint.activate(_:) is preferred — it batches all activations, identifies the common ancestor view, and is more efficient. It also conveys intent more clearly.

Setting isActive = true one-by-one is equivalent but slightly less efficient and more verbose.

What is layout priority and when would you use it?

Hard

Priority (UILayoutPriority, 1–1000) tells the constraint solver how hard to try to satisfy a constraint. Required (1000) must be satisfied. Lower priorities are broken before higher ones when conflicts arise.

Swift
let heightConstraint = view.heightAnchor.constraint(equalToConstant: 100)
heightConstraint.priority = .defaultHigh // 750
heightConstraint.isActive = true

// A required constraint elsewhere can override this one
// without an 'Unable to simultaneously satisfy' error

Common use: letting intrinsic size 'win' over a soft constraint, or building compression-resistance behaviour.

Architecture Patterns

Overview

What are the main architectural patterns used in iOS development?

Medium

MVC is Apple's default but often leads to massive view controllers due to weak separation of concerns. MVVM improves this by introducing a ViewModel that handles presentation logic and enables better testability. VIPER takes it further by strictly separating responsibilities into multiple layers, making it highly modular and testable but more complex and verbose.

Memory Management

References & ARC

Explain the difference between strong, weak, and unowned references.

Medium

Strong increases the reference count and prevents deallocation. Weak doesn't increase the count and becomes nil when the object deallocates. Unowned is like weak but non-optional — crashes if the object is deallocated.

• Every property is strong by default
• As long as one strong reference exists, the object stays alive
• Two objects holding strong references to each other → retain cycle → memory leak

What is ARC in iOS Development?

Easy

ARC (Automatic Reference Counting) automatically manages heap memory by tracking strong references to class instances. When the strong reference count drops to zero, the object is deallocated. ARC is a compile-time mechanism — no runtime garbage collector.

Concurrency & Threading

Basics

What's the difference between synchronous and asynchronous tasks in iOS?

Easy

Synchronous tasks block the calling thread until completion, potentially freezing the UI if run on the main thread. Asynchronous tasks return immediately, running concurrently and notifying via callbacks, delegates, or async/await.

App Lifecycle & System Knowledge

App Lifecycle

Describe the app lifecycle in iOS.

Easy

Not Running → Inactive → Active → Background → Suspended.

Active: foreground, receiving events.
Inactive: transitioning (e.g. notification appears).
Background: executing code after leaving foreground.
Suspended: in memory, no code running — killed by OS if memory is needed.