iOS Senior Interview — Detailed Answers

A senior-level reference covering Swift, architecture, Xcode/UIKit, design, and frameworks. Each answer is written at a depth meant to demonstrate not just what the right answer is, but why it's right and when the trade-offs matter.


Technical — Swift Language

What is a strong reference cycle?

A strong reference cycle (a.k.a. retain cycle) happens when two or more reference-typed instances each hold a strong reference to the other, so their reference counts never drop to zero. ARC can't deallocate them, and the memory leaks for the lifetime of the process.

The classic two-object cycle:

final class Person {
    let name: String
    var apartment: Apartment?     // strong
    init(name: String) { self.name = name }
    deinit { print("\(name) deinit") }
}

final class Apartment {
    let unit: String
    var tenant: Person?           // strong → cycle
    init(unit: String) { self.unit = unit }
    deinit { print("Apt \(unit) deinit") }
}

var simran: Person? = Person(name: "Simran")
var apt: Apartment? = Apartment(unit: "4B")
simran?.apartment = apt
apt?.tenant = simran

simran = nil
apt = nil
// Neither deinit prints — both are leaked.

The most common real-world cause in iOS is closures capturing self strongly, especially in long-lived closures stored as properties (network handlers, Combine subscriptions, NotificationCenter blocks, Timer callbacks):

final class FeedViewModel {
    var onLoad: (() -> Void)?
    func setup() {
        onLoad = { self.refresh() }   // self → closure → self
    }
    func refresh() { /* ... */ }
}

Ways to avoid them

  1. Capture lists with [weak self] or [unowned self] in closures.
  2. weak references for relationships where the referent can outlive (or at least independently disappear from) the holder — delegates, parent ↔ child where child shouldn't pin the parent.
  3. unowned references when the lifetime relationship guarantees the referent will always outlive the holder.
  4. Break ownership by design — restructure so one side doesn't hold a reference at all (e.g., closure-based callbacks rather than stored references, IDs instead of object pointers, observer patterns where the subject doesn't retain observers).
  5. Use value types (structs/enums) where possible — they don't participate in ARC and can't form cycles.
  6. Manually nil out references at known teardown points (e.g., viewWillDisappear, custom cleanup() methods) when ownership inversion isn't practical.
final class FeedViewModel {
    var onLoad: (() -> Void)?
    func setup() {
        onLoad = { [weak self] in self?.refresh() }
    }
    func refresh() { /* ... */ }
}

Where do you see "weak, unowned, and strong" used? What are the differences?

Increments retain count? Optional? What happens when target deallocates?
strong (default) Yes Either N/A — keeps target alive
weak No Must be optional var Automatically set to nil (zeroing weak reference)
unowned No Non-optional Dangling pointer; crash on access (unowned(safe) traps; unowned(unsafe) is UB)

Where you see them:

A senior heuristic: default to weak. Reach for unowned only when you can articulate why the lifetime invariant truly holds; otherwise the small cost of unwrapping self? is cheaper than the rare-but-painful crash.

What is an optional in Swift and what problem do optionals solve?

An Optional<T> is an enum with two cases:

@frozen public enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}

T? is sugar for Optional<T>.

The problem they solve: in languages with implicit nullability (Objective-C, Java pre-Optional, C, etc.), every reference can be null and the compiler can't help you tell the difference. Tony Hoare called null references his "billion-dollar mistake" because of the volume of bugs they've caused.

Swift moves the nullability decision to the type system. A String is guaranteed to hold a value; a String? might not. The compiler refuses to let you treat them interchangeably and forces you to handle the absence explicitly. That converts a class of runtime crashes into compile-time errors.

It also makes intent self-documenting: a function returning User? is communicating "the user might not exist" without reading docs.

// Objective-C era: caller has no idea this can be nil
func findUser(id: String) -> User { ... }

// Swift: contract is in the signature
func findUser(id: String) -> User? { ... }

What are the various ways to unwrap an optional? How do they rate in terms of safety?

From safest to least safe:

