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
inkeyword 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
selfimplicitly - Escaping closures require explicit
selfreference
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 Type | Behavior | Use Case |
|---|---|---|
weak | Does not increase reference count, becomes nil when target deallocates | Referenced object may be deallocated first |
unowned | Does not increase reference count, assumes target is always valid | Referenced 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.
| Method | When 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 instanceviewWillAppear(_:)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:
| Priority | Description | Default Value |
|---|---|---|
| Content Hugging | Resistance to growing larger than intrinsic size | 250 |
| Compression Resistance | Resistance to shrinking smaller than intrinsic size | 750 |
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 oneprepareForReuse()is called before a cell is reused; reset cell state here- iOS 13+ provides
UITableViewDiffableDataSourcefor automatic animations
SwiftUI
SwiftUI is a declarative framework for building user interfaces.
State Management
| Property Wrapper | Purpose |
|---|---|
@State | Local state owned by the view |
@Binding | Two-way connection to parent's state |
@ObservedObject | External observable object (not owned) |
@StateObject | External observable object (owned by view) |
@EnvironmentObject | Shared 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:
@StateObjectcreates and owns the object; it persists across view recreation@ObservedObjectdoes not own the object; a new object may be created when the view is recreated- Use
@StateObjectfor initialization,@ObservedObjectfor 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
bodyis 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):
.userInteractive- UI updates.userInitiated- User is waiting for results.default- Default priority.utility- Long-running tasks with progress indication.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 systembounds: 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:
| Method | Use Case |
|---|---|
| UserDefaults | Small values, user preferences |
| Keychain | Secure data (tokens, passwords) |
| File System | Documents, caches |
| Core Data | Complex object graphs |
| SQLite | Direct 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.