These iOS interview questions test whether a developer can build and ship an app on Apple's platform, not just write Swift. They cover SwiftUI and UIKit architecture, the app and scene lifecycle, persistence with Core Data and SwiftData, push notifications, background tasks, Instruments, accessibility and App Store release. At Ryz, senior iOS engineers are sourced by recruiters who look at the apps they actually shipped, then go through structured NTRVSTA AI interviews built around questions like these, and recruiters review every candidate before and after. The AI scores are advisory. People make the hiring decisions. Language-level topics (generics, actors, Sendable, ARC) live in our separate Swift question set.
Pick six to eight questions per interview, weighted to the level you are hiring. Fundamentals show whether someone has shipped a real app; the performance and senior sections show whether they have owned one through crash spikes, OS releases and App Review. Add follow-ups from your own codebase, listen for trade-offs rather than recalled API names, and close with the practical exercise below.
SwiftUI is the default for new screens: faster to build and where Apple adds new APIs first. UIKit still wins for screens that need precise control: complex collection view layouts with heavy custom interaction, text editing with fine-grained control, or code that must match an existing UIKit design system. Mixing is normal: UIHostingController puts SwiftUI inside UIKit, and UIViewRepresentable or UIViewControllerRepresentable wraps UIKit for SwiftUI.
What a strong answer shows: No ideology. They name concrete cases where each framework is the better tool and know the bridging types and their sizing and lifecycle quirks.
The app delegate handles process-level events; scenes (each window) move through foreground active, foreground inactive and background via the scene delegate or SwiftUI's scenePhase. The app can be suspended and then terminated without further notice, so unsaved state should be persisted when the scene moves to background, not at termination.
What a strong answer shows: They know applicationWillTerminate is not reliable, think per scene on iPad, and distinguish UI restoration state from real user data.
Mark model classes with @Observable, own them with @State in the view that creates them, pass them down as plain properties or through the environment, and use @Bindable when a child needs bindings. Views only update for properties they read in body, unlike ObservableObject, where any @Published change invalidated every observer.
@Observable final class CartModel {
var items: [Item] = []
var total: Decimal { items.reduce(0) { $0 + $1.price } }
}
struct CartScreen: View {
@State private var cart = CartModel()
var body: some View {
CartList(cart: cart)
.environment(cart)
}
}
What a strong answer shows: Clear ownership rules (who creates, who borrows) and an understanding of why property-level tracking reduces unnecessary view updates.
Cell reuse. A cell starts an image load, scrolls off, gets reused for another row, and the first load finishes and writes into it. Fix it by cancelling the load in prepareForReuse or by checking the cell still represents the same item identifier before applying the result.
What a strong answer shows: Instant recognition of a classic bug and a fix that handles cancellation, not just a placeholder image.
Use NavigationStack with a path you own (an array of hashable route values or NavigationPath) and navigationDestination(for:). A deep link or notification tap then becomes "parse URL into routes, set the path". Universal links need an apple-app-site-association file on the domain and the associated domains entitlement; the app handles them through onOpenURL or the scene delegate.
What a strong answer shows: Navigation as state, testable without UI, and awareness that universal links fail silently when the association file or entitlement is wrong.
Usage strings for every permission, an accurate privacy label, and a privacy manifest (PrivacyInfo.xcprivacy) that declares collected data and approved reasons for "required reason" APIs such as UserDefaults and file timestamp APIs. Listed third-party SDKs must ship their own manifests.
What a strong answer shows: They treat privacy as a release requirement and know that SDKs bring their own obligations.
SwiftData fits apps targeting iOS 17 and later with straightforward models and SwiftUI views using @Query. Core Data is mature, supports older OS versions and has more tuning options. GRDB or SQLite fit teams that want SQL control. In SwiftData, schema changes go through VersionedSchema and a SchemaMigrationPlan with lightweight or custom stages.
@Model final class Note {
var title: String
var createdAt: Date
@Relationship(deleteRule: .cascade) var tags: [Tag] = []
init(title: String, createdAt: Date = .now) {
self.title = title
self.createdAt = createdAt
}
}
What a strong answer shows: They pick by OS floor and data complexity, and they have tested a migration against a real user's old store before shipping it.
Start the request in SwiftUI's .task modifier, which cancels the task when the view disappears, or .task(id:) to restart when an input changes. URLSession's async methods respond to task cancellation.
.task(id: query) {
do {
results = try await api.search(query)
} catch is CancellationError {
// view went away or query changed
} catch {
errorMessage = error.localizedDescription
}
}
What a strong answer shows: They tie work to view lifetime and handle cancellation separately from real errors.
The APNs environment: debug builds use the sandbox gateway, TestFlight and App Store builds use production, and device tokens differ between them. Check the aps-environment entitlement in the signed build, which gateway the backend targets, and APNs response codes in server logs.
What a strong answer shows: A systematic path from entitlement to server to device, and knowledge that silent pushes are throttled and not guaranteed.
In the Keychain, never UserDefaults. The accessibility class matters: an item restricted to kSecAttrAccessibleWhenUnlocked cannot be read by a background task while the phone is locked, so tokens needed by background refresh use kSecAttrAccessibleAfterFirstUnlock. Files with .complete data protection are likewise unreadable while locked.
What a strong answer shows: They connect a mysterious background failure to data protection classes, a bug that only shows up on real devices.
Split it into local Swift packages by feature and by layer (networking, design system, persistence), with feature modules depending on interfaces rather than each other. Measure first with Xcode's build timeline, and avoid one giant "Core" module everything depends on.
What a strong answer shows: Measurement before restructuring, and a dependency graph that keeps feature modules independent.
Most coverage in fast unit tests of view models, reducers and services (XCTest or Swift Testing), snapshot tests for key screens across Dynamic Type sizes and dark mode, and a small number of XCUITest flows for critical paths like sign-in and purchase. UI tests stub the network through launch arguments so they are not flaky.
What a strong answer shows: A pyramid shaped by cost and flakiness, and practical tricks for making UI tests deterministic.
Extensions run in separate processes, so use an App Group container for shared files or a shared SwiftData or Core Data store, a shared Keychain access group for credentials, and WidgetCenter.shared.reloadTimelines(ofKind:) to refresh widgets when data changes. Extensions have tight memory limits, so give them a small precomputed snapshot.
What a strong answer shows: They understand extensions are separate processes with their own limits and plan for concurrent access to shared storage.
Use BGTaskScheduler: register handlers before the app finishes launching, add identifiers to BGTaskSchedulerPermittedIdentifiers in Info.plist, and submit a BGAppRefreshTaskRequest for short work or a BGProcessingTaskRequest for longer work that can require power or network. The system decides when it runs, so it is a hint, not a schedule.
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.app.sync", using: nil) { task in
let work = Task {
let ok = await SyncService.shared.run()
task.setTaskCompleted(success: ok)
}
task.expirationHandler = { work.cancel() }
scheduleNextSync()
}
What a strong answer shows: Realistic expectations (no guaranteed timing, user force-quit stops it), and a design that works even when background runs never happen.
Profile a release build on a real older device with Instruments: the Time Profiler and Hangs instruments for main-thread work, and the SwiftUI instrument in recent Xcode versions for expensive view updates. Usual causes: main-thread image decoding, heavy text layout, and views observing more state than they need.
What a strong answer shows: They measure on real hardware in release mode before changing code, and know the usual suspects.
Never decode a 12-megapixel photo to display a 100-point thumbnail. Downsample with ImageIO so the full bitmap never exists in memory, or use UIImage.byPreparingThumbnail(ofSize:). Decoded image memory is width times height times bytes per pixel, regardless of file size.
func downsample(_ url: URL, to maxPixels: CGFloat) -> UIImage? {
let srcOpts = [kCGImageSourceShouldCache: false] as CFDictionary
guard let src = CGImageSourceCreateWithURL(url as CFURL, srcOpts) else { return nil }
let opts = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxPixels
] as CFDictionary
guard let cg = CGImageSourceCreateThumbnailAtIndex(src, 0, opts) else { return nil }
return UIImage(cgImage: cg)
}
What a strong answer shows: They reason in decoded bytes, not file size, and know that jetsam kills apps that ignore it.
Split it into pre-main (loading dynamic libraries, static initializers) and post-main (work in app delegate, first frame). Use the App Launch instrument and Organizer launch metrics from real users. Fixes: fewer dynamic frameworks, deferred SDK initialization, no synchronous disk or network work before the first frame.
What a strong answer shows: They know launch has distinct phases and use field data, not just a fast developer device.
Terminations that are not crashes: watchdog kills for blocking the main thread too long during launch or resume, jetsam memory terminations, or background task overruns. MetricKit diagnostics and the Xcode Organizer show hang and termination reports, and exit reason data helps separate memory pressure from watchdog kills.
What a strong answer shows: They know the difference between a crash and a system termination and where Apple surfaces each.
Combine child elements into one accessible element with a meaningful label (accessibilityElement(children: .combine) or an explicit label), expose actions with accessibilityAction rather than hidden tap targets, use text styles instead of fixed font sizes, and let layouts switch from horizontal to vertical at accessibility sizes.
What a strong answer shows: They have actually used VoiceOver on their own app and design layouts that survive the largest text sizes.
Feature modules owned by teams, a shared design system package, and a thin app target that composes features. Inside features, consistency (MVVM or a unidirectional architecture like TCA) matters more than the pattern chosen. A router keeps features from importing each other.
What a strong answer shows: They optimize for team independence and build times, and can argue for and against a heavier framework like TCA.
Write locally first, mark records dirty, and sync in the background through a queue of operations. Each record carries a server version or timestamp for conflict detection. Resolve conflicts by field-level merge or last-writer-wins, and surface real conflicts to the user. A custom backend needs idempotent endpoints.
What a strong answer shows: They talk about conflict policy, idempotency and what the user sees, not just "use Core Data".
Pause the phased release in App Store Connect, which stops automatic updates to new users. Turn off the feature through a remote flag if the crash is in flagged code. Symbolicate with the build's dSYMs, find the crashing path, ship a fix through expedited review if needed.
What a strong answer shows: Calm incident handling and knowledge that you cannot roll back an App Store binary, so kill switches matter.
Screen by screen, starting with leaf screens (settings, detail views) hosted in UIHostingController, while UIKit keeps navigation. Keep shared models observable and the design system available in both. Move navigation containers last.
What a strong answer shows: Incremental delivery, continued feature work during migration, and awareness of bridging costs.
Use App Attest (DeviceCheck framework) to have the server verify requests come from a genuine instance of your app, combined with server-side rate limiting and normal authentication. Pin public keys with backups if you pin at all. Jailbreak detection is easy to bypass and should never be the main control.
What a strong answer shows: They put trust decisions on the server and treat client checks as signals.
Automatic or centrally managed signing (Xcode Cloud, or fastlane match with certificates in an encrypted store), builds on every pull request with tests, and release builds that upload to TestFlight with build numbers set by CI. Pin the Xcode version per branch.
What a strong answer shows: They have fixed expired certificates at a bad moment and designed the setup so nobody signs from a laptop.
Build with the beta Xcode on a branch, run the full test suite, fix deprecation warnings and behavior changes, and test on devices running the beta. Review design changes that affect your UI, decide what to adopt at launch, and confirm third-party SDKs are ready.
What a strong answer shows: A yearly routine rather than a scramble, and owning SDK risk too.
applicationWillTerminate and assumes it always runs.UserDefaults.Give a 3-hour take-home: a small SwiftUI app that lists items from a public JSON endpoint (provide a static file), shows a detail screen, lets the user favorite items, and keeps favorites offline across launches. Require one deep link that opens a specific item, pull to refresh, and graceful handling of a failed request. Ask for a short README on decisions, then hold a 45-minute review where they extend it live.
If you would rather skip building the loop yourself, hire senior iOS developers through Ryz. They are the top 1% of the candidates we interview, they join your team, repos and release process, and they work within ±1h of US time zones. See how candidates are screened in our vetting process, and use our iOS developer job description to scope the role.
Both, in proportion to your codebase. Most production apps are mixed, so ask at least one bridging question. SwiftUI-only engineers can struggle in large UIKit codebases, and the reverse.
An iOS interview tests the platform: lifecycle, frameworks, performance on devices and release. A Swift interview tests the language: type system, concurrency model and memory semantics. Senior iOS hires need both, but if the role is server-side Swift or a shared package, weight the language questions.
Share a small project with a deliberate problem, such as main-thread image decoding in a list, and ask the candidate to profile it with Instruments while sharing their screen. Which tool they open first and how they read the trace tell you a lot.
Questions we didn't answer? Email info@ryzlabs.com.