1. Optional binding (if let / guard let)safest, idiomatic:

if let user = currentUser {
    print(user.name)
}

guard let user = currentUser else { return }
process(user)

Swift 5.7's shorthand is even cleaner: if let user { ... }.

2. Nil-coalescing ??safe, when a default makes sense:

let displayName = user?.name ?? "Guest"

3. Optional chaining ?.safe, propagates nil through a chain:

let cityLength = user?.address?.city?.count   // Int?

4. switch / pattern matchingsafe, explicit about both cases:

switch result {
case .some(let value): handle(value)
case .none: handleMissing()
}

5. Optional map / flatMapsafe, functional style:

let upper = name.map { $0.uppercased() }       // String? → String?
let parsed = idString.flatMap(Int.init)        // String? → Int?

6. Implicitly unwrapped optionals (T!)unsafe, but useful in narrow cases:

@IBOutlet weak var titleLabel: UILabel!

Loaded but not nil after viewDidLoad — using ! here avoids unwrapping noise everywhere, at the cost of a crash if you misuse it.

7. Force unwrap !least safe:

let value = currentUser!.name   // crashes if nil

Acceptable when (a) you can prove the optional is non-nil from program invariants, (b) the alternative would be a meaningless default, and (c) crashing early is the right behavior. URL(string: "https://constant.url")! is fine. someUserInput! almost never is. A senior dev should be able to defend every ! they leave in code.

What are the main differences between structures and classes?

Aspect struct class
Semantics Value (copied on assignment/passing) Reference (shared)
Identity None — equality is structural Identity (===) distinct from equality
Inheritance None Single inheritance
deinit No Yes
Memberwise init Synthesized No
Mutation Methods need mutating keyword Methods mutate by default
ARC No (lives on stack typically; copy semantics) Yes (heap, retain/release)
Thread safety Easier — no shared mutable state by default Manual — shared instances need synchronization
let instance Properties are immutable Properties of let instance can still be mutated
struct Point { var x, y: Double }
class Box { var value: Int = 0 }

let p = Point(x: 1, y: 2)
// p.x = 5         // ❌ compiler error — let struct is immutable

let b = Box()
b.value = 5         // ✅ — let class is constant binding, not constant content

