Skip to main content

iOS Development

This guide covers iOS-specific concepts that appear in mobile engineering interviews.

Swift Language Fundamentals

Value Types and Reference Types

Swift distinguishes between value types and reference types. Structs, enums, and tuples are value types. Classes are reference types.

Value types are copied when assigned to a new variable or passed to a function. Reference types share a single instance.

Value type example (struct): When assigning a struct variable to another variable, a copy is created. Modifying the copy does not affect the original. For example, copying a Point struct and modifying the copy's x value leaves the original's x unchanged.

Reference type example (class): When assigning a class instance to another variable, both variables reference the same instance. Modifying through one reference affects all references. For example, assigning a Counter class instance to another variable and modifying the count changes the value seen through both variables.

Swift Standard Library Types:

  • Array, Dictionary, String, and Set are structs (value types)
  • These types use copy-on-write optimization; copying only occurs on mutation
  • Classes support inheritance; structs do not
  • Structs receive an automatic memberwise initializer

Optionals

Optionals represent values that may be absent. An optional either contains a value or contains nil.

Unwrapping techniques:

  • Force unwrapping (!): Extracts the value; crashes at runtime if nil
  • Optional binding (if let): Safely unwraps into a new constant within an if block
  • Guard statement (guard let): Safely unwraps or exits the current scope early
  • Nil coalescing (??): Provides a default value when the optional is nil
  • Optional chaining (?.property): Accesses properties or methods on optionals; returns an optional result

Optional Implementation: Optional<T> is an enum with two cases: .some(T) and .none. The value nil is equivalent to .none.

Closures

Closures are self-contained blocks of functionality that can be passed and used in code.

Syntax variations:

  • Full syntax: Specify parameter names, types, return type, and use in keyword to separate signature from body
  • Shorthand: Use $0, $1, etc. for anonymous parameters; type inference handles types
  • Trailing closure: When a closure is the last argument, place it after the parentheses

Capture Semantics:

Closures capture references to variables and constants from their surrounding context.

Strong capture: Without specifying capture behavior, closures capture self strongly. In a Timer callback, this creates a retain cycle because Timer retains the closure, the closure retains self, and self retains the Timer.

Weak capture: Use [weak self] in the capture list to break retain cycles. Access self with optional chaining (self?.property) since weak references become nil when the referenced object is deallocated.

Closure Attributes:

  • @escaping: Closure may be called after the function returns
  • Non-escaping closures can capture self implicitly
  • Escaping closures require explicit self reference

Protocols

Protocols define a blueprint of methods, properties, and requirements for conforming types.

Default implementations: Define a protocol with required methods. Use a protocol extension to provide default implementations. Conforming types can use the default or provide their own implementation.

Protocol Features:

  • Protocols can have associated types using associatedtype
  • Protocol extensions provide default implementations
  • Multiple protocol conformance is supported
  • Protocols with associated types cannot be used as standalone types

Associated Types: Define a protocol with an associatedtype placeholder. Conforming types specify the concrete type either through typealias or through type inference from method implementations. For example, a Container protocol with an Item associated type can be implemented with Int as the Item type.

Memory Management

iOS uses Automatic Reference Counting (ARC) for memory management. ARC tracks the number of strong references to each class instance and deallocates instances when the count reaches zero.

Reference Counting

ARC tracks strong references to each instance. When an instance is created and assigned to a variable, the reference count is 1. Each additional strong reference increases the count. Setting a reference to nil decreases the count. When the count reaches 0, ARC calls deinit and deallocates the instance.

Retain Cycles

A retain cycle occurs when two or more objects hold strong references to each other, preventing deallocation.

Example: A Person class has an optional Apartment property. An Apartment class has a strong tenant property referencing Person. When Person holds a reference to Apartment and Apartment holds a strong reference back to Person, neither can be deallocated even when external references are removed.

Solutions:

Reference TypeBehaviorUse Case
weakDoes not increase reference count, becomes nil when target deallocatesReferenced object may be deallocated first
unownedDoes not increase reference count, assumes target is always validReferenced object will never be deallocated before the referencing object

Making the Apartment's tenant property weak breaks the retain cycle; when the Person is deallocated, the weak reference automatically becomes nil.

Common Retain Cycle Scenarios

Closures: Network callbacks and other escaping closures that reference self should capture self weakly using [weak self] in the capture list. Access properties through optional chaining.

Delegates: Define delegate protocols as constrained to AnyObject to enable weak references. Declare delegate properties as weak to prevent the delegate holder from retaining its delegate.

Timers: Timer retains its closure. Capture self weakly in the timer callback. Invalidate the timer in deinit to allow deallocation.

UIKit

View Controller Lifecycle

UIViewController manages a view hierarchy and coordinates with the rest of the application.

MethodWhen Called
loadView()Creates the view hierarchy (override only when not using storyboards)
viewDidLoad()View hierarchy loaded into memory
viewWillAppear(_:)View is about to be added to view hierarchy
viewDidAppear(_:)View has been added to view hierarchy
viewWillDisappear(_:)View is about to be removed from view hierarchy
viewDidDisappear(_:)View has been removed from view hierarchy

Considerations:

  • viewDidLoad() is called once per view controller instance
  • viewWillAppear(_:) is called each time the view becomes visible
  • View geometry is not finalized in viewDidLoad()
  • Use viewDidLayoutSubviews() for geometry-dependent calculations

Auto Layout

Auto Layout uses constraints to define relationships between views.

