iOS

Learn the whole stack

Study Guide

Every core iOS topic, explained from first principles and framed the way interviews ask. Read the concept, then the level note so you know whether it is table-stakes or differentiating.

How to read this Each topic opens with what it is, then the points an interview actually probes, and a level note so you know whether it's table-stakes (junior/mid) or differentiating (senior/architect). Read top to bottom once, then come back via the flashcards and prompts to make it stick.

01 · Swift Language Essentials

What it is. Swift is a value-oriented, type-safe language. The single most important idea to internalize early is value types vs reference types: struct and enum are copied on assignment (value semantics), while class instances are shared by reference. SwiftUI leans hard on value types, so default to struct and reach for class only when you need identity or reference semantics.

Core fluency the interview assumes: optionals (and safe unwrapping with if let / guard let rather than force-unwrap !), enums with associated values, closures and how they capture, protocols and protocol-oriented design, generics, and error handling with do/try/catch.

// Value vs reference struct Point { var x = 0 } // copied final class Box { var x = 0 } // shared var a = Point(); var b = a; b.x = 9 // a.x still 0 let p = Box(); let q = p; q.x = 9 // p.x is now 9 // Optionals, no force-unwrap func first(_ xs: [Int]) -> Int { guard let head = xs.first else { return 0 } return head }
Memory Classes are reference-counted (ARC). A closure capturing self strongly inside a stored property creates a retain cycle — break it with [weak self]. This is a classic senior follow-up.
Level Junior: optionals, structs, closures. Senior: protocol-oriented design, generics with constraints, and reasoning about ARC and value-semantics performance (copy-on-write).

02 · SwiftUI Fundamentals

What it is. SwiftUI is a declarative UI framework: you describe what the UI should look like for a given state, and the framework diffs and updates the screen when state changes. A view is a lightweight struct conforming to View with a body that returns more views.

Master modifiers (and that order matters.padding().background() differs from .background().padding()), layout with VStack/HStack/ ZStack, Spacer, frame, and alignment, and lists with List and ForEach keyed by stable identity.

struct Greeting: View { let name: String var body: some View { VStack(alignment: .leading, spacing: 8) { Text("Hello, \(name)").font(.title.bold()) Text("Welcome back").foregroundStyle(.secondary) } .padding() } }
Identity SwiftUI tracks views by identity. In ForEach, use a stable id (a real model id, not the array index) or animations and state will attach to the wrong row.
Level Junior/mid: build screens, compose views, use modifiers correctly. Senior: reason about view identity, diffing, and when a re-render is unnecessary (see Performance, topic 09).

03 · State & Data Flow in SwiftUI

What it is. SwiftUI is driven by a single source of truth. Pick the right property wrapper for who owns the state:

  • @State — value owned by this view (private, transient UI state).
  • @Binding — a two-way reference to state owned elsewhere.
  • @Observable (the Observation framework, iOS 17+) — reference-type model whose properties the views observe automatically; pair with @State to own it and @Bindable to bind to it.
  • @Environment — dependencies injected down the view tree.

Before iOS 17 this was ObservableObject + @Published + @StateObject/ @ObservedObject; know both, because plenty of code still uses the older API.

@Observable final class CounterModel { var count = 0 } struct CounterView: View { @State private var model = CounterModel() var body: some View { Button("Tapped \(model.count)") { model.count += 1 } } }
Why it matters The Observation framework tracks reads at the property level, so only views that actually read a changed property re-render — a real performance win over the older @Published model, which invalidated all observers.

04 · UIKit & Interoperability

What it is. UIKit is the imperative, mature UI framework that powered iOS for a decade. Even in a SwiftUI-first app you need it for APIs SwiftUI doesn't fully cover, deep customization, and large existing codebases. Know the UIViewController lifecycle (viewDidLoad → viewWillAppear → viewDidAppear → …), Auto Layout constraints, and UITableView/UICollectionView with diffable data sources.

The bridge goes both ways: wrap UIKit in SwiftUI with UIViewRepresentable / UIViewControllerRepresentable, and host SwiftUI inside UIKit with UIHostingController.

struct MapView: UIViewRepresentable { func makeUIView(context: Context) -> MKMapView { MKMapView() } func updateUIView(_ view: MKMapView, context: Context) { /* sync state */ } }
Level Mid: use UIKit components and the representable wrappers. Senior: drive a UIKit ↔ SwiftUI migration, manage the Coordinator pattern for delegates, and know which framework owns the navigation stack.

05 · Swift Concurrency

What it is. Modern async iOS is built on async/await and structured concurrency. async functions suspend without blocking a thread; Task starts concurrent work; async let and TaskGroup run children in parallel and join them. Actors protect mutable state from data races by serializing access, and @MainActor guarantees code runs on the main thread (required for UI).

