Prepare for iOS interview questions grouped by experience level.
iOS Interview Question & Answers
0-2 Years
Swift is Apple's modern, type-safe programming language designed to replace Objective-C for building iOS, macOS, and other Apple platform apps. It offers safer memory handling, cleaner syntax, and better performance than Objective-C while still interoperating with existing Objective-C code.
UIKit is Apple's original imperative framework for building iOS interfaces, using view controllers and manually wiring up UI state. SwiftUI is a newer declarative framework where you describe what the UI should look like for a given state, and the framework handles updating the view automatically.
A UIViewController manages a single screen's worth of content and view hierarchy in a UIKit app, handling its view's lifecycle events like appearing and disappearing. It's the basic building block for structuring an app's screens in UIKit.
The app lifecycle describes the states an app moves through, like not running, inactive, active, background, and suspended, and the system calls specific delegate methods as the app transitions between them. Understanding it matters for things like saving state before the app is suspended or pausing work when it goes to the background.
Auto Layout is Apple's constraint-based system for defining a view's position and size relative to other views, rather than using fixed coordinates. It lets a single layout adapt automatically to different screen sizes and orientations.
A strong reference keeps an object alive by increasing its reference count, while a weak reference doesn't increase the count and becomes nil automatically if the object is deallocated. Weak references are commonly used to avoid retain cycles, like in delegate relationships.
ARC, or Automatic Reference Counting, is Swift's memory management system that automatically tracks and releases objects once nothing strongly references them anymore. It removes the need for manual memory management while still requiring developers to avoid retain cycles.
An optional is a type that can either hold a value or be nil, written with a question mark like String?. It forces developers to explicitly handle the case where a value might be missing rather than crashing unexpectedly on a nil value.
A struct is a value type, so it's copied when assigned or passed around, while a class is a reference type, so multiple variables can point to the same underlying instance. Swift favors structs for most data models unless you specifically need reference semantics or inheritance.
A delegate is an object that another object calls back to for handling specific events or providing data, following a common pattern in UIKit like UITableViewDelegate. It lets one object customize another's behavior without subclassing it.
A Storyboard is a visual file that lays out an app's screens and the navigation connections between them, editable in Xcode's Interface Builder. Some teams prefer building UI entirely in code instead, but Storyboards remain common for simpler apps.
A UITableView displays data in a single scrollable column of rows, commonly used for lists. A UICollectionView is more flexible, supporting custom grid layouts and multiple items per row, using a layout object to define its arrangement.
Xcode is Apple's integrated development environment for building apps across all Apple platforms, including a code editor, Interface Builder, simulator, and debugging tools. It's the primary tool iOS developers use day to day.
viewDidLoad is called once after a view controller's view is loaded into memory, and it's the typical place to do one-time setup like configuring subviews or fetching initial data. It runs before the view actually appears on screen.
viewDidLoad runs once when the view is first loaded into memory, while viewWillAppear runs every time the view is about to become visible, including when navigating back to it. Code that needs to refresh each time the screen appears belongs in viewWillAppear, not viewDidLoad.
A closure is a self-contained block of functionality that can be passed around and executed later, similar to a function without a name. Closures are commonly used for completion handlers, like the callback passed to a network request.
Synchronous code executes in order, blocking the current thread until each step finishes. Asynchronous code lets a task, like a network call, run without blocking, notifying the caller through a closure or async/await when it completes.
A UINavigationController manages a stack of view controllers, providing push and pop navigation along with a navigation bar. It's the standard way to build drill-down navigation flows in UIKit apps.
Interface Builder is Xcode's visual editor for designing UI in Storyboards and XIB files, letting developers drag and arrange views instead of writing all layout code by hand. It integrates directly with the code through outlets and actions.
An IBOutlet is a property connected to a UI element in Interface Builder, letting code reference and manipulate that element programmatically, like changing a label's text. It's declared with the @IBOutlet attribute.
An IBAction is a method connected to a UI control's event in Interface Builder, like a button tap, so the method runs when that event occurs. It's declared with the @IBAction attribute.
MVC separates an app into the data model, the view that displays it, and the controller that mediates between them. It's Apple's traditionally recommended architecture for UIKit apps, though it can lead to bloated view controllers if not managed carefully.
A UIStackView arranges a set of views in a horizontal or vertical line automatically, distributing space based on configurable alignment and distribution settings. It reduces the number of manual Auto Layout constraints needed for common layouts.
The frame describes a view's position and size relative to its superview's coordinate system. The bounds describes the view's own internal coordinate system, which is usually the same size but with an origin of zero.
A segue defines a transition from one view controller to another, typically configured visually in a Storyboard, and it can pass data between the two view controllers through the prepare(for:) method. Segues are UIKit-specific and don't exist in SwiftUI's navigation model.
Before an app becomes publicly available, Apple's review team checks it against the App Store Review Guidelines for things like functionality, content, and privacy compliance. Developers submit builds through App Store Connect and can be rejected with specific feedback if something fails to meet the guidelines.
A Bundle Identifier is a unique string, usually in reverse domain notation like com.company.appname, that identifies an app to the operating system and the App Store. It's set in the project configuration and must match the identifier registered in the Apple Developer account.
Both are dependency managers that let iOS developers pull in third-party libraries and manage their versions without manually copying source files into a project. Swift Package Manager is Apple's own built-in solution, while CocoaPods is an older, widely used third-party tool.
UIAlertController presents an alert or action sheet to the user, letting them confirm an action or choose between options. It replaced the older, separate UIAlertView and UIActionSheet classes.
The simulator runs on a Mac and approximates iOS behavior, which is convenient for quick iteration, but it can't accurately test things like camera access, real performance, or certain hardware sensors. Testing on a real device is necessary before release to catch issues the simulator can't reproduce.
A UITextField is a single-line input control that lets users enter and edit text, commonly used in forms like login screens. Developers often set its delegate to respond to events like the user finishing editing or pressing return.
@State is a property wrapper that marks a value as owned and managed by a SwiftUI view, automatically triggering a UI refresh whenever the value changes. It's meant for simple, view-local state rather than data shared across multiple views.
A view modifier is a method you chain onto a SwiftUI view to change its appearance or behavior, like .padding() or .foregroundColor(). Modifiers return a new view wrapping the original, which is why they can be chained together.
Info.plist is a configuration file storing key metadata about the app, like its display name, version, and required permissions descriptions for things like camera or location access. iOS reads it at build and runtime to configure app behavior.
iOS push notifications rely on Apple Push Notification service, where a server sends a notification payload to Apple's servers, which then deliver it to the device. The app must register for remote notifications and receive a device token to enable this.
A UILabel displays static, non-editable text and is optimized for simple text display. A UITextView supports multi-line, scrollable, and optionally editable text, making it suited for longer or user-editable content.
3-6 Years
I'd add a weak or unowned capture for self inside the closure, choosing weak when the closure might outlive the view controller and unowned when I'm confident the view controller will always exist while the closure runs. I'd also use Xcode's memory graph debugger to confirm the cycle is actually resolved rather than just assuming the fix worked.
I'd check whether the feature is self-contained enough to build as a SwiftUI view embedded via UIHostingController, which is a common way to adopt SwiftUI incrementally without rewriting the whole app. If the feature needs deep integration with existing UIKit navigation or complex custom UIKit components, I'd stick with UIKit rather than fighting the interop layer.
I'd make the network call on a background thread, since blocking the main thread freezes the UI, and then explicitly dispatch the UI update back to the main thread once the response arrives. With async/await, this is handled more cleanly by marking the UI-updating function with @MainActor instead of manually dispatching.
For small amounts of simple data, like settings, I'd use UserDefaults. For structured data with relationships or that needs querying, I'd reach for Core Data or a lighter alternative like Realm, depending on the team's existing stack and how much complexity the data actually needs.
I'd check Auto Layout constraints for anything that assumes a specific screen size rather than being relative, and test against the actual range of screen sizes and safe area insets the app needs to support. Xcode's view debugger is useful here for inspecting the actual constraint values being applied at runtime on the problematic device or simulator size.
I'd isolate the view model from UIKit and network dependencies by injecting protocols instead of concrete types, so tests can substitute mock implementations. I'd focus tests on the view model's published state changing correctly in response to input, rather than testing UIKit rendering itself.
I'd persist critical state, like an in-progress form or navigation position, to disk in applicationDidEnterBackground rather than assuming the app will resume from memory. On relaunch, I'd check for that saved state and restore the user's context instead of always starting from a blank slate.
I'd rely on Auto Layout with size classes rather than hardcoded dimensions, and use trait collections to adapt layout decisions, like showing a split view on iPad versus a single column on iPhone. I'd test against the actual range of supported devices rather than just the simulator's default size.
I'd check whether cells are being reused properly through dequeueReusableCell rather than instantiated fresh, and look for expensive work, like image decoding or layout calculations, happening synchronously on the main thread during cellForRowAt. Moving heavy work off the main thread and caching computed values are usually the first fixes to try.
I'd store it in the Keychain rather than UserDefaults, since UserDefaults isn't encrypted and isn't meant for sensitive information. I'd also make sure the token is cleared from the Keychain on logout rather than just clearing in-memory state.
I'd replace hardcoded colors with semantic or asset catalog colors that automatically adapt to the current trait collection, rather than manually checking the interface style everywhere in code. I'd also test each screen in both modes, since issues like poor contrast or a fixed white background often only surface visually.
I'd add it through Swift Package Manager when supported, since it integrates more cleanly with Xcode's build system than older dependency managers. I'd also isolate calls to the SDK behind a thin wrapper of my own, so swapping the SDK later doesn't mean rewriting code throughout the app.
I'd implement didReceiveMemoryWarning in relevant view controllers to release cached data, like off-screen images, that can be recreated later if needed. I'd also profile with Instruments beforehand to understand what's actually consuming memory rather than guessing at what to release.
I'd build a dedicated networking layer using URLSession, wrapped behind protocols so it can be mocked in tests, rather than scattering URLSession calls throughout view controllers. I'd also centralize things like authentication headers, error handling, and response decoding so they're consistent across every request.
I'd set meaningful accessibilityLabel values on custom controls and images, and test the actual app with VoiceOver turned on rather than assuming default behavior is sufficient. I'd pay particular attention to custom views built without standard UIKit controls, since those don't get sensible accessibility behavior automatically.
I'd request extra execution time with beginBackgroundTask, being careful to end the task explicitly once the work completes so the system doesn't terminate the app abruptly. For work that needs to run later rather than immediately, I'd use BackgroundTasks framework's scheduled tasks instead.
I'd check whether the device is registered in the provisioning profile and whether the profile itself has expired, since those are the two most common causes of a signing failure that only shows up on specific devices. I'd also confirm the team is using automatic signing consistently, since mixing manual and automatic signing across a team tends to produce confusing, inconsistent failures.
I'd start by analyzing the app size report in Xcode to see what's actually contributing, since it's often unused assets or a bloated dependency rather than the app's own code. I'd also enable on-demand resources or app thinning for large assets that not every user needs immediately.
I'd check whether the data source is actually marked with an appropriate property wrapper, like @Published inside an ObservableObject, since SwiftUI only refreshes when it can observe the change through its own mechanisms. A common mistake is mutating a nested object's property without the containing object itself being marked as changed.
I'd request the permission at the point where the user understands why it's needed, rather than immediately on launch, since context-free permission prompts have much higher decline rates. I'd also design the app to degrade gracefully if permission is denied, rather than treating denial as an error state.
I'd use UIRefreshControl for the pull-to-refresh gesture, resetting to the first page of data on trigger, and implement pagination by detecting when the user scrolls near the bottom of the current data set to fetch the next page. I'd make sure both paths funnel through the same data-loading logic so state stays consistent regardless of which triggered the fetch.
I'd externalize all user-facing strings into Localizable.strings files rather than hardcoding text, and use NSLocalizedString or SwiftUI's built-in localization support throughout. I'd also test layouts with longer translated strings, since text expansion in some languages can break layouts that only looked fine in English.
I'd read the conflicting constraints listed in the console output carefully, since Xcode identifies exactly which constraints are competing, then decide which one should actually yield by adjusting its priority. I'd avoid just deleting constraints randomly until the warning disappears, since that often just hides the underlying layout ambiguity rather than fixing it.
For straightforward, single-step asynchronous work, I'd use async/await since it reads cleanly and avoids nested closures. I'd reach for Combine when the feature genuinely involves reacting to a stream of values over time, like live search input, rather than adopting it as a default for every asynchronous task.
6-8 Years
I'd modularize the app into separate Swift packages or frameworks along feature boundaries, so teams can build and test their areas independently without the entire app recompiling for every change. I'd also define clear, stable interfaces between modules, since tightly coupled modules quickly erode the benefit of splitting the app up in the first place.
I'd start with the crash reports and symbolicated stack traces from a crash reporting tool, looking for patterns like a specific iOS version, device model, or user action sequence. If the stack trace alone isn't conclusive, I'd add targeted logging or breadcrumbs around the suspected code path and ship that in a build to gather more context before the next report comes in.
I'd treat the local persistence layer, usually Core Data or a similar store, as the source of truth for the UI, with a separate sync layer reconciling local changes with the server in the background. I'd also design a conflict resolution strategy up front, since offline edits colliding with server-side changes is inevitable at any real scale.
I'd weigh the migration cost against the actual pain points in the current UIKit codebase, since a full rewrite carries real risk and rarely gets the sustained investment it needs to actually finish. I'd more often recommend an incremental approach, adopting SwiftUI for new features and screens while leaving stable, working UIKit code alone until there's a concrete reason to touch it.
I'd use Instruments' Energy Log and Time Profiler to identify what's consuming CPU or triggering excessive background activity, since battery drain often traces back to things like polling network requests, unnecessary location updates, or inefficient background processing. I'd fix the highest-impact offender first rather than trying to optimize everything at once.
I'd build a remote configuration layer that can toggle features without requiring an App Store release, since App Store review turnaround makes fast iteration difficult otherwise. I'd also keep a safe local default for every flag, so the app behaves reasonably even if the remote config fails to load.
I'd use Core Data's lightweight migration for straightforward changes, but for more complex restructuring, I'd write a custom mapping model and migration policy, testing it thoroughly against real production-like data before shipping. I'd also make sure the migration path is tested for users upgrading from several versions back, beyond just the immediately prior version.
I'd favor constructor-based injection through protocols over a heavy DI framework, since it keeps dependencies explicit and testable without adding a steep learning curve for the team. I'd reserve a more structured DI container for cases where the object graph genuinely becomes too complex to wire up manually.
I'd use OperationQueue with explicit dependencies between operations, giving me control over concurrency limits, cancellation, and prioritization that raw dispatch queues don't offer as cleanly. I'd also make sure each operation handles cancellation and failure gracefully, since background work is inherently more prone to interruption than foreground tasks.
I'd look closely for accidental data races, like a mutable property accessed from both a background task and the main actor without proper isolation, since Swift's concurrency checking catches some but not all of these issues at compile time. I'd also push for actors to encapsulate genuinely shared mutable state, rather than relying on manual locking or dispatch queue synchronization scattered through the codebase.
I'd centralize routing logic in a coordinator or router layer that can construct the correct navigation stack from a given deep link, rather than handling it ad hoc in individual view controllers. I'd also make sure the routing logic handles the case where the app is launched cold versus already running, since the navigation stack starts from a different state in each case.
I'd automate build, test, and code signing through a CI service like Xcode Cloud or Fastlane, so releases aren't dependent on a manual, error-prone process on one person's machine. I'd also build in automated checks, like unit test pass rates and binary size thresholds, that gate a build from being promoted to TestFlight or the App Store.
8-10 Years
I'd establish shared modules for common concerns, like networking, authentication, and design system components, so teams build on the same foundation instead of each solving the same problems independently. I'd keep the standards focused on things that create real cross-team risk if inconsistent, like security practices and API contracts, rather than mandating identical architecture patterns for every team's specific feature work.
I'd pilot SwiftUI adoption on a real but lower-risk project first, since its behavior and tooling maturity have shifted significantly across iOS versions, and a demo doesn't reveal the same friction points as production use. I'd base the broader rollout decision on what that pilot reveals about team productivity and any real limitations encountered, not on SwiftUI's marketing positioning alone.
I'd weigh how much duplicated effort exists across teams solving the same platform problems, like build tooling or shared component libraries, against the risk that heavy centralization slows product teams down. A hybrid model, where a platform team owns shared infrastructure but product teams retain ownership of their own features, is usually the right balance at larger scale.
I'd track Apple's deprecation timelines actively and budget dedicated time each cycle for adoption and migration, rather than letting deprecated API usage accumulate until it becomes an urgent App Store compliance issue. I'd evaluate new capabilities against actual product needs rather than adopting them reflexively just because they're new.
I'd frame it around measurable outcomes, like reduced build times or faster feature delivery across teams, rather than technical architecture details. I'd also be upfront about the disruption and short-term velocity cost of the migration itself, since underselling that tends to create friction with leadership partway through.
I'd set a measurable baseline, tied to real accessibility guidelines, and require automated accessibility checks in CI alongside manual review for new complex components, since accessibility regressions are easy to introduce silently. I'd also make accessibility part of design review from the start, since retrofitting it after a feature ships is far more costly than building it in.
I'd require a review process for adding new dependencies, weighing their maintenance activity, binary size impact, and privacy implications, since an unreviewed SDK sprawl creates real risk around App Store privacy manifests and app size. I'd also periodically audit existing dependencies, since a library that was reasonable to add years ago can become a liability if it goes unmaintained.
I'd weigh this against the product's actual needs, since native iOS gives the best performance and platform-idiomatic experience but costs more to maintain alongside a separate Android codebase. For products where deep platform integration or performance genuinely matters, native usually wins, while for simpler apps with tight resource constraints, a cross-platform approach can be the more pragmatic choice.
I'd centralize tracking of App Store guideline changes and privacy requirements, like nutrition labels and tracking permissions, so individual product teams aren't each independently monitoring Apple's policy changes. I'd also build a compliance checklist into the release process so issues get caught before submission rather than as a rejection from Apple.
I'd set a baseline expectation, like meaningful unit test coverage on business logic and view models, without mandating a rigid coverage percentage that teams end up gaming rather than genuinely improving quality. I'd invest in shared testing infrastructure and examples from strong teams so weaker teams have a concrete pattern to follow rather than starting from scratch.
I'd base the decision on actual user analytics showing adoption of older iOS versions, weighed against the maintenance cost of supporting deprecated APIs and workarounds. I'd communicate the tradeoff clearly to stakeholders, since dropping support for older devices always has some near-term user impact even when it's the right long-term call.
I'd default to established third-party tools for commodity capabilities like crash reporting or analytics, since building and maintaining that infrastructure in-house rarely adds real differentiation. I'd reserve in-house investment for capabilities genuinely core to the product's competitive advantage.
I'd combine structured onboarding to shared architecture patterns with hands-on mentorship on real feature work, since reading documentation alone doesn't build the judgment needed for real architectural tradeoffs. I'd also invest in internal knowledge sharing, like regular tech talks on platform changes, since Apple's ecosystem evolves quickly enough that continuous learning has to be built into the culture.
I'd make the tradeoff visible with data, like build time trends or crash rate correlated with release velocity, so it becomes a shared decision with product leadership rather than an engineering-only frustration. Reserving explicit capacity for platform health work, rather than treating it as leftover time, is usually what keeps it from being perpetually deprioritized.
10+ Years
I'd involve them directly in real architecture discussions, having them present design tradeoffs for review rather than just implementing decisions someone else already made. I'd also push them to articulate the reasoning behind choices like when to introduce a new abstraction, since that judgment is what separates someone who can build a feature from someone who can own an app's architecture.
I'd invest early in shared tooling, like a standard project template, CI pipeline, and reusable component library, so new teams don't each reinvent foundational decisions from scratch. I'd also build a lightweight architecture review process for major decisions, enough to catch organization-wide risks without slowing every team down with unnecessary process.
I'd translate the technical risk into business terms, like potential App Store rejection or a forced emergency migration under time pressure, and present a small number of clear options with tradeoffs. I'd also be direct about the cost of inaction, since executives need to weigh it against other priorities competing for the same engineering time.
I'd anchor the vision to where the business expects to scale, like new markets or product lines, and work backward to what platform capabilities and team structure that requires. I'd also revisit the vision regularly, since Apple's platform itself evolves quickly enough that a vision set once and never reassessed tends to go stale.
I'd push hard against tribal knowledge living only in a few people's heads, requiring architectural decisions to be documented and pairing to be a normal part of how critical systems get built. Losing a senior engineer should be a setback, not a crisis, and that only holds if the knowledge was genuinely distributed beforehand.
I'd build the case with concrete costs, like maintenance burden or how it blocks platform upgrades, and pair it with a realistic, staged migration path rather than a hard cutoff. I'd also involve the teams most affected in shaping that migration plan, since a top-down deprecation with no input from consuming teams tends to stall in practice.
I'd focus on making good practices the path of least resistance, through shared templates, linting rules, and reusable libraries, rather than relying on everyone reading a standards document closely. Real influence at this level comes from what you make easy to do right, beyond just what you formally mandate.
I'd keep postmortems focused on systemic gaps, like missing crash monitoring or unclear release verification steps, rather than individual mistakes, since a punitive culture just teaches people to hide problems instead of surfacing them. I'd also track whether the resulting action items actually get completed, since a postmortem process with no follow-through quietly becomes theater.
I'd stay closely involved in a meaningful slice of real app code, since that's what keeps my sense of the platform's actual state accurate and my credibility with engineering teams intact. The broader influence work, like shaping cross-team standards, has to be scoped so it doesn't crowd out that hands-on time entirely.
I'd define clear, observable behaviors at each level, like the scope of systems someone can own independently or their ability to navigate ambiguous cross-team tradeoffs, rather than vague seniority labels. I'd also make sure the framework values deep technical architecture as a legitimate senior path, not one that only funnels toward people management.
I'd break the initiative into milestones that each deliver measurable value along the way, so leadership sees real progress instead of betting everything on a distant finish line. I'd also revisit the business case periodically, since a multi-year initiative needs to stay justified as priorities and Apple's platform itself shift underneath it.
I'd bring concrete data showing the real cost of the current tradeoff, like rising crash rates or slowing feature velocity tied to a brittle codebase, since abstract technical-debt arguments rarely move a room focused on quarterly goals. I'd also propose a specific, time-boxed remediation plan rather than an open-ended ask, since that's usually easier for leadership to actually commit to.
I'd get engineers involved early in product discovery rather than handing them fully-formed specs, since that's where technical constraints and platform-specific tradeoffs get worked out together instead of causing friction later. Consistently following through on commitments made during that collaboration is what actually builds trust over time, beyond just any single process.
I'd want the architecture and practices I helped build to keep working well without me, which means prioritizing documentation, mentorship, and distributed ownership over being the single point of expertise. The clearest sign it worked is that the platform's quality doesn't visibly dip after I've moved on.




