Value vs Reference Types
What's the difference between struct and class?
EasyStruct 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?
EasyPrefer 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)?
EasyA 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?
MediumNo. 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?
MediumThe 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?
HardUse isKnownUniquelyReferenced() before any mutation. If the reference isn't unique, copy the storage first.
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?
HardAn 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.
Optionals
What is an optional?
EasyA 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?
Easyif 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?
EasyCrashes 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?
EasyDeclared 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?
MediumA way to call properties and methods on an optional that returns nil gracefully if any link is nil, instead of crashing.
let city = user?.address?.city // nil if any part is nilWhat does the nil-coalescing operator (??) do?
MediumReturns the unwrapped optional if it has a value, otherwise returns a default.
let name = user.name ?? "Anonymous"What is the difference between map and flatMap on an optional?
Mediummap transforms the wrapped value and re-wraps it: Optional<T> → Optional<U>.
flatMap also transforms but the closure returns an optional, preventing double-wrapping.
let str: String? = "42"
let num = str.flatMap { Int($0) } // Int?, not Int??How does Swift handle Optional in a generic context?
HardOptional<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.
func firstInt(_ values: [String?]) -> Int? {
values.lazy.compactMap { $0 }.compactMap { Int($0) }.first
}Closures
What is a closure?
EasyA self-contained block of code that can capture and store references to variables from its surrounding context.
let add = { (a: Int, b: Int) -> Int in
a + b
}What is a capture list?
EasyA list in square brackets at the start of a closure that declares how external values are captured.
{ [weak self, count] in
self?.update(count)
}Escaping vs non-escaping closures?
MediumNon-escaping (default): executed synchronously within the function body.
Escaping (@escaping): stored or called after the function returns — required for async callbacks.
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?
MediumA retain cycle forms when a closure captures self strongly and self holds a strong reference to that closure. Neither can be deallocated.
// Retain cycle
self.onComplete = { self.dismiss() }
// Fixed
self.onComplete = { [weak self] in self?.dismiss() }What is @autoclosure?
MediumWraps an expression in a closure automatically, delaying evaluation until the closure is called. Used for short-circuit operators and conditional logging.
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.
// 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?
HardSwift 5.3+ lets you write multiple closure arguments after the closing parenthesis, with labeled syntax for all but the first.
UIView.animate(withDuration: 0.3) {
view.alpha = 0
} completion: { finished in
view.removeFromSuperview()
}Protocols
What is a protocol in Swift?
EasyA 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?
EasyYes. 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?
MediumExtensions on a protocol that provide default method implementations. Conforming types get these for free but can override them.
protocol Greetable {
var name: String { get }
}
extension Greetable {
func greet() { print("Hello, \(name)!") }
}What are associated types?
MediumA placeholder type defined in a protocol, resolved concretely by the conforming type.
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)?
MediumA 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'?
Hardsome 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.
// 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?
HardIntroduced in Swift 5.7 — lets you constrain associated types in angle brackets on existentials, similar to generics.
// Before Swift 5.7
func process<C: Collection>(_ items: C) where C.Element == Int { }
// Swift 5.7+
func process(_ items: any Collection<Int>) { }Generics
What are generics in Swift?
EasyA way to write flexible, reusable code that works with any type while maintaining type safety. The compiler generates specialised versions at compile time.
func swap<T>(_ a: inout T, _ b: inout T) {
let tmp = a
a = b
b = tmp
}What is a generic constraint?
MediumA restriction on a type parameter requiring protocol conformance or a specific superclass.
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?
MediumA 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?
HardParameter packs let functions and types accept a variable number of generic type parameters, eliminating combinatorial overloads.
func printAll<each T>(_ values: repeat each T) {
repeat print(each values)
}
printAll(1, "hello", true) // works with any number of argsError Handling
How does error handling work in Swift?
EasyFunctions that can fail are marked throws. Callers use try inside a do-catch block. Errors conform to the Error protocol.
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!?
Easytry: 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.
let a = try parseJSON(data) // throws on failure
let b = try? parseJSON(data) // nil on failure
let c = try! parseJSON(data) // crash on failureWhat is the Result type?
MediumAn enum with .success(Value) and .failure(Error) cases for representing outcomes without throwing. Useful for async callbacks.
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?
MediumUse 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?
Hardthrows(SpecificError) lets a function declare the exact error type it throws, enabling exhaustive catch blocks without casting.
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
}Enums
What is an enum in Swift?
EasyA 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?
EasyA pre-set primitive value associated with each case.
enum Planet: Int {
case mercury = 1, venus, earth
}
Planet(rawValue: 3) // Optional(.earth)
Planet.venus.rawValue // 2What are associated values?
MediumData attached to a specific case, allowing each case to carry different types of data.
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?
MediumAn enum where cases hold a value of the same enum type. indirect enables recursive data structures by using a reference internally.
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?
MediumAutomatic synthesis of an allCases static property containing all cases in declaration order.
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?
HardSwift uses switch, if case, and guard case to destructure enum cases and bind associated values. where clauses add extra conditions.
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)
}Collections & Functional Programming
What are the main collection types in Swift?
EasyArray: 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?
Mediummap: transforms each element, same count.
filter: keeps elements matching a predicate.
reduce: folds all elements into a single value.
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, +) // 15What is a lazy sequence?
MediumA sequence that defers element computation until actually consumed — no intermediate arrays are allocated.
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 firstWhat does compactMap do?
MediumTransforms each element and removes nils in a single pass — more efficient than map + filter.
let strings = ["1", "two", "3", "four"]
let ints = strings.compactMap { Int($0) } // [1, 3]How would you implement a custom Sequence?
HardConform to Sequence by implementing makeIterator(), returning a type conforming to IteratorProtocol with a mutating next() -> Element? method.
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, 1Access Control
What are Swift's access control levels?
EasyFrom 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?
Mediumprivate 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?
MediumBoth allow access from outside the module. open additionally allows subclassing and overriding externally.
// 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?
HardExtensions in the same file as the type can access private members. You can set a default access level for all members of an extension.
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
}
}Property Wrappers
What is a property wrapper?
EasyA 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?
MediumDefine a struct/class with @propertyWrapper and implement wrappedValue.
@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 100What is projectedValue in a property wrapper?
MediumAn optional secondary value exposed via the $ prefix on the property.
@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?
HardCannot 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.
View Hierarchy & Layout
What is the difference between a view's frame and its bounds?
Easyframe: 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).
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 coordsRotating a view changes frame (bounding box grows) but not bounds.
What is the UIView hierarchy?
EasyUIView 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?
EasyA 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?
MediumIntrinsic 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?
MediumUIKit 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()?
MediumsetNeedsLayout() 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.
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?
HardWhen 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.
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?
Hard1. 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:
let c = label.topAnchor.constraint(equalTo: view.topAnchor, constant: 20)
c.identifier = "label-top"
c.isActive = true4. Break on UIViewAlertForUnsatisfiableConstraints in the debugger.
View Controller Lifecycle
What are the main UIViewController lifecycle methods?
EasyviewDidLoad() — 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?
EasyviewDidLoad 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?
MediumviewDidLoad 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?
MediumCalled 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.
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?
HardA 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.
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
}Table Views & Collection Views
How does UITableView cell reuse work?
EasyCells 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.
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?
EasyUITableView: 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?
MediumA 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.
let path = IndexPath(row: 2, section: 0)
tableView.scrollToRow(at: path, at: .middle, animated: true)What is UICollectionViewDiffableDataSource and why is it preferred?
MediumIntroduced 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.
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?
MediumA declarative layout API (iOS 13+) built from three nested primitives:
NSCollectionLayoutItem → NSCollectionLayoutGroup → NSCollectionLayoutSection
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?
HardSet rowHeight to UITableView.automaticDimension and provide an estimatedRowHeight. The table view will use Auto Layout constraints inside the cell to compute the real height.
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 80 // close estimate avoids jumpy scrollThe 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?
HardreloadData: 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.
Navigation & Presentation
What is the difference between push and modal presentation?
EasyPush (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).
// Push
navigationController?.pushViewController(detailVC, animated: true)
// Modal
present(settingsVC, animated: true)What is UINavigationController?
EasyA 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?
MediumAn 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.
// In DestinationViewController
@IBAction func unwindToHome(_ segue: UIStoryboardSegue) {
// receive data from source if needed
}How would you implement a custom view controller transition?
Hard1. Set the presented VC's transitioningDelegate.
2. Return a UIViewControllerAnimatedTransitioning object from animationController(forPresented:presenting:source:).
3. Implement animateTransition(using:) to drive the animation.
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)
}
}
}Responder Chain & Gestures
What is the responder chain?
EasyA 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?
MediumA 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.
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:)?
MediumhitTest 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.
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?
HardBy default, only one gesture recogniser succeeds at a time. Implement UIGestureRecognizerDelegate to allow simultaneous recognition.
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 }
}Animations
How do you perform a basic UIView animation?
EasyUIView.animate provides a block-based API. Any animatable property changed inside the animations block is interpolated over the specified duration.
UIView.animate(withDuration: 0.3) {
self.label.alpha = 0
self.label.transform = CGAffineTransform(translationX: 0, y: -20)
}What animatable properties does UIView support?
Mediumframe / 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?
MediumUIViewPropertyAnimator 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.
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?
HardUIView 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:
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 layerCALayer properties not exposed by UIView (shadow, borderRadius, transform3D) require Core Animation directly.
Custom Views & Drawing
How do you create a custom UIView subclass?
EasySubclass UIView, override draw(_:) for custom rendering, or add subviews in init. Always provide both initializers.
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()?
Mediumdraw(_:) 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?
MediumEvery 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.
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 boundsHow do you achieve smooth 60fps custom drawing?
Hard1. 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.
Auto Layout in Code
What are NSLayoutAnchor APIs?
EasyA type-safe API for creating Auto Layout constraints in code. Anchors correspond to edges, centers, and dimensions of views.
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?
MediumRepresents 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.
label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8)What is the difference between NSLayoutConstraint.activate and constraint.isActive = true?
MediumBoth 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?
HardPriority (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.
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' errorCommon use: letting intrinsic size 'win' over a soft constraint, or building compression-resistance behaviour.
Overview
What are the main architectural patterns used in iOS development?
MediumMVC 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.
References & ARC
Explain the difference between strong, weak, and unowned references.
MediumStrong 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?
EasyARC (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.
Basics
What's the difference between synchronous and asynchronous tasks in iOS?
EasySynchronous 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
Describe the app lifecycle in iOS.
EasyNot 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.