When to choose which (Swift's own guidance):

Reference types vs value types

Beyond the table above, the deeper implications:

Value types give you local reasoning. When you hand a [User] to a function, you know the function can't mutate your copy. That eliminates a huge class of "spooky action at a distance" bugs and makes concurrent code dramatically easier to reason about.

Reference types give you shared mutable state, which is sometimes exactly what you want — multiple objects observing the same model, a single source of truth — but it's a feature you should opt into deliberately.

Subtleties seniors should know:

What are generics and what problem do they solve?

Generics let you write code that works with any type while preserving full type safety, instead of forcing you to either duplicate the code per type or fall back to type-erased containers (Any) and runtime checks.

The problem they solve: before generics, you'd write the same algorithm five times for five concrete types, or you'd write it once over Any and lose type safety, paying for casts everywhere.

// Without generics — code duplication
func swapInts(_ a: inout Int, _ b: inout Int) { let t = a; a = b; b = t }
func swapStrings(_ a: inout String, _ b: inout String) { ... }

// With generics — written once, type-safe
func swap<T>(_ a: inout T, _ b: inout T) { let t = a; a = b; b = t }

Generics shine in collections (Array<Element>, Dictionary<Key, Value>), algorithms (Sequence.map<U>(_:)), and abstractions (Result type, Combine's Publisher<Output, Failure>).

You constrain generics with where clauses and protocol conformance:

func sorted<T: Comparable>(_ xs: [T]) -> [T] { ... }

func areEqual<T: Equatable>(_ a: T, _ b: T) -> Bool { a == b }

extension Array where Element: Numeric {
    func sum() -> Element { reduce(0, +) }
}

A real-world example from generic networking — one definition that types-safely returns whatever model the call site asks for:

func fetch<T: Decodable>(_ endpoint: Endpoint, as: T.Type) async throws -> T {
    let (data, _) = try await URLSession.shared.data(for: endpoint.request)
    return try JSONDecoder().decode(T.self, from: data)
}

let user: User = try await fetch(.profile, as: User.self)
let feed: [Post] = try await fetch(.feed, as: [Post].self)

Swift 5.7+ added primary associated types and opaque/existential any improvements that make generic APIs much more ergonomic — some Collection<Int>, any Identifiable<UUID>, etc.

What is lazy loading?

Lazy loading defers initialization of a value until the first time it's actually accessed. In Swift this is the lazy keyword on a stored property:

final class ImageGallery {
    lazy var thumbnailGenerator: ThumbnailGenerator = {
        // Only constructed the first time .thumbnailGenerator is read
        return ThumbnailGenerator(quality: .high)
    }()
}

Why use it:

Caveats:

DateFormatters are a classic case — expensive to construct, often used many times:

private lazy var iso8601: ISO8601DateFormatter = {
    let f = ISO8601DateFormatter()
    f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
    return f
}()

Enums: Associated Types vs Raw Values

These are easy to confuse but solve different problems.

Raw values — every case is backed by a single literal of a single type (Int, String, etc.). The mapping case ↔ raw is fixed at compile time:

enum HTTPStatus: Int {
    case ok = 200
    case notFound = 404
    case serverError = 500
}

let s = HTTPStatus(rawValue: 404)   // → .notFound
print(HTTPStatus.ok.rawValue)        // → 200

Common uses: serializing to/from APIs and disk, bridging to C constants, Codable synthesizes Codable for free when the raw type is Codable.

Associated values — each instance of a case can carry payload data, and different cases can carry different shapes of data. This is what makes Swift enums sum types / tagged unions:

enum LoadState<T> {
    case idle
    case loading
    case loaded(T)
    case failed(Error)
}

let state: LoadState<[User]> = .loaded([alice, bob])

switch state {
case .idle:                 break
case .loading:              showSpinner()
case .loaded(let users):    render(users)
case .failed(let error):    showError(error)
}

Key differences:

Raw values Associated values
Stored where? One value per case (compile-time) Different value per instance (runtime)
All cases same type? Yes No, each case can be different
Can have both? No (mutually exclusive)
Init from data? init?(rawValue:) synthesized Must construct case explicitly

A senior tip: associated values are how you model state in Swift. Replacing booleans-and-optionals with a state enum that has associated values is one of the highest-ROI refactors you can do — LoadState above replaces var isLoading: Bool, users: [User]?, error: Error? with one variable that the compiler can exhaustively check.

Are you familiar with the Swift API Design Guidelines?

Yes — they're maintained at swift.org/documentation/api-design-guidelines and they're the unofficial style of the standard library.

The headline principles:

  1. Clarity at the point of use is the most important goal. Code is read far more often than written.
  2. Clarity is more important than brevity. Don't sacrifice one for the other; do prefer compact code when both are achievable.
  3. Write a documentation comment for every declaration. Writing the docs surfaces unclear APIs.

Concrete naming guidance worth memorizing:

// Following the guidelines
extension Array {
    mutating func remove(at index: Int) -> Element        // verb, mutates
    func removingFirst() -> [Element]                      // -ing, non-mutating
    var isEmpty: Bool                                      // assertion property
}

Architecture

What is MVC? What other design patterns do you use?

MVC = Model / View / Controller. It separates data (Model) from presentation (View) with a Controller that binds them. Apple's interpretation as it ships in UIKit puts UIViewController between the view hierarchy and the model and ends up coupling them tightly — the View is owned by the VC, the VC also drives navigation, formats display data, fetches data, and handles input. That's the well-documented "Massive View Controller" failure mode.

A "thin" MVC where the controller really is just glue and the model is rich is workable. The trouble is that UIKit nudges you toward the opposite.

Other patterns I reach for routinely:

MVVM definition and benefits

MVVM = Model / View / ViewModel.

The View binds to the ViewModel — historically via delegates/closures, now via Combine, @Observable, or async streams.

@Observable
final class ProfileViewModel {
    private(set) var state: LoadState<DisplayProfile> = .idle
    private let repository: ProfileRepository

    init(repository: ProfileRepository) { self.repository = repository }

    func load() async {
        state = .loading
        do {
            let user = try await repository.currentUser()
            state = .loaded(DisplayProfile(name: user.fullName,
                                            joined: Self.dateFormatter.string(from: user.joinDate)))
        } catch {
            state = .failed(error)
        }
    }
}

The View just renders state and calls load() — no formatting, no networking, no business decisions live in the View.

Benefits:

  1. Testability — ViewModels are plain Swift types testable without a host application or UI runtime. You can unit-test all your presentation logic.
  2. Separation of concerns — formatting, state, and business logic live somewhere that isn't a UIViewController.
  3. Reuse — the same ViewModel can drive a UIKit and a SwiftUI implementation of the same screen during a migration.
  4. Clearer data flow — input goes one way (View → VM via methods), output goes the other (VM → View via observed state). No two-way magic to debug.
  5. Easier mocking — inject a ProfileRepositoryMock and you can drive the VM through any state.

MVVM tradeoffs (cons)

  1. Boilerplate. Every screen now has at least a View, a ViewModel, and usually a protocol for the dependency. For a screen that's truly just "show a label," it's overkill.
  2. Massive ViewModel. The pattern doesn't enforce anything about what belongs in a VM. Without discipline, you trade Massive VC for Massive VM. The fix is to push business logic down into the domain layer (use cases / services) and keep the VM as a presentation translator.
  3. Navigation has no home. MVVM says nothing about screen-to-screen flow. People end up routing through delegates, closures, environment objects, or just calling present from the VM (which couples it to UIKit). MVVM-C solves this explicitly with Coordinators.
  4. Binding mechanism complexity. Combine pipelines, observation tokens, [weak self] everywhere — the binding wiring is non-trivial and a frequent source of bugs (retain cycles, unintended re-renders, ordering).
  5. Steeper learning curve for newcomers — the indirection that helps testability also adds cognitive load.
  6. Doesn't solve everything. It's a presentation pattern. It says nothing about persistence, networking, modularization, dependency injection, or feature isolation. You still need other patterns alongside it.

A pragmatic stance: MVVM is a great default for non-trivial screens; for a static settings list, plain MVC is fine. Match the pattern to the problem.

Network + local DB sync — approaches

This is one of the most consequential architectural questions in a real app. The choices below sit on a spectrum.

1. Single source of truth (local DB). All UI reads from the database, never directly from a network response. Network responses are writes to the DB. The UI observes the DB and updates reactively. This is the most robust pattern — works offline, survives app restart, and there's only one place to look for "what does the app think is true."

Network → Repository → DB (truth) → UI observes DB

2. Repository pattern as gatekeeper. A Repository is the only thing that knows about both network and DB. Higher layers ask the repository for data; the repository decides whether to read from cache, fetch fresh, or merge.

3. Observation channel from DB to UI. GRDB's ValueObservation, Core Data's NSFetchedResultsController, SwiftData's @Query, or rolling your own with Combine over change notifications. The UI subscribes and re-renders whenever rows it cares about change.

4. Sync strategies:

5. Conflict resolution for two-way sync.

6. Optimistic vs pessimistic UI updates.

7. Identifiers. Use a stable identifier strategy (server IDs, UUIDs, or local IDs that get reconciled). Never key UI by row index.

8. Offline-first. Treat network as an optimization. Every operation goes to the DB first; a background queue replays them to the server as connectivity allows. Requires a "pending operations" table and idempotent server endpoints.

I lean toward DB-as-truth + Repository + observation + optimistic updates with rollback for connected apps — the Apple Notes / Things-style pattern. It survives airplane mode, app suspension, and bad networks gracefully.

Massive View Controller — why is it a problem, and how to mitigate it?

Why it's a problem:

Mitigation strategies (use several):

  1. Extract a ViewModel. Move all formatting, state derivation, and presentation logic into a UIKit-free type. Probably the highest single ROI.
  2. Extract services / use cases. Networking, persistence, analytics, authentication — none of these belong in the VC. Inject them.
  3. Coordinator pattern. Move navigation out. The VC's job is its own screen, not deciding what comes next.
  4. Child view controllers / container VCs. Compose complex screens out of multiple smaller VCs, each owning their own logic. Apple's own apps do this constantly.
  5. Custom UIView subclasses with their own logic. A complex header view, a floating control bar — these can be self-contained UIViews rather than VC code.
  6. Diffable data sources. Move table/collection data wrangling out of numberOfRows / cellForRow into a typed snapshot model.
  7. Composable layouts. Compositional layout makes complex collection layouts declarative instead of nested in delegate methods.
  8. Move IBAction handlers to the VM as intents (func didTapSubmit()).
  9. Protocol-oriented refactoring. Identify cross-cutting concerns (logging, analytics, error presentation) and lift them into protocol extensions or default implementations.
  10. Modularization. At a certain size, extract feature modules into Swift packages — physical separation enforces architectural separation.

The tactical principle: a VC's job is to bind a view hierarchy to a ViewModel and forward intents. Anything beyond that is a candidate for extraction.

What's required for an iOS app to be "fully and well tested"? Ideal vs practical

Test types and what each is good for:

Cross-cutting practices:

Ideal world (greenfield, generous timeline):

Practical world (deadlines, growing team, pre-existing tech debt):

The senior framing: testing is a tool for managing change cost, not a checkbox. Decide what you can't afford to break, test that thoroughly, and accept lighter coverage on lower-risk areas — but never less than enough to refactor safely.


Xcode + UIKit

Features for efficiently following logic through a large project

For really big codebases I'll also lean on grep -rn in Terminal when Xcode's indexer is choking, and SourceKit-LSP / xcrun sourcekit-lsp in editor-agnostic tools when needed.

Favorite tips and tricks for Xcode beginners

Have you used any of the Instruments?

Yes, regularly. The ones I reach for most:

A real example: I diagnosed a 50ms scroll hitch on a list view by combining Time Profiler (showed text-shaping cost in cellForRow) with Hitches (confirmed dropped frames during scroll). The fix moved the formatting into the model layer once at insert time, dropping it to ~9ms.

Responder Chain & First Responder

The responder chain is the linked path that events propagate along, starting from a leaf object and walking up through ancestors until something handles them. Every UIResponder (and that's UIView, UIViewController, UIWindow, UIApplication, your AppDelegate, etc.) has a next pointer that defines its position in the chain.

For a typical view inside a VC, the chain looks like:

UIView → ... → UIViewController.view → UIViewController
       → UIWindow → UIApplication → UIApplicationDelegate

(UIView's next is its superview, except for the VC's root view, whose next is the VC itself; the VC's next is the window's view; etc.)

The chain handles:

First Responder is the responder currently designated as the active one for input. The most visible case is keyboard input — a UITextField becomes first responder when tapped, the keyboard appears, and key events are delivered to it. Programmatically: becomeFirstResponder() / resignFirstResponder(). Only one object is the first responder at a time within a window.

A senior trick exploiting this: you can find "the currently focused field" without knowing about it explicitly by sending an action up the chain with target: nil — whatever's focused will receive it. This is how the editing menu finds the text view to copy from.

// Implementing a custom action on whatever is focused
UIApplication.shared.sendAction(#selector(UIResponder.copy(_:)), to: nil, from: self, for: nil)

Experience with UICollectionViewDiffableDataSource and UICollectionViewCompositionalLayout

Yes, both are my default for collection views in UIKit projects from iOS 13/14 onward.

UICollectionViewDiffableDataSource — replaces the index-path-based delegate approach with a snapshot model. You build a NSDiffableDataSourceSnapshot<Section, Item>, apply it, and the framework computes the diff and animates inserts/deletes/moves automatically. The wins:

  1. No more "invalid update" crashes. performBatchUpdates was a constant source of "the number of items in section after the update doesn't match" crashes. Diffable just makes them go away.
  2. Item-driven, not index-driven. Items are Hashable, so you reason about your data, not array indices.
  3. Animations for free. Type-driven diffs animate sensibly without you specifying inserts/deletes.
enum Section: Hashable { case main }
struct Item: Hashable { let id: UUID; let title: String }

let dataSource = UICollectionViewDiffableDataSource<Section, Item>(
    collectionView: collectionView
) { cv, indexPath, item in
    let cell = cv.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
    cell.label.text = item.title
    return cell
}

var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(items)
dataSource.apply(snapshot, animatingDifferences: true)

In iOS 14+, cell registration with UICollectionView.CellRegistration made boilerplate cell registration much cleaner.

UICollectionViewCompositionalLayout — builds layouts as a tree of ItemGroupSectionLayout. Each section can have a totally different layout. This replaced 90% of the cases where I used to need a custom UICollectionViewLayout subclass.

let layout = UICollectionViewCompositionalLayout { sectionIndex, env in
    let item = NSCollectionLayoutItem(
        layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .estimated(100))
    )
    let group = NSCollectionLayoutGroup.horizontal(
        layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .estimated(100)),
        subitems: [item]
    )
    let section = NSCollectionLayoutSection(group: group)
    section.contentInsets = .init(top: 8, leading: 16, bottom: 8, trailing: 16)
    return section
}

I use them together. Compositional Layout describes the shape, Diffable Data Source describes the content, and the resulting code is both more declarative and more correct than the legacy delegate/datasource API. The badge-system feature work I did at lululemon (which contributed to the 25% sales lift) used both.

For section snapshots and outline-style layouts: NSDiffableDataSourceSectionSnapshot plus .list configurations is wonderful for sidebar-style or expandable-row UIs.


Design

Familiarity with Apple's Human Interface Guidelines

Strong familiarity. The HIG is the source of truth for how things should look and behave on Apple platforms; reading it is non-negotiable for a senior iOS dev. The principles I keep coming back to:

Concrete areas of the HIG I reference repeatedly:

The HIG isn't a rulebook to follow blindly — it's the Schelling point. Every time you deviate from it, you're spending some of your users' familiarity budget; do that only when the deviation pays for itself.

The role of a developer in UX design when working with designers

The senior iOS engineer's job in design partnership is to be a first-class collaborator, not a passive implementer. Concretely:

  1. Understand the design system at the same depth as the designers do. Know the type ramp, color tokens, spacing scale, and component variants. Catch "off-system" choices in review and ask whether they're intentional or accidental.
  2. Speak fluent HIG and platform conventions. When a proposed design conflicts with platform expectations (a non-standard nav bar, a tap target that's too small, a gesture that conflicts with edge-swipe-to-go-back), raise it early.
  3. Surface technical constraints early. "This animation needs offscreen rendering and will hitch on older devices." "This blur effect costs 6ms on iPhone X-class hardware." Designers can usually adapt if you bring the constraint with an alternative.
  4. Prototype in code, not in Figma, when motion and feel matter. A static spec rarely captures what an interaction should feel like; a 30-minute Swift Playground prototype almost always does.
  5. Treat accessibility as a design concern, not a post-hoc engineering chore. Bring up Dynamic Type, VoiceOver order, contrast, and focus trapping during the design review, when changes are cheap.
  6. Push back, professionally. If a design will produce a worse user experience or a maintenance burden disproportionate to the value, say so with reasoning and, ideally, an alternative.
  7. Be the user's advocate when stakes get crossed. Performance, latency, error states, empty states, loading states — designers may default to optimistic specs; engineers see all the unhappy paths.
  8. Bring data. Crash logs, analytics, support tickets — if a design pattern is causing problems in the live app, engineering owns the evidence.
  9. Implement what was specified, then offer to demo. Once the build is in TestFlight, walk designers through the implementation; small details (spring stiffness, easing curves, haptic feedback) are easier to tune in conversation than over Slack.

The shorthand: engineers and designers both serve the same user; the relationship is a partnership of expertise, not a handoff.

Staying current on iOS design conventions

Within the team: regular UX office hours with designers, periodic design reviews on shipped features (what worked, what didn't), and a Slack channel for "I saw this" drops keeps the whole team learning together.

Familiarity with animation frameworks

UIView block-based animation (UIView.animate(withDuration:...)) — the most common API; easy, sufficient for most fades, slides, color transitions. Supports spring, options like .curveEaseInOut, .allowUserInteraction, etc. Limited in interruptibility.

UIViewPropertyAnimator (iOS 10+) — the modern, far more flexible replacement. Animations are interruptible, reversible, and scrubbable. Critical for any interactive transition (pull-to-dismiss, custom sheet drags, interactive view controller transitions).

let animator = UIViewPropertyAnimator(duration: 0.4, dampingRatio: 0.8) {
    self.cardView.transform = CGAffineTransform(translationX: 0, y: -200)
    self.cardView.layer.cornerRadius = 24
}
animator.addCompletion { position in
    if position == .end { self.didExpand() }
}
animator.startAnimation()

// Later, mid-flight:
animator.pauseAnimation()
animator.fractionComplete = 0.7   // scrub
animator.isReversed = true
animator.continueAnimation(withTimingParameters: nil, durationFactor: 1)

Custom view controller transitions (UIViewControllerAnimatedTransitioning / UIViewControllerTransitioningDelegate / UIPercentDrivenInteractiveTransition) — for bespoke present/dismiss flows. Pair with UIViewPropertyAnimator for interruptible interactive transitions (the "flick to dismiss" pattern).

Core Animation (CALayer, CABasicAnimation, CAKeyframeAnimation, CASpringAnimation, CATransaction) — the layer-level engine UIView animations are built on top of. Reach for it when:

let stroke = CABasicAnimation(keyPath: "strokeEnd")
stroke.fromValue = 0
stroke.toValue = 1
stroke.duration = 1.2
stroke.timingFunction = CAMediaTimingFunction(name: .easeOut)
shapeLayer.add(stroke, forKey: "draw")

CADisplayLink — for animations driven by frame ticks (custom interpolation, physics, render-loop-synced updates).

UIDynamicAnimator / UIKit Dynamics — physics-based animation (gravity, attachment, snap). I've found it niche; rarely worth the complexity vs. spring animations.

SwiftUI animation: .animation(_:value:), explicit withAnimation { }, custom Animation curves, .matchedGeometryEffect for hero transitions, PhaseAnimator and KeyframeAnimator (iOS 17+), Animation.spring(duration: response, bounce: bounce). SwiftUI's animation model is declarative and considerably cleaner for state-driven motion.

Lottie / Rive — if designers are producing complex vector animations, these run designer-authored files at runtime.

A senior heuristic: prefer the highest-level API that does the job. UIView.animate is fine 80% of the time; UIViewPropertyAnimator for interactive transitions; Core Animation when you genuinely need its capabilities. Don't use Core Animation when UIView.animate would work — you'll regret the verbosity.


Frameworks

Continuous Integration

Comfortable. I've built and maintained pipelines using:

Things I bake in by default:

SQL / SQLite

Strong. I built and maintain SwiftQuery, an open-source Swift SQLite library at github.com/simrandotdev/SwiftQuery, so I work with SQLite at the C-API level as well as via wrappers like GRDB.

Comfort areas:

Where iOS apps usually trip:

Autolayout & Constraints

Comfortable, including the parts people skip past:

NSLayoutConstraint.activate([
    titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
    titleLabel.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
    titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: view.layoutMarginsGuide.trailingAnchor),
])
titleLabel.setContentCompressionResistancePriority(.required, for: .vertical)

Anchor APIs are my default; I avoid VFL (visual format language) — it's harder to read and refactor.

Comfortable across all the modern variants:

Architecture-wise, I separate the link-parsing layer (URL → route enum) from the routing layer (route → which screen). That keeps deep-link handling testable and lets me unify the routes coming from any source.

enum Route { case profile(userID: String), feed, settings }

protocol DeepLinkParser { func parse(_ url: URL) -> Route? }
protocol Router { func navigate(to route: Route) }

Parser unit-tested against URL fixtures; Router is responsible for animating into the right screen regardless of source.

CocoaPods / SPM

Swift Package Manager is my default. It's native, integrated into Xcode, and increasingly supported by every library I depend on. Source-only by default (resources and binary targets supported), versioned via Swift's package manifests, integrates with multi-module project setups.

CocoaPods I've used extensively in the past and still maintain in legacy projects. It's mature and battle-tested but adds a build step, modifies your workspace, and the Ruby toolchain dependency is friction many teams have moved off.

Carthage — used historically; SPM has eaten its niche.

For new projects: SPM, full stop, with multi-module Xcode projects (or Package.swift-only when fitting). For legacy projects on CocoaPods: migrate as dependencies become SPM-compatible, but don't rip-and-replace before the value is there.

SwiftUI

Comfortable and increasingly defaulting to it for new screens on iOS 17+:

The recent on-device work I've been doing — including a NoteFlow app on a 21-day SwiftUI + GRDB plan with @Observable and NavigationStack — has me building production-grade SwiftUI day to day.

Things SwiftUI is still rough at: complex collection layouts, deep navigation programmatic control, fine-grained input handling. For those screens I still reach for UIKit + a UIHostingController if needed.

Xcode Instruments

(Covered above under "Have you used any of the instruments?" — yes, with the toolset and a real-world case study.)

Accessibility

Comfortable across the iOS accessibility surface:

imageButton.accessibilityLabel = "Add to favorites"
imageButton.accessibilityHint = "Marks this item as a favorite. Activate to toggle."
imageButton.accessibilityTraits = isFavorited ? [.button, .selected] : .button
imageButton.accessibilityValue = isFavorited ? "On" : "Off"

Why is accessibility important?

Several converging reasons, in order of how I tend to argue it depending on audience:

  1. It serves real users. Roughly 1 in 4 adults in the US lives with some form of disability, per the CDC; globally over a billion people. VoiceOver, Dynamic Type, Switch Control, Voice Control — these aren't niche features. Building inaccessibly is choosing to exclude millions of paying users from a product that could otherwise serve them.

  2. It's the right thing to do. Software is increasingly the way people manage their finances, communication, healthcare, work, and government services. An inaccessible app isn't an inconvenience for some users — it's a barrier to participation in modern life.

  3. Legal exposure is real and growing. ADA-related lawsuits over inaccessible digital products have been climbing year over year in the US; the EAA (European Accessibility Act) takes effect in 2025; many enterprise and government procurement requirements include WCAG conformance as a hard prerequisite. Building accessibly is risk management.

  4. Accessibility benefits everyone. Dynamic Type helps anyone reading in bright sun or with tired eyes, not only users with low vision. Captions help people watching in noisy environments. Reduce Motion helps users with motion sensitivity but also conserves battery. High contrast helps everyone in glare. The "curb cut effect" is real: fixing for the edge of the distribution improves life in the middle too.

  5. It improves engineering quality. Accessible code is structured code. Apps that handle Dynamic Type correctly tend to handle internationalization and localization correctly, because both depend on flexible layout. Apps with proper accessibility labels tend to be easier to UI-test. Forcing yourself to think about non-visual interaction surfaces flaws in your information architecture.

  6. It's a small fraction of the work if you do it as you build. Adding accessibility after the fact is multiples more expensive than building it in from the start. Treating it as a first-class design and engineering concern from sprint one is the cheapest path to an accessible app.

  7. Apple cares. App Store featuring, ADA awards, and editorial coverage all favor accessible apps. Apple's own apps set the bar — competing without matching it leaves users with a reason to switch.

The bottom line: an app that works only for users with perfect vision, perfect hearing, perfect motor control, and the patience to deal with hostile design isn't a finished product. It's a prototype.