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.
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() { /* ... */ }
}
[weak self] or [unowned self] in closures.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.unowned references when the lifetime relationship guarantees the referent will always outlive the holder.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() { /* ... */ }
}
| 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:
strong: default for properties, ViewModel → Model, Coordinator → child Coordinator (parent owns child), array of subviews.weak:
weak var delegate: SomeDelegate? — the delegate (often a parent VC) owns the object that has the delegate property.IBOutlets for subviews owned by the view hierarchy (the view hierarchy is the owner; the VC just borrows).weak var parent: Node?.[weak self].unowned:
Customer ↔ CreditCard — a card can't exist without a customer.Task that you know completes before self deallocates) — but weak self is a safer default unless you've measured a real cost.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.
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? { ... }
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 matching — safe, explicit about both cases:
switch result {
case .some(let value): handle(value)
case .none: handleMissing()
}
5. Optional map / flatMap — safe, 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.
| 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):
struct. Reach for class only when you need reference semantics, identity, inheritance, or interop with Objective-C (NSObject, KVO, etc.).class for things that genuinely model identity (a window, a network connection, a database handle) or controllers in UIKit.struct for data, models, view state, descriptions of work — anything where two instances with the same fields are interchangeable.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:
Array, Dictionary, Set, String). They behave as values, but you're not literally copying the buffer on every assignment — only when one of them mutates while sharing.struct containing a class reference still has the class's semantics inside it. A struct UserSession { let token: NSMutableString } looks like a value type but isn't one in any meaningful sense. Compose values out of values.Views are structs, state changes produce new descriptions, the framework diffs and updates.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.
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:
self — lazy is the cleanest way around that, since stored property initializers can't reference self.Caveats:
lazy properties can't be let — they're inherently mutable (they go from "not initialized" to "initialized").lazy initialization is not thread-safe in Swift. If two threads access an uninitialized lazy property simultaneously, the initializer can run twice. For thread-safe lazy init, use static let (which is atomic via dispatch_once semantics) or a manually synchronized accessor.lazy properties make the first access slow rather than init slow — be mindful if that first access is on the main thread during a user interaction.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
}()
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.
Yes — they're maintained at swift.org/documentation/api-design-guidelines and they're the unofficial style of the standard library.
The headline principles:
Concrete naming guidance worth memorizing:
x.sort() mutates; x.sorted() returns a new value. The "-ed/-ing" rule.list.removeElement(x) → list.remove(x). The type tells you it's an element.xs.count not count(xs).move(from: a, to: b).view.addSubview(panel) — no label, "addSubview" already describes the action.string.append("!", times: 3) — second label is essential.array.isEmpty, line.intersects(other).Collection); protocols describing capability often end in -able/-ible (Equatable, Hashable).// 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
}
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 = Model / View / ViewModel.
LoadState, sorted arrays, etc.) and exposes the operations the View can trigger.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:
UIViewController.ProfileRepositoryMock and you can drive the VM through any state.present from the VM (which couples it to UIKit). MVVM-C solves this explicitly with Coordinators.[weak self] everywhere — the binding wiring is non-trivial and a frequent source of bugs (retain cycles, unintended re-renders, ordering).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.
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.
Why it's a problem:
viewDidLoad and IBOutlets is impractical, so coverage falls.Mitigation strategies (use several):
UIView subclasses with their own logic. A complex header view, a floating control bar — these can be self-contained UIViews rather than VC code.numberOfRows / cellForRow into a typed snapshot model.func didTapSubmit()).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.
Test types and what each is good for:
XCTMetric-based measurement of launch time, scroll performance, memory.Cross-cutting practices:
Ideal world (greenfield, generous timeline):
Practical world (deadlines, growing team, pre-existing tech debt):
UIView subclasses, Apple SDK wrappers, and bindings — diminishing returns.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.
condition: user.id == 42, log without stopping with "Log message" + "Automatically continue." Lets you trace flow with no code changes.-[NSException raise], UIViewAlertForUnsatisfiableConstraints) — break inside the SDK without source.#warning / #error as breadcrumbs while refactoring.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.
-com.apple.CoreData.SQLDebug 1).po and p in lldb — po self to print object description, expression to evaluate Swift, v (frame variable) for fast inspection.UIImage, UIColor, UIView, Data — you get a visual preview.#if DEBUG for diagnostic-only code; configure your Debug schema to set DEBUG if it isn't already.xcrun simctl for simulator scripting (boot, reset, push notification simulation).Yes, regularly. The ones I reach for most:
os_signpost for custom intervals.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.
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:
UIGestureRecognizer or directly handled in touchesBegan/Moved/Ended.motionBegan/Ended).target: nil (the biggest practical use). When you call sendAction(_:to:from:for:) with to: nil, UIKit walks the responder chain looking for the first responder that implements the selector. This is why UIBarButtonItem(... action: #selector(save), target: nil) works — UIKit finds the first responder up the chain that implements save.UIKeyCommand) on iPad/Mac.canPerformAction(_:withSender:) walks the chain.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)
UICollectionViewDiffableDataSource and UICollectionViewCompositionalLayoutYes, 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:
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.Hashable, so you reason about your data, not array indices.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 Item → Group → Section → Layout. 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.
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 senior iOS engineer's job in design partnership is to be a first-class collaborator, not a passive implementer. Concretely:
The shorthand: engineers and designers both serve the same user; the relationship is a partnership of expertise, not a handoff.
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.
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:
CALayer exposes (shadowPath, strokeEnd, path on CAShapeLayer).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.
Comfortable. I've built and maintained pipelines using:
xcodebuild runners for build/test/archive.Things I bake in by default:
swift-format or SwiftLint), warnings-as-errors on Release.~/Library/Caches/org.swift.swiftpm and DerivedData to keep build times tolerable.increment_build_number or a script).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:
EXPLAIN QUERY PLAN, identifying full scans, choosing index columns.PRAGMA user_version.INSERT OR REPLACE, upsert with ON CONFLICT, conditional updates.ValueObservation and the broader pattern of observing query results to drive UI.Where iOS apps usually trip:
await — deadlocks readers.Comfortable, including the parts people skip past:
required only when truly required).UILayoutGuide for spacing without dummy views.safeAreaLayoutGuide vs readableContentGuide vs layoutMarginsGuide.estimatedRowHeight + properly constrained content + dynamic type support.UIView.constraintsAffectingLayout(for:), _autolayoutTrace(), the unsatisfiable-constraints log message, the UIViewAlertForUnsatisfiableConstraints symbolic breakpoint.< .required), implicit intrinsic content size mismatches.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:
myapp://path/...) — easiest, weakest. Anyone can register the same scheme; iOS only honors the first.apple-app-site-association (AASA) hosted on your server, capability + entitlement, and matching applinks: associated domains. The right answer for any user-facing link.NSUserActivity — continue work between devices; also the mechanism for App Search and Spotlight indexing.aps payload + custom keys routed through UNUserNotificationCenter delegate.widgetURL / Link.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.
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.
Comfortable and increasingly defaulting to it for new screens on iOS 17+:
@State, @Binding, @ObservedObject/@StateObject/@EnvironmentObject (legacy), @Observable macro + @Bindable (modern), @Environment(\.something).NavigationStack with type-safe navigationDestination(for:) — a major improvement over NavigationView.HStack/VStack/ZStack, Grid, Layout protocol for custom layouts..animation(_:value:), explicit withAnimation, Transition, matchedGeometryEffect, PhaseAnimator, KeyframeAnimator.Task for async work tied to view lifecycle, .task(id:) for parameterized.Equatable views, LazyVStack, id(_:) to scope diffs, avoiding unnecessary observation.UIViewRepresentable / UIViewControllerRepresentable to drop in UIKit; the inverse with UIHostingController.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.
(Covered above under "Have you used any of the instruments?" — yes, with the toolset and a real-world case study.)
Comfortable across the iOS accessibility surface:
accessibilityElements, rotor support.UIFont.preferredFont(forTextStyle:), adjustsFontForContentSizeCategory, supporting up to AX5 sizes (UIContentSizeCategory.accessibilityExtraExtraExtraLarge), Font.system(.body) in SwiftUI.UIAccessibility.isReduceMotionEnabled etc., adapt accordingly.accessibilityElements ordering, especially for non-trivial layouts..button, .adjustable), accessibilityAdjustable for sliders, accessibilityActivate() for non-standard activation.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"
Several converging reasons, in order of how I tend to argue it depending on audience:
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.
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.
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.
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.
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.
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.
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.