Swift 6 turns this into compile-time data-race safety: types crossing concurrency boundaries must be Sendable, and the compiler enforces actor isolation. Understanding isolation, Sendable, and @MainActor is the senior concurrency conversation.

func loadProfile() async throws -> Profile { async let user = api.user() // these two run async let posts = api.posts() // concurrently return try await Profile(user: user, posts: posts) } @MainActor func show(_ p: Profile) { self.profile = p } // safe UI update
Pitfall Don't reach for DispatchQueue.main.async out of habit — use @MainActor. And never block the main actor with synchronous work; move it off with a Task or a background actor.
Level Mid: use async/await and @MainActor. Senior: explain actor isolation, Sendable, cancellation, and how to migrate a callback-based API to async with withCheckedContinuation.

06 · Networking & Codable

What it is. The standard stack is URLSession's async API plus Codable for JSON. Codable (= Encodable & Decodable) maps JSON to Swift types automatically; use CodingKeys to rename fields and decoder strategies (e.g. .convertFromSnakeCase, custom date decoding) to handle real-world payloads.

struct User: Codable, Identifiable { let id: Int let fullName: String enum CodingKeys: String, CodingKey { case id, fullName = "full_name" } } func fetchUser(id: Int) async throws -> User { let url = URL(string: "https://api.example.com/users/\(id)")! let (data, response) = try await URLSession.shared.data(from: url) guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw URLError(.badServerResponse) } return try JSONDecoder().decode(User.self, from: data) }
Talk track Wrap networking behind a protocol (APIClient) so view models depend on an abstraction you can mock in tests. Layer caching with URLCache or your persistence store, and centralize retry/auth-refresh logic.

07 · Persistence

What it is. Pick storage by the data: SwiftData (iOS 17+) for app models — annotate a class with @Model, save through a ModelContext, and query reactively with @Query; Core Data for older targets or advanced control; UserDefaults for small preferences; the Keychain for secrets (tokens, passwords — never UserDefaults); and the file system for blobs.

@Model final class Task { var title: String var isDone = false init(title: String) { self.title = title } } struct TaskList: View { @Query(sort: \Task.title) private var tasks: [Task] @Environment(\.modelContext) private var context var body: some View { List(tasks) { Text($0.title) } } }
Don't store auth tokens or PII in UserDefaults — it's an unencrypted plist. Use the Keychain (see Security, topic 14).
Level Mid: model data and do CRUD with SwiftData/Core Data. Senior: design migrations, reason about the context/threading model, and sync with a backend (offline-first — see Architecture).

08 · Navigation & App Structure

What it is. An app's entry point is a struct conforming to App with @main, composing Scenes (usually a WindowGroup). For navigation, NavigationStack with navigationDestination replaced the old NavigationView and enables programmatic, value-driven navigation: bind a path array and push/pop by mutating it. TabView handles top-level sections, and deep links map a URL onto that path.

@main struct MyApp: App { var body: some Scene { WindowGroup { RootView() } } } struct RootView: View { @State private var path: [Route] = [] var body: some View { NavigationStack(path: $path) { HomeView() .navigationDestination(for: Route.self) { route in DetailView(route: route) } } } }
Talk track Value-driven navigation makes deep linking and state restoration straightforward: a URL or push notification decodes into Route values you append to path. That's the senior framing — navigation as data.

09 · Performance & Instruments

What it is. Performance work is mostly about doing less: fewer re-renders, fewer allocations, less main-thread work. In SwiftUI that means keeping body cheap and pure (no heavy computation, no side effects), giving views stable identity, using LazyVStack/ LazyHStack and List for long content, and letting the Observation framework re-render only the views that read a changed property.

Measure with Instruments: Time Profiler for CPU hotspots, Allocations/Leaks for memory and retain cycles, the SwiftUI instrument for view-body counts, and Hangs for main-thread stalls. Rule one: profile before you optimize — guesses are usually wrong.

Common bugs Decoding or sorting inside body; loading full-resolution images into small thumbnails (downsample first); retain cycles from closures capturing self; and blocking the main actor with synchronous I/O.
Level Senior: read an Instruments trace, find the hotspot, and explain the fix. Architect: set performance budgets (cold-launch, scroll, memory) and wire them into CI and observability.

10 · Testing