Programmatic constraints: Use anchor-based APIs (topAnchor, leadingAnchor, etc.) to create constraints relative to other views or layout guides. Specify constants for margins. Activate multiple constraints at once using NSLayoutConstraint.activate with an array of constraints.

Intrinsic Content Size: Views can define a natural size based on their content. UILabel's intrinsic size is based on its text. Views with intrinsic content size do not require explicit width or height constraints.

Content Priorities:

PriorityDescriptionDefault Value
Content HuggingResistance to growing larger than intrinsic size250
Compression ResistanceResistance to shrinking smaller than intrinsic size750

Table View and Collection View

UITableView and UICollectionView display scrollable lists of content.

UITableViewDataSource implementation: Implement numberOfRowsInSection to return the data count. Implement cellForRowAt to dequeue a reusable cell, configure it with data for the given index path, and return it.

Cell Reuse:

  • dequeueReusableCell(withIdentifier:for:) returns a reused cell or creates a new one
  • prepareForReuse() is called before a cell is reused; reset cell state here
  • iOS 13+ provides UITableViewDiffableDataSource for automatic animations

SwiftUI

SwiftUI is a declarative framework for building user interfaces.

State Management

Property WrapperPurpose
@StateLocal state owned by the view
@BindingTwo-way connection to parent's state
@ObservedObjectExternal observable object (not owned)
@StateObjectExternal observable object (owned by view)
@EnvironmentObjectShared data injected through environment

@State example: Declare a private @State property to hold local view state. Access and modify the property in the view body. SwiftUI automatically re-renders the view when the state changes.

@Binding example: Parent views pass state down using the $ prefix to create a binding. Child views declare @Binding properties to receive two-way connections to parent state. Changes in the child propagate back to the parent.

@StateObject vs @ObservedObject:

  • @StateObject creates and owns the object; it persists across view recreation
  • @ObservedObject does not own the object; a new object may be created when the view is recreated
  • Use @StateObject for initialization, @ObservedObject for injection

View Identity

SwiftUI uses identity to determine which views changed between updates.

Structural identity: Views are identified by their position in the view hierarchy. In a VStack, the first child has identity based on being at index 0, the second child at index 1, and so on.

Explicit identity: When using ForEach, provide an id parameter to uniquely identify each item. SwiftUI uses these IDs to track items across updates, enabling correct animations and state preservation.

View Updates:

  • SwiftUI compares view values to determine what changed
  • Views are structs; creating them is inexpensive
  • body is re-evaluated when dependencies change

Concurrency

Grand Central Dispatch

GCD provides queues for executing work concurrently.

Usage pattern: Dispatch work to a global queue with an appropriate quality of service for background processing. When the work completes, dispatch back to the main queue to update the UI.

Quality of Service (highest to lowest priority):

  1. .userInteractive - UI updates
  2. .userInitiated - User is waiting for results
  3. .default - Default priority
  4. .utility - Long-running tasks with progress indication
  5. .background - User is not waiting

Swift Concurrency

Swift 5.5 introduced structured concurrency with async/await.

async/await pattern: Mark functions that perform asynchronous work with async. Use try await to call async functions that can throw. Wrap async calls in a Task block to call from synchronous code.

Actors:

Actors protect mutable state from data races. Only one task can access an actor's state at a time. Declare an actor with the actor keyword. Properties and methods are isolated by default. External access requires await because calls are queued.

@MainActor:

The @MainActor attribute ensures code runs on the main thread. Apply it to classes like ViewModels to ensure all property updates happen on the main thread. Async methods within @MainActor classes return to the main thread after await.

Common Interview Topics

frame vs bounds:

  • frame: Position and size in the superview's coordinate system
  • bounds: Position and size in the view's own coordinate system (origin typically 0,0)

Responder Chain: The chain of objects that can respond to events. Events propagate from the first responder up through the view hierarchy to the view controller, window, and application.

Equality Operators:

  • ==: Value equality (Equatable conformance)
  • ===: Reference equality (same instance, classes only)

@objc: Exposes Swift declarations to the Objective-C runtime. Required for:

  • Selectors (#selector)
  • Key-value observing
  • Interface Builder connections

Copy-on-Write: Value types in Swift use copy-on-write optimization. The actual copy is deferred until mutation occurs. When an array is assigned to a new variable, both variables share the same underlying storage. Only when one is mutated does Swift create a separate copy.

App Delegate vs Scene Delegate:

  • App Delegate: Application lifecycle (launch, terminate, push notification registration)
  • Scene Delegate: UI lifecycle per window (iOS 13+, supports multiple windows on iPad)

lazy: Lazy properties are initialized on first access. Declare with the lazy keyword followed by var. The initialization expression is evaluated only when the property is first accessed. Properties marked lazy must be var and are not thread-safe.

Data Persistence:

MethodUse Case
UserDefaultsSmall values, user preferences
KeychainSecure data (tokens, passwords)
File SystemDocuments, caches
Core DataComplex object graphs
SQLiteDirect database access

Deep Links:

  • URL Schemes: myapp://path
  • Universal Links: https://mysite.com/path (requires apple-app-site-association file)

Handle in application(_:open:options:) for URL schemes or application(_:continue:restorationHandler:) for Universal Links.

layoutIfNeeded(): Forces an immediate layout pass. Call within UIView.animate blocks to animate constraint changes. First modify the constraint constant, then call layoutIfNeeded() on the view to trigger the animated layout update.