What it is. Two unit-test frameworks ship today: the long-standing XCTest and the newer Swift Testing (@Test functions with #expect/#require macros, parameterized tests, and async support), introduced with Xcode 16. UI flows are covered by XCUITest. The thing interviews probe is testability: inject dependencies behind protocols so you can substitute fakes, and keep logic in view models rather than views.

import Testing @Test func totalsAreSummed() { let cart = Cart(items: [2, 3]) #expect(cart.total == 5) } @Test func loadsUser() async throws { let client = MockAPIClient(user: .stub) let model = ProfileModel(api: client) try await model.load() #expect(model.user?.id == 1) }
Talk track "I depend on an APIClient protocol, not URLSession, so the view model takes a mock in tests and runs with no network." That one sentence signals you design for testability.

11 · Dependencies & Swift Package Manager

What it is. Swift Package Manager (SPM) is the first-class way to add dependencies and — just as important — to modularize your own app into local packages. A Package.swift manifest declares targets (code) and products (what you expose). Splitting a feature into its own package enforces boundaries, speeds incremental builds, and lets features be tested in isolation. CocoaPods and Carthage still exist in legacy projects, but SPM is the default for new work.

// Package.swift let package = Package( name: "Feature", products: [.library(name: "Feature", targets: ["Feature"])], dependencies: [], targets: [ .target(name: "Feature"), .testTarget(name: "FeatureTests", dependencies: ["Feature"]), ] )
Architect lens Local packages are the cheapest way to get modular architecture: a clear dependency direction, no accidental cross-feature imports, and parallel compilation. This is a recurring system-design answer (see Architecture).

12 · CI/CD & Release

What it is. A repeatable pipeline that builds, tests, signs, and ships every change. Xcode Cloud is Apple's hosted CI integrated with App Store Connect; Fastlane is the cross-tool automation standard (and runs on GitHub Actions, etc.). The part that trips everyone up is code signing: certificates identify the team, provisioning profiles tie an app id + devices + certificate together. Automatic signing handles the common case; teams often manage it explicitly (e.g. Fastlane match) for shared, reproducible credentials.

# Fastlane: build & ship a TestFlight beta lane :beta do increment_build_number build_app(scheme: "App") upload_to_testflight end
Talk track Distinguish version (marketing, CFBundleShortVersionString) from build number (CFBundleVersion, must increase per upload). Senior answer: PRs run unit + UI tests on CI; merges to main auto-ship a TestFlight build; releases are promoted with a phased rollout.

13 · App Store Review & Distribution

What it is. Shipping happens through App Store Connect: you upload a build, fill in metadata and screenshots, complete App Privacy (the privacy "nutrition label"), and submit for review against the App Store Review Guidelines. Beta testing goes through TestFlight. Know the common rejection reasons so you can avoid them: broken/incomplete functionality, privacy issues (missing usage-description strings, undisclosed data collection), misleading metadata, and using private APIs.

  • Phased release rolls an update out to a growing % of users over ~7 days — pause if crash rates spike.
  • Privacy manifests + required-reason APIs must be declared (see Security, topic 14).
  • StoreKit handles in-app purchases and subscriptions; Apple requires IAP for digital goods.
Reality Review is mostly automated checks plus a human pass; most rejections are avoidable with complete metadata, honest privacy answers, and a build that doesn't crash on first launch. Use TestFlight to catch the obvious stuff first.

14 · Security & Privacy

What it is. Store secrets in the Keychain (encrypted, hardware-backed), never in UserDefaults. Gate sensitive flows with biometrics via the LocalAuthentication framework (Face ID / Touch ID). Keep App Transport Security (HTTPS-only) on. Use data protection classes so files are encrypted at rest when the device is locked. Respect privacy: declare a privacy manifest (PrivacyInfo.xcprivacy) and required-reason API usage, and request tracking permission via App Tracking Transparency before using the IDFA.

import LocalAuthentication func authenticate() async -> Bool { let ctx = LAContext() guard ctx.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) else { return false } return (try? await ctx.evaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, localizedReason: "Unlock your vault")) ?? false }
Level Senior: Keychain, biometrics, ATS, and privacy manifests are expected. Architect: own the app's privacy posture — data-minimization, on-device processing (see topic 15), and a defensible answer to "what data leaves the device and why".

15 · On-Device AI & Machine Learning

What it is. Apple's ML runs on device for privacy, low latency, and offline use, accelerated by the Neural Engine. The toolbox: Core ML (run trained models), Create ML (train without leaving Swift), and task frameworks — Vision (images), Natural Language (text), Speech (transcription), and Sound Analysis. Apple Intelligence pushes this further with system-level generative features and on-device foundation models. Convert third-party models to Core ML with coremltools, and optimize (quantize, prune) to fit memory and latency budgets.

import Vision func detectText(in image: CGImage) async throws -> [String] { let request = VNRecognizeTextRequest() request.recognitionLevel = .accurate let handler = VNImageRequestHandler(cgImage: image) try handler.perform([request]) return request.results?.compactMap { $0.topCandidates(1).first?.string } ?? [] }
Frontier framing "Run inference on device when you can — it's private, works offline, and has no per-call cost; fall back to a server model only when the task exceeds on-device capability." This guide's own search runs a small embedding model in your browser as a working demonstration.