Swift • ARC and memory • SwiftUI and UIKit • Concurrency • App Store release • 2026

iOS Developer Interview Questions

30 questions What each one tests, an answer frame, a spoken answer 34 min read

This page is for anyone preparing for an iOS developer interview, from a first job to a senior role. Most rounds start with your path and an app you built, then test Swift basics like optionals, value types and protocols, memory and retain cycles, SwiftUI state and the UIKit lifecycle, and concurrency from Grand Central Dispatch to async/await and actors. Stronger rounds add networking, Core Data, testing, release stories and judgement calls. Each question shows what the interviewer is checking, the shape of a good answer and a short answer you can say out loud. Swap in your own apps and stories.

Search all questions by round, difficulty and level, or save the ones you want to practise.

Career and Teamwork 6 questions

Easy Screening round Fresher, Mid-level Practice question

1. Walk me through how you got into iOS development and what you have shipped so far.

What the interviewer is really testing:
Whether you chose iOS on purpose, can talk about real apps you built, and know where your skills stop today.
Answer frame:

Start: how you first got into building for iPhone, in one or two sentences.

Shipped: the apps or features you built, and whether real users touched them.

Next: what you want to get better at and why this role helps.

Sample spoken answer:

"I started with Swift in my final year because I wanted to build a study timer for my friends, and I got hooked on how fast you see your work on a real phone. That app went on the App Store and picked up a few hundred users, which taught me about review, crash reports and people using it in ways I didn't expect. After that I joined a small agency where I worked on two client apps, mostly in UIKit, with newer screens in SwiftUI. I built login, a feed with image caching, and push notification handling. What I want next is a product team where I own features for longer and get deeper into concurrency and performance, not just ship and hand over."

Red flag to avoid:

Listing tutorials and courses with nothing built for real users, or being unable to say what you personally did on a team app.

They may ask next:
  • What would you change about the first app you shipped if you rebuilt it today?
  • Which part of iOS development still feels hardest for you?
Say it in 60 seconds
Easy Screening round Fresher, Mid-level, Senior Practice question

2. Pick one iOS app you worked on. What did it do, what did you own, and what was the hardest technical problem?

What the interviewer is really testing:
Whether you can explain a product and your part in it clearly, and go one level deep on a real technical decision without hand-waving.
Answer frame:

Product: what the app does and who uses it, in one sentence.

Your part: the screens or systems you owned, not the whole team.

Hard part: one real problem, the options you had and what you chose.

Sample spoken answer:

"The one I'm proudest of is a field inspection app I built at my last company. Inspectors used it on sites with bad signal, so it had to work fully offline. I owned the data layer and the sync. The hardest part was sync conflicts, when two inspectors edited the same report. We looked at last-write-wins, but that silently lost notes, so I stored changes per field with a timestamp and merged field by field, and only asked the user when the same field changed on both sides. I also moved the upload to a background URLSession so large photo batches finished even if the inspector locked the phone. Complaints about lost work pretty much stopped after that release."

Red flag to avoid:

Describing the product for two minutes and never getting to a decision you made, or saying "we" so often the interviewer cannot tell what you did.

They may ask next:
  • How did you test the offline and conflict cases before release?
  • If you had another month on that app, what would you fix first?
Say it in 60 seconds
Medium Behavioral round Fresher, Mid-level, Senior Practice question

3. Tell me about a time a design you were given did not fit well on iOS. How did you work it out with the designer?

What the interviewer is really testing:
Whether you can push back on a design with platform reasons and a better option, while keeping the relationship and the design intent.
Answer frame:

The problem: what in the design fought the platform, such as gestures, accessibility or navigation.

How you raised it: show, do not argue; a prototype, the platform guidance, a real device.

Outcome: what you agreed and what you learned about working with that designer.

Sample spoken answer:

"On my last team, a designer gave me a custom bottom sheet with its own swipe gestures and a hand-drawn dimmed background. When I built it, the swipe fought with the system back gesture, and it didn't work well with larger text sizes or VoiceOver. Instead of saying it couldn't be done, I built two quick versions, his custom one and one using the system sheet with detents styled to match his colours and corners. We tried both on a real phone together, including with large text turned on. He liked that the system one felt native and got accessibility for free, and we kept his visual style. After that he started asking me to look at interaction ideas early, before designs were final, which saved us both a lot of rework."

Red flag to avoid:

Saying you just built whatever the design said, or that you overruled the designer without showing them why.

They may ask next:
  • What would you have done if the designer insisted on the custom version?
  • Which parts of the platform guidelines do you think matter most for a developer to know?
Say it in 60 seconds
Medium Situational round Mid-level, Senior Practice question

4. In a teammate’s pull request you find several force unwraps and a shared singleton being changed from background threads. How do you handle it?

What the interviewer is really testing:
Whether you can separate a real crash risk from style preferences and give feedback that fixes the code without damaging the relationship.
Answer frame:

Rank the issues: the data race is a must-fix; force unwraps depend on whether nil is truly impossible.

Explain concretely: say what can crash and when, and suggest a specific safer pattern.

Keep it kind: comment on the code, offer to pair, and turn repeated issues into a team rule or lint.

Sample spoken answer:

"I'd sort the problems first. The singleton being changed from background threads is a real data race, and those crash randomly and are painful to trace, so that's a must-fix before merging. I'd explain the exact scenario, suggest making that state an actor or confining it to the main actor, and mention that Thread Sanitizer would show it. The force unwraps I'd look at one by one. If a value can genuinely be nil, like data from the network, I'd suggest a guard let with a proper error path. If it's something like a URL built from a constant string, a force unwrap is fine and I'd say so. I keep comments about the code, not the person, and offer to pair if it's a bigger change. If it keeps coming up, I'd suggest a lint rule so reviews aren't personal."

Red flag to avoid:

Approving it to avoid friction, or leaving a wall of nit comments with the real data race buried among them.

They may ask next:
  • What would you do if the teammate is senior to you and disagrees?
  • Which checks would you automate so reviewers do not have to catch them by hand?
Say it in 60 seconds
Easy Culture fit round Fresher, Mid-level, Senior Practice question

5. Swift and iOS change every year. How do you keep up, and how do you decide what is worth adopting in a production app?

What the interviewer is really testing:
Whether you learn steadily and adopt new things with judgement, rather than chasing every announcement or ignoring change.
Answer frame:

How you learn: conference sessions, release notes, Swift Evolution and small experiments.

Filter: does it fix a real problem for our users or code, and does our minimum iOS version allow it?

Adopt safely: start with one small, low-risk area and share what you learned with the team.

Sample spoken answer:

"Every year after the new SDK is announced, I watch the sessions that touch what we actually build, and I read the release notes for the frameworks we use most. For Swift itself I skim the evolution proposals that are accepted, because that's where language changes are explained properly. Then I try the interesting parts in a small side project, not in the main app. For production, my filter is simple: does it solve a problem we really have, and can we use it given our minimum iOS version? If yes, I try it in one small, low-risk screen first and write a short note for the team on what worked. For example, we moved to async/await on new code first, and only later converted older code where it was touched anyway."

Red flag to avoid:

Saying you do not follow changes at all, or rewriting working code every time a new framework appears.

They may ask next:
  • What is a recent change you decided not to adopt yet, and why?
  • How do you share what you learn with the rest of your team?
Say it in 60 seconds
Easy Culture fit round Fresher, Mid-level, Senior Practice question

6. What does good code review look like on an iOS team to you, both when you review and when your own code is reviewed?

What the interviewer is really testing:
Whether you would raise the quality of the team through reviews, and take feedback on your own work without getting defensive.
Answer frame:

As author: small pull requests with a clear description, screenshots or a video for UI changes.

As reviewer: focus on correctness, threading, memory, accessibility and readability; leave style to tools.

Tone: explain the why, mark nits as nits, and treat feedback on your code as help, not judgement.

Sample spoken answer:

"When I send code for review, I try to keep the pull request small and say what changed and why, and for UI work I add screenshots or a short screen recording, including dark mode and a large text size. When I review, I focus on the things tools can't catch: is the logic right, is anything touching the UI off the main thread, could a closure create a retain cycle, does it work with VoiceOver, and will the next person understand it. Formatting and naming conventions I'd rather leave to a linter, so we don't argue about them. I explain the reason behind a comment and mark small things as optional. When my own code gets comments, I assume good intent, ask questions if I don't agree, and change it if they're right."

Red flag to avoid:

Treating review as a formality that only checks formatting, or getting defensive about every comment.

They may ask next:
  • How do you handle a review comment you strongly disagree with?
  • How big is too big for one pull request, in your view?
Say it in 60 seconds

Swift Language 3 questions

Easy Technical round Fresher, Mid-level Practice question

7. What is an optional in Swift, and what are the safe ways to get the value out of one?

What the interviewer is really testing:
Whether you understand that nil is part of the type system in Swift, and that you default to safe unwrapping instead of force unwraps.
Answer frame:

What it is: a type that holds either a value or nil; under the hood an enum with some and none.

Safe unwrapping: if let, guard let, ?? for a default, and optional chaining with ?..

Force unwrap: ! crashes on nil, so keep it for cases that truly cannot be nil.

Sample spoken answer:

"An optional says a value might be missing. String? means either a string or nil, and the compiler won't let me use it as a plain string until I deal with the nil case. Under the hood it's an enum with two cases, some and none. To unwrap safely I use if let when I only need the value in one branch, and guard let at the top of a function when I want to exit early and keep the unwrapped value for the rest. For a fallback I use the nil-coalescing operator, and for a chain of properties I use optional chaining, which just returns nil if any link is missing. A force unwrap with an exclamation mark crashes on nil, so I avoid it unless nil would be a programmer error."

Code:
func greeting(for name: String?) -> String {
    guard let name, !name.isEmpty else {
        return "Hello, guest"
    }
    return "Hello, \(name)"
}

let city = user.address?.city ?? "Unknown"
Red flag to avoid:

Scattering force unwraps everywhere, or saying an optional is "just a nullable pointer" with no idea the compiler forces you to handle nil.

They may ask next:
  • When is an implicitly unwrapped optional reasonable, for example with outlets?
  • What does map do on an optional, and when would you use it instead of if let?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level, Senior Practice question

8. When would you choose a struct over a class in Swift, and what actually happens when you copy each one?

What the interviewer is really testing:
Whether you understand value and reference semantics well enough to predict bugs, not just recite that structs live on the stack.
Answer frame:

Value type: assigning a struct gives an independent copy; changing one never changes the other.

Reference type: assigning a class shares one instance; it has identity, inheritance, deinit and ARC.

Default: start with a struct; pick a class when you need shared mutable state, identity or inheritance.

Sample spoken answer:

"Structs are value types, so when I assign one to another variable or pass it into a function, I get an independent copy. If I change the copy, the original doesn't change. Classes are reference types, so two variables can point at the same instance and a change through one shows up through the other. Classes also give me identity checks, inheritance and deinit, and they're managed by ARC. My default is a struct for models and view state because it's easier to reason about and safer across threads. The standard collections like Array and Dictionary use copy-on-write, so copying them is cheap until one side actually changes. I reach for a class when something needs to be shared and mutated in one place, like a cache or a view model several screens observe."

Red flag to avoid:

Saying the only difference is stack versus heap, or not knowing that mutating a copied class instance changes the original.

They may ask next:
  • What happens if a struct has a property that is a class instance, and you copy the struct?
  • Does your own custom struct get copy-on-write automatically?
  • Why are SwiftUI views structs?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

9. How do protocols and protocol extensions work together in Swift? What is a default implementation, and where can it surprise you?

What the interviewer is really testing:
Whether you use protocol-oriented design in practice and know the dispatch rule that makes extension-only methods behave unexpectedly.
Answer frame:

Protocol: a contract of requirements any struct, class or enum can adopt.

Extension: adds shared behaviour, including default implementations of requirements.

The trap: a method only in the extension, not declared in the protocol, is chosen by the static type, so a conforming type cannot really override it.

Sample spoken answer:

"A protocol is a contract, and any type can adopt it. With an extension I can give every conforming type a default implementation, so I get shared behaviour without a base class, and structs and enums can use it too. I use this a lot for things like a Loggable or a reusable-cell identifier. The surprise is dispatch. If the method is declared in the protocol itself, calls go through the conforming type's version even when the variable is typed as the protocol. But if the method only lives in the extension and isn't a requirement, Swift picks the version from the static type. So if my variable is typed as the protocol, it calls the extension version even though the concrete type has its own. The fix is to declare it in the protocol."

Code:
protocol Greeter {
    func greet() -> String            // requirement
}

extension Greeter {
    func greet() -> String { "Hello" } // default implementation
    func wave() -> String { "Wave" }   // extension only, not a requirement
}

struct Friendly: Greeter {
    func greet() -> String { "Hi there" }
    func wave() -> String { "Big wave" }
}

let g: any Greeter = Friendly()
print(g.greet()) // Hi there
print(g.wave())  // Wave
Red flag to avoid:

Treating protocols as Java-style interfaces only, with no idea that extensions can supply behaviour or that extension-only methods are statically chosen.

They may ask next:
  • When would you still prefer a base class over a protocol with extensions?
  • What does a protocol with an associated type change about how you can use it?
Say it in 60 seconds

Memory and Performance 4 questions

Medium Technical round Fresher, Mid-level, Senior Practice question

10. What does a capture list do in a Swift closure, and when do you actually need [weak self]?

What the interviewer is really testing:
Whether you know how closures capture variables and can tell a real retain cycle from a closure that is simply short-lived.
Answer frame:

Default capture: a closure captures variables themselves, so it sees later changes and holds class instances strongly.

Capture list: [x] copies the value at creation; [weak self] or [unowned self] changes how self is held.

When it matters: when self stores the closure, directly or through something it owns, and the closure uses self.

Sample spoken answer:

"By default a closure captures the variables it uses, not a snapshot, so if the variable changes later the closure sees the new value, and any class instance it touches is held strongly. A capture list lets me change that. Writing a variable in square brackets copies its value at the moment the closure is created, and writing weak self means the closure won't keep self alive. I need weak self when there's a cycle: self owns something that stores the closure, and the closure uses self. Stored completion handlers, timers and notification blocks are the usual cases. A non-escaping closure like the one passed to map can't outlive the call, so it can't cause a cycle. A one-off async call doesn't cause a cycle either, it just keeps self alive a bit longer."

Code:
var count = 1
let byReference = { print(count) }
let byValue = { [count] in print(count) }
count = 2
byReference() // 2
byValue()     // 1
Red flag to avoid:

Adding [weak self] to every closure by habit without being able to explain where the cycle is, or saying closures copy every variable they use.

They may ask next:
  • What is the difference between an escaping and a non-escaping closure?
  • Inside a closure with weak self, when would you write guard let self and when would you use optional chaining on self instead?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level, Senior Practice question

11. How does ARC decide when an object is freed, and how do weak and unowned references help you break a retain cycle?

What the interviewer is really testing:
Whether you understand reference counting well enough to spot a cycle and choose between weak and unowned for the right reason.
Answer frame:

ARC: counts strong references to each class instance and frees it when the count reaches zero; there is no cycle collector.

Retain cycle: two objects hold each other strongly, so neither count reaches zero.

Weak vs unowned: weak is optional and becomes nil when the object goes; unowned is non-optional and crashes if used after the object is freed.

Sample spoken answer:

"ARC keeps a count of strong references to every class instance. The compiler inserts the retains and releases, and when the count drops to zero the object is freed and deinit runs. It doesn't find cycles for you, so if a view controller holds a closure and that closure holds the view controller, neither ever goes away. I break a cycle by making one side not count. A weak reference is always optional and is set to nil automatically when the object is freed, so it's safe when the other object might go away first, which is why delegates are weak. Unowned doesn't make it optional, but if I touch it after the object is gone, the app crashes. I only use unowned when the other object is guaranteed to live at least as long, and weak whenever I'm unsure."

Code:
final class Loader {
    var onFinish: (() -> Void)?
}

final class ProfileViewController: UIViewController {
    let loader = Loader()

    override func viewDidLoad() {
        super.viewDidLoad()
        loader.onFinish = { [weak self] in
            self?.title = "Loaded" // no cycle: the closure does not own self
        }
    }
}
Red flag to avoid:

Saying ARC is a garbage collector that cleans up cycles, or choosing unowned just to avoid unwrapping.

They may ask next:
  • Why does a delegate protocol need to be class-only before you can hold the delegate weakly?
  • How would you confirm a view controller is actually freed after you dismiss it?
Say it in 60 seconds
Medium Behavioral round Mid-level, Senior Practice question

12. Tell me about a memory leak or retain cycle you tracked down in an iOS app. How did you find it and prove it was fixed?

What the interviewer is really testing:
Whether you have used the real tools to find a leak and can explain the cause precisely, not just that you added weak self until it went away.
Answer frame:

Symptom: what you noticed, such as memory climbing or a screen doing work after it closed.

Tools: how you found it, for example a deinit log, the memory graph debugger or Instruments.

Cause and proof: the exact reference cycle, the fix, and how you confirmed the object is now freed.

Sample spoken answer:

"At my last company, QA noticed memory climbing each time they opened and closed the chat screen, and sometimes old screens were still reacting to incoming messages. I added a log in the view controller's deinit and it never printed. Then I opened Xcode's memory graph debugger after closing the screen a few times and saw several copies of the controller still alive. The graph showed a shared socket manager holding a message handler closure, and that closure captured the controller strongly. So the singleton kept every chat screen alive. I changed the closure to capture self weakly and removed the handler when the screen closed. To prove it, deinit started printing, the memory graph showed one controller at a time, and I added a small test that checks a weak reference to the controller becomes nil after it's dismissed."

Red flag to avoid:

A vague story with no tool and no specific cycle, or claiming the fix was restarting the app or clearing caches.

They may ask next:
  • What is the difference between what the Leaks instrument shows and what the memory graph debugger shows?
  • How would you stop this kind of leak coming back in future code?
Say it in 60 seconds
Medium Behavioral round Mid-level, Senior Practice question

13. Tell me about a time a list or feed in your app was stuttering. How did you find the cause and make it smooth?

What the interviewer is really testing:
Whether you measure before you optimise and know the usual causes of dropped frames on iOS.
Answer frame:

Measure first: reproduce on a real, older device and profile with Instruments, such as Time Profiler.

Find the cause: usually heavy work on the main thread, like image decoding, formatting or complex layout.

Fix and verify: move work off main, cache it, then profile again on the same device.

Sample spoken answer:

"At my last company our product feed stuttered badly on older phones, and it was the first screen people saw. I reproduced it on the oldest device we supported and ran Time Profiler while scrolling. Two things stood out on the main thread. We were decoding full-size product photos, several times larger than the cell, and each cell was creating a new date formatter. I moved image decoding and downsampling to a background task, so we only decoded an image at the size it's actually shown, cached the results, and made the formatter a shared static. I also used the prefetching API to start loading images a few rows ahead. Then I profiled again on the same phone, and the long main thread spikes were gone and scrolling felt smooth. I kept that old phone in our release checklist after that."

Red flag to avoid:

Guessing at the cause and changing code without profiling, or only testing on the newest phone.

They may ask next:
  • Why does downsampling an image before showing it save memory as well as time?
  • How would you find the same kind of problem in a SwiftUI list?
Say it in 60 seconds

SwiftUI 3 questions

Medium Technical round Fresher, Mid-level, Senior Practice question

14. In SwiftUI, when do you use @State, @Binding, @StateObject and @ObservedObject? What goes wrong if you mix them up?

What the interviewer is really testing:
Whether you understand who owns each piece of state in SwiftUI, which is the root of most state bugs on real projects.
Answer frame:

@State: a simple value the view owns; SwiftUI stores it outside the struct so it survives redraws.

@Binding: read and write access to state owned by a parent, passed with a dollar-sign prefix.

Objects: @StateObject when this view creates and owns an ObservableObject; @ObservedObject when it is handed one owned elsewhere.

Sample spoken answer:

"The question I ask is who owns the data. If it's a small value that only this view cares about, like whether a sheet is showing, I use State, and SwiftUI keeps it alive even though the view struct is recreated all the time. If a child needs to read and change a parent's value, the child takes a Binding and the parent passes it with a dollar sign. For a reference type model, if this view creates it, it's StateObject, which is created once for the view's lifetime. If the object is created somewhere else and passed in, it's ObservedObject. The classic bug is creating a model inside a view with ObservedObject. Every time the parent redraws, the model can be recreated and the screen loses its data, like a half-filled form resetting."

Code:
struct CounterScreen: View {
    @StateObject private var model = CounterModel() // this view owns it
    @State private var showHelp = false

    var body: some View {
        VStack {
            CounterLabel(model: model)
            Toggle("Show help", isOn: $showHelp)
        }
    }
}

struct CounterLabel: View {
    @ObservedObject var model: CounterModel // owned by the parent
    var body: some View { Text("\(model.count)") }
}
Red flag to avoid:

Using @ObservedObject for an object the view creates itself, or passing a plain value where the child needs a binding and then wondering why changes are lost.

They may ask next:
  • When would you use @EnvironmentObject, and what happens if you forget to inject it?
  • Why should @State properties be private?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

15. What changes when you move a view model from ObservableObject to the @Observable macro? How do ownership and bindings work after the switch?

What the interviewer is really testing:
Whether you have used the Observation framework for real and understand why it updates views more precisely, not just that the syntax is shorter.
Answer frame:

Tracking: @Observable tracks each property a view actually reads in body, instead of one change signal for the whole object.

Less code: no @Published; stored properties are tracked unless marked @ObservationIgnored.

Ownership: own it with @State, pass it as a plain property, use @Bindable for bindings and @Environment to share it.

Sample spoken answer:

"With ObservableObject, any change to any Published property fires objectWillChange, and every view observing that object redraws, even if it only shows one field. With the Observable macro, SwiftUI records which properties a view actually reads in its body and only redraws when one of those changes, so big shared models get much cheaper. I also drop all the Published wrappers. Ownership changes too. A view that creates the model holds it in State instead of StateObject, children just take it as a normal property, and if a child needs a binding to one of its fields, it marks the property Bindable. For app-wide models I use the environment with the type as the key. One thing to watch: the model's initializer can run again whenever the parent rebuilds the view, even though SwiftUI keeps the first instance, so the init should stay cheap."

Code:
@Observable
final class ProfileModel {
    var name = ""
    var bio = ""
}

struct ProfileScreen: View {
    @State private var model = ProfileModel()
    var body: some View { ProfileForm(model: model) }
}

struct ProfileForm: View {
    @Bindable var model: ProfileModel
    var body: some View {
        TextField("Name", text: $model.name)
    }
}
Red flag to avoid:

Saying it is just a renamed ObservableObject, or still reaching for @StateObject and @Published with an @Observable model.

They may ask next:
  • If a view only reads model.name, does it redraw when model.bio changes, and why?
  • How would you support users on older iOS versions that do not have Observation?
Say it in 60 seconds
Hard Behavioral round Senior Practice question

16. Tell me about moving part of an app to a newer approach, like UIKit to SwiftUI or callbacks to async/await, without breaking what already worked.

What the interviewer is really testing:
Whether you can plan an incremental migration with bridges and safety nets, and judge what is worth migrating at all.
Answer frame:

Why and what: the concrete reason and which parts you chose to move first, and which you left alone.

Bridges: how old and new code lived together, such as UIHostingController or continuations.

Safety: tests, flags and release steps that kept users safe during the move.

Sample spoken answer:

"At my last company we had a large UIKit app and wanted new screens in SwiftUI without a rewrite. I proposed going screen by screen, starting with settings and profile, which were simple and rarely broke. Navigation stayed in UIKit, and each new SwiftUI screen was wrapped in a hosting controller, so the rest of the app didn't know the difference. For the networking layer we moved from callbacks to async/await in the same way. I added async versions next to the old methods using checked continuations, being careful to resume exactly once on every path, and moved callers over a few at a time. Each migrated screen went out behind a remote flag so we could switch back without a new release. We deliberately left the complex chat screen in UIKit, because moving it would have cost a lot and fixed nothing users felt."

Red flag to avoid:

Proposing a full rewrite as the first option, or migrating everything at once with no way to switch back.

They may ask next:
  • What happens if a continuation is resumed twice, or never?
  • How did you share state between a UIKit screen and a SwiftUI screen during the move?
Say it in 60 seconds

UIKit 1 questions

Easy Technical round Fresher, Mid-level Practice question

17. Walk me through the lifecycle of a UIViewController. Where do you put one-time setup, layout-dependent work and cleanup?

What the interviewer is really testing:
Whether you know which callbacks run once and which run many times, so you avoid duplicate work and layout bugs.
Answer frame:

Once: loadView builds the view, viewDidLoad runs once after it loads; do one-time setup there.

Each time it shows: viewWillAppear, viewDidAppear, then viewWillDisappear, viewDidDisappear.

Layout: viewDidLayoutSubviews can run many times; frames are not final in viewDidLoad.

Sample spoken answer:

"First the controller is created, then loadView creates its view, and viewDidLoad runs once after that. That's where I add subviews, set constraints and wire up bindings. The bounds aren't final yet, so I don't do frame math there. Every time the screen is about to show, viewWillAppear runs, which is where I refresh data that might have changed while it was hidden. viewDidAppear runs once it's on screen, so I start animations or log the screen view there. Layout callbacks like viewDidLayoutSubviews can run many times, for rotation or size changes, so anything in them has to be cheap and safe to repeat. When it leaves, viewWillDisappear and viewDidDisappear run, where I pause timers or stop observing. Finally deinit runs when nothing holds it anymore."

Red flag to avoid:

Doing frame-based layout in viewDidLoad, or adding observers in viewWillAppear without removing them, so they stack up each visit.

They may ask next:
  • If a user starts a back swipe and then cancels it, which callbacks run?
  • Why is adding subviews in viewWillAppear usually a bug?
Say it in 60 seconds

Concurrency 4 questions

Easy Technical round Fresher, Mid-level Practice question

18. Why must UI updates happen on the main thread, and how would you use Grand Central Dispatch to move heavy work off it?

What the interviewer is really testing:
Whether you understand the main thread rule and the basic GCD pattern, including the one call that deadlocks.
Answer frame:

Why: UIKit and SwiftUI are not thread-safe; the main thread runs the event loop that draws and handles touches.

Pattern: do heavy work on a background queue, then hop back with DispatchQueue.main.async for UI.

Traps: calling DispatchQueue.main.sync from the main thread deadlocks; the Main Thread Checker catches UI calls off main.

Sample spoken answer:

"UIKit isn't thread-safe, and the main thread runs the loop that handles touches and draws the screen. If I update views from another thread I get random glitches or crashes, and if I do slow work on the main thread the app freezes. So the pattern is to send heavy work like parsing a big file or resizing images to a global queue with a suitable quality of service, and when it's done, dispatch back to the main queue to update the UI. The main queue is serial, so UI updates happen in order. One classic mistake is calling sync on the main queue while already on the main thread, which deadlocks because it waits for itself. In debug builds Xcode's Main Thread Checker flags UI calls made off the main thread, which catches most of these early."

Code:
DispatchQueue.global(qos: .userInitiated).async {
    let thumbnail = makeThumbnail(from: largeImage) // slow work
    DispatchQueue.main.async {
        imageView.image = thumbnail                  // UI on main
    }
}
Red flag to avoid:

Not knowing why the main thread matters, or suggesting main.sync as a way to update UI from background code.

They may ask next:
  • What is the difference between a serial and a concurrent queue?
  • How does quality of service change the way your work is scheduled?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

19. How does async/await change networking code compared with completion handlers? How do you run two requests in parallel and handle cancellation?

What the interviewer is really testing:
Whether you understand what structured concurrency buys you beyond nicer syntax: errors, parallel work and cooperative cancellation.
Answer frame:

Readability and safety: code reads top to bottom, errors use throws, and you cannot forget to call a callback.

Parallel: async let or a task group start work together; await collects results.

Cancellation: it is cooperative; child tasks are cancelled with the parent, and code checks Task.isCancelled or throws.

Sample spoken answer:

"With completion handlers, the flow jumps around, every path has to remember to call the handler exactly once, and errors are passed as optionals. With async/await the code reads top to bottom, errors are thrown and caught normally, and the compiler makes sure every path returns or throws. An await is a suspension point: the thread is freed for other work while we wait, and the function may resume on a different thread unless it's isolated to an actor like the main actor. To load a profile and its posts together, I use async let for both and then await both results, so they run in parallel. Cancellation is cooperative. If the parent task is cancelled, the children are too, and URLSession's async methods throw when cancelled, but my own long loops need to check for cancellation themselves."

Code:
func loadScreen(userID: Int) async throws -> (User, [Post]) {
    async let user = api.fetchUser(id: userID)
    async let posts = api.fetchPosts(userID: userID)
    return try await (user, posts) // both run at the same time
}
Red flag to avoid:

Saying await blocks the thread, or assuming code after an await is still on the main thread without any actor isolation.

They may ask next:
  • How would you wrap an old completion-handler API so it can be awaited, and what rule must you follow?
  • When would you choose a task group over async let?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

20. What problem do actors solve in Swift, what is actor reentrancy, and where does @MainActor fit in?

What the interviewer is really testing:
Whether you can use actors to remove data races and know the reentrancy trap that catches people who think an actor method runs start to finish without interruption.
Answer frame:

Isolation: an actor lets only one task touch its mutable state at a time; outside callers must await.

Reentrancy: at every await inside an actor method, other calls can run, so state may change across the suspension.

@MainActor: a global actor for the main thread; put it on UI code and view models.

Sample spoken answer:

"Actors protect mutable state from data races. Only one task runs on an actor's state at a time, and code outside the actor has to await to reach it, so the compiler catches unsafe access instead of me finding it in a crash log. The catch is reentrancy. When an actor method hits an await, it gives up the actor, and other calls can run in between. So anything I checked before the await might be different after it. A classic example is an image loader: two callers ask for the same URL, both see it's not cached, and both download it. The fix is to store the in-flight Task before awaiting, so the second caller awaits the same task. MainActor is a global actor tied to the main thread, and I mark view models and UI code with it so updates always land on main."

Code:
actor ImageLoader {
    private var tasks: [URL: Task<Data, Error>] = [:]

    func data(for url: URL) async throws -> Data {
        if let existing = tasks[url] {
            return try await existing.value
        }
        let task = Task {
            let (data, _) = try await URLSession.shared.data(from: url)
            return data
        }
        tasks[url] = task // stored before awaiting, so callers share it
        return try await task.value
    }
}
Red flag to avoid:

Believing an actor method cannot be interleaved with other calls, or putting everything on the main actor to silence compiler warnings.

They may ask next:
  • In that loader, what happens to later callers if one download fails, and how would you fix it?
  • What does Sendable mean, and why does the compiler ask for it when you pass values into an actor?
Say it in 60 seconds
Hard Coding round Mid-level, Senior Practice question

21. Write a view model for a search field that waits until the user stops typing before calling the API, and never shows results from an older query.

What the interviewer is really testing:
Whether you can combine tasks, cancellation and the main actor into a small, correct piece of real app code, including the stale-result race.
Answer frame:

Debounce: on each keystroke cancel the previous task, sleep briefly, then search.

Stale results: check cancellation after the sleep and again after the network call, before updating state.

Isolation: mark the view model @MainActor so published results change on the main thread.

Sample spoken answer:

"I'd keep a reference to the current search task. Every time the text changes, I cancel that task and start a new one. The new task sleeps for a short delay first, and if the user types again during that time, it gets cancelled and never reaches the API. That's the debounce. The subtle part is stale results. A slow response for an old query could arrive after a newer one, so after the network call I check again whether this task was cancelled, and only then assign the results. Because the whole view model is on the main actor, the task it creates runs there too, so the results update on the main thread. The service protocol is Sendable so it's safe to call from the task. Passing it in also makes this easy to test with a fake."

Code:
protocol SearchService: Sendable {
    func search(_ text: String) async throws -> [String]
}

@MainActor
final class SearchViewModel: ObservableObject {
    @Published private(set) var results: [String] = []
    private var searchTask: Task<Void, Never>?
    private let service: any SearchService

    init(service: any SearchService) { self.service = service }

    func queryChanged(_ text: String) {
        searchTask?.cancel()
        searchTask = Task {
            try? await Task.sleep(nanoseconds: 300_000_000)
            guard !Task.isCancelled else { return }
            let found = (try? await service.search(text)) ?? []
            guard !Task.isCancelled else { return }
            results = found
        }
    }
}
Red flag to avoid:

Firing a request on every keystroke and assigning whatever comes back last, which shows old results when responses arrive out of order.

They may ask next:
  • How would you show a loading state and an error message without flicker?
  • How would you write a unit test for this without waiting in real time?
Say it in 60 seconds

Data and Networking 3 questions

Easy Technical round Fresher, Mid-level Practice question

22. How would you decode a JSON API response into Swift models with Codable when the JSON keys do not match your property names?

What the interviewer is really testing:
Whether you can write everyday networking code: fetching, checking the response and decoding, including mismatched keys and missing fields.
Answer frame:

Model: a struct conforming to Decodable, with optionals for fields that may be missing.

Key mapping: a CodingKeys enum, or a key decoding strategy like convertFromSnakeCase.

Fetch: URLSession, check the HTTP status, then decode with JSONDecoder and handle errors.

Sample spoken answer:

"I'd make a struct that conforms to Decodable, with a property for each field I need. If the API sends snake case, like full underscore name, I either add a CodingKeys enum that maps each property to its JSON key, or set the decoder's key strategy to convert from snake case. Fields that might be missing become optionals, and the synthesized decoder then treats a missing key as nil instead of failing. For the request, I use URLSession's async data method, check the response is an HTTP status in the 200 range, and only then decode. If decoding fails, the DecodingError tells me which key or type was wrong, which I log in debug builds because it saves a lot of guessing. Dates need their own strategy, like ISO 8601, set on the decoder."

Code:
struct User: Decodable {
    let id: Int
    let fullName: String
    let avatarURL: URL?

    enum CodingKeys: String, CodingKey {
        case id
        case fullName = "full_name"
        case avatarURL = "avatar_url"
    }
}

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 let http = response as? HTTPURLResponse,
          (200..<300).contains(http.statusCode) else {
        throw URLError(.badServerResponse)
    }
    return try JSONDecoder().decode(User.self, from: data)
}
Red flag to avoid:

Parsing JSON by hand into dictionaries with force casts, or decoding without ever checking the HTTP status code.

They may ask next:
  • With convertFromSnakeCase, what property name does "avatar_url" map to, and why might that surprise you?
  • How would you decode a field that is sometimes a number and sometimes a string?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

23. Explain the main pieces of Core Data, and how you keep it safe when you import data on a background thread.

What the interviewer is really testing:
Whether you know Core Data beyond the template code, especially the context-per-queue rule that causes most Core Data crashes.
Answer frame:

Pieces: the model is the schema, the persistent container sets up the store, and a managed object context is the workspace where you fetch, change and save.

Threading rule: each context belongs to one queue; work inside perform, and never pass managed objects between contexts.

Background import: use a background context, pass object IDs across, and let the view context merge changes.

Sample spoken answer:

"Core Data has a model, which describes the entities, a persistent store, usually SQLite, and managed object contexts, which are like scratchpads where I fetch objects, change them and then save. NSPersistentContainer sets all of that up. The rule that matters most is that a context and its objects belong to one queue. The view context runs on the main queue for the UI. For a big import I create a background context, or use performBackgroundTask, and do all the work inside perform so it runs on that context's queue. I never hand a managed object to another thread. If I need it elsewhere, I pass its object ID and fetch it again in the other context. I turn on automatic merging on the view context so the UI picks up background saves, and set a merge policy for conflicts."

Red flag to avoid:

Using the main view context for a large import, or passing NSManagedObject instances between threads.

They may ask next:
  • How would you catch Core Data threading mistakes during development?
  • What would make you choose SwiftData or a plain SQLite layer instead?
Say it in 60 seconds
Hard Situational round Mid-level, Senior Practice question

24. Users complain the app loses what they typed when the network drops during submit. How would you redesign that flow?

What the interviewer is really testing:
Whether you think about real mobile conditions like flaky networks and app suspension, and design for no lost work and no duplicates.
Answer frame:

Save first: persist the draft locally as the user types and when the app goes to the background.

Send from an outbox: queue the submission locally, send it, and retry with backoff when the network returns.

No duplicates, clear status: a unique request ID so retries are safe, and a visible pending or failed state.

Sample spoken answer:

"I'd start by reproducing it with the Network Link Conditioner, and I'd bet the form only lives in memory, so a failed request or the app being suspended wipes it. First, I'd save the draft locally as the user types, with a short debounce, and again when the scene goes to the background. Second, submit would write the item to a small local outbox before sending. If the request fails, it stays there and I retry with backoff when the network path monitor says we're back online. Third, each item gets a unique ID that the backend uses to ignore duplicates, because a request can succeed on the server while the response is lost on the way back. In the UI I'd show a clear pending state and a failed state with retry, so people trust that their work is safe."

Red flag to avoid:

Only adding an error alert that says try again, or retrying blindly without any protection against duplicate submissions.

They may ask next:
  • Where would you store the outbox, and why there?
  • What changes if the user signs out while items are still waiting to send?
Say it in 60 seconds

App Lifecycle 2 questions

Medium Technical round Fresher, Mid-level, Senior Practice question

25. What states can an iOS app be in, and what should your code do when the user sends it to the background?

What the interviewer is really testing:
Whether you know the app and scene lifecycle well enough to save user work and not rely on callbacks that may never run.
Answer frame:

States: not running, inactive, active, background and suspended.

Scenes: the scene delegate or SwiftUI scenePhase handles each window’s foreground and background; the app delegate handles process-level events.

On background: save user work and pause what is running; a suspended app can be ended without warning.

Sample spoken answer:

"An app can be not running, inactive, active, in the background or suspended. Inactive is when it's in the foreground but not getting events, like when Control Center is pulled down. Background means it can still run code for a short time, and suspended means it's in memory but not running at all. Since scenes came in, the scene delegate handles each window's foreground and background changes, and the app delegate handles process-level things like launch setup and push registration. In SwiftUI I read scenePhase from the environment. When the app goes to the background, I save drafts and state, pause timers and media, and if I need to finish a short task like an upload, I ask for extra background time. I never rely on the will-terminate callback, because the system can end a suspended app without calling anything."

Red flag to avoid:

Saving critical data only in applicationWillTerminate, or not knowing the difference between background and suspended.

They may ask next:
  • How would you run a periodic sync while the app is not open?
  • What happens to an in-flight network request when the app is suspended?
Say it in 60 seconds
Medium Situational round Mid-level, Senior Practice question

26. Product wants a feature built on an API that only exists in the latest iOS, but many of your users are still on older versions. How do you decide?

What the interviewer is really testing:
Whether you can weigh user reach against engineering cost using data, and know how to ship a feature with a fallback.
Answer frame:

Data: check analytics for how many active users are on each iOS version, especially paying users.

Options: use availability checks with a simpler fallback, build it for everyone the older way, or raise the minimum version later.

Cost: name the cost of keeping two code paths and agree when to remove the old one.

Sample spoken answer:

"I'd start with our own analytics, because the answer depends on how many active users, and especially paying users, are on older versions. If most people are on the new version, I'd build the feature with the new API behind an availability check, and give older versions either a simpler fallback or no feature at all with nothing broken. If a big group is still on older versions, I'd look at whether the feature can be built with older APIs at an acceptable cost. I'd be honest with the product owner that two code paths means more testing and more bugs, so I'd put a date on dropping the old path, usually when we next raise our minimum iOS version. Whatever we pick, I'd test on a real device running the oldest version we support."

Code:
if #available(iOS 17, *) {
    showNewExperience()
} else {
    showClassicExperience()
}
Red flag to avoid:

Either refusing the feature outright or raising the minimum version without looking at who would lose access.

They may ask next:
  • What is the difference between #available and @available?
  • What would convince you to raise the app’s minimum iOS version?
Say it in 60 seconds

Testing and Release 4 questions

Medium Technical round Fresher, Mid-level, Senior Practice question

27. How would you make a view model that loads data from an API unit-testable, and what would you actually test?

What the interviewer is really testing:
Whether you design for testing through dependency injection and test behaviour that matters, rather than chasing coverage.
Answer frame:

Inject: depend on a protocol for the API, passed in through the initializer, not a hard-coded singleton.

Fake: a small stub returns success or a chosen error, so tests are fast and never hit the network.

Test behaviour: loading, success, empty and error states, and what the user would see in each.

Sample spoken answer:

"The main thing is not letting the view model reach out to a singleton network client. I define a small protocol for what it needs, like fetch profile, and pass it in through the initializer. In the app I pass the real client, and in tests a stub that returns whatever I tell it to, a success or a specific error. Then I test the states a user would see: that it shows loading, that on success the title is set, that an empty list shows the empty message, and that an error shows a retry option instead of crashing. Tests can be async, so I just await the load call and assert. I keep UI tests for a few critical flows like sign-in and checkout, because they're slower and more fragile than unit tests."

Code:
import XCTest
@testable import MyApp

// ProfileAPI is a protocol in the app target
struct StubAPI: ProfileAPI {
    let result: Result<String, Error>
    func fetchName() async throws -> String { try result.get() }
}

final class ProfileViewModelTests: XCTestCase {
    @MainActor
    func testShowsNameOnSuccess() async {
        let model = ProfileViewModel(api: StubAPI(result: .success("Asha")))
        await model.load()
        XCTAssertEqual(model.title, "Asha")
    }
}
Red flag to avoid:

Saying the app is too UI-heavy to test, or writing tests that call the real server and fail whenever the network is slow.

They may ask next:
  • How would you test code that depends on the current date or a timer?
  • What would you not bother unit testing on an iOS app?
Say it in 60 seconds
Medium Behavioral round Mid-level, Senior Practice question

28. Tell me about a crash that reached real users. How did you find the cause, and what did you change so it would not happen again?

What the interviewer is really testing:
Whether you can read symbolicated crash reports, reason about the real cause, and fix the class of bug rather than only the one line.
Answer frame:

Signal: where the crash showed up, such as the Xcode Organizer or your crash reporting tool, and how many users it hit.

Diagnosis: the symbolicated stack trace, the pattern in devices or steps, and how you reproduced it.

Fix and prevention: the fix plus a test, a guard or a process change so the same kind of bug cannot slip through.

Sample spoken answer:

"After one release at my last company, our crash reporting tool showed a new top crash: index out of range inside a table view data source. It only happened on the orders screen, mostly when people pulled to refresh while a background update was coming in. The stack trace was symbolicated, so I could see the exact line. The cause was that a network callback changed the orders array on a background thread while the table was reloading on the main thread, so the count and the rows didn't match. I moved all updates to the list onto the main actor and switched the screen to a diffable data source, which applies changes as one snapshot. I added a test that fires refreshes from several tasks at once, and I turned on Thread Sanitizer in one of our CI test runs so data races like this get caught earlier."

Red flag to avoid:

Wrapping the crashing line in a check that hides the symptom without understanding why the data was wrong.

They may ask next:
  • What do you need to keep from each build so crash reports can be symbolicated?
  • How did you decide whether this needed a hotfix or could wait for the next release?
Say it in 60 seconds
Medium Situational round Mid-level, Senior Practice question

29. Your build is rejected in App Store review two days before a planned launch. What do you do?

What the interviewer is really testing:
Whether you know how app review works in practice, stay calm, and communicate early with the people planning the launch.
Answer frame:

Read carefully: understand the exact guideline and what the reviewer saw; it may be a misunderstanding.

Respond or fix: reply with notes or a demo account if they could not test, or make the smallest fix and resubmit.

Communicate: tell the launch owner the risk now; ask for an expedited review if the deadline truly matters.

Sample spoken answer:

"First I'd read the rejection message closely and look at any screenshots, because a lot of rejections are the reviewer not being able to get in or not finding a feature. If that's the case, I reply in App Store Connect with a working demo account and clear notes on where to find things. If it's a real issue, like a permission prompt without a clear purpose string or a missing way to delete an account when we let people create one, I make the smallest fix that addresses it and resubmit. If the deadline really matters, I can request an expedited review, though that's never guaranteed. In parallel I tell the product owner straight away, with a realistic view of the risk, so marketing can plan. Going forward I'd submit earlier and set the release to manual so approval and launch day are separate."

Red flag to avoid:

Arguing angrily with the reviewer, hiding the rejection from the launch owner, or quietly removing features to slip past review.

They may ask next:
  • What would you do if you believe the reviewer applied the guideline wrongly?
  • Which things do you check before every submission to avoid common rejections?
Say it in 60 seconds
Hard Situational round Mid-level, Senior Practice question

30. You release a new version and crash reports start climbing within hours. You cannot roll back on the App Store. What do you do?

What the interviewer is really testing:
Whether you know the limits of mobile releases, have tools to reduce harm before a fix ships, and keep people informed.
Answer frame:

Stop the spread: pause the phased release if one is running; turn off the feature with a remote flag if possible.

Diagnose fast: group crashes by stack, OS and device; find the change that caused it.

Fix and follow up: ship a small hotfix, ask for an expedited review, tell support and stakeholders, then do a blameless review.

Sample spoken answer:

"My first goal is to stop more people getting the bad build. If we used a phased release, I'd pause it straight away, which limits new automatic updates. If the crashing code is behind a remote flag, I'd switch it off, which can fix things for users who already updated, without waiting for review. Then I'd look at the crash reports grouped by stack trace, OS version and device, and compare with what changed in this release. Once I know the cause, I'd ship the smallest possible fix, not a bundle of other changes, and request an expedited review. I'd keep support and the product owner updated so they can answer users. Afterwards we'd do a blameless review and ask why this wasn't caught, and whether risky features should always ship behind a flag."

Red flag to avoid:

Assuming you can revert to the previous version like a web deploy, or bundling the fix with unrelated features.

They may ask next:
  • Why does removing the app from sale not help users who already updated?
  • What would you put behind remote flags by default from now on?
Say it in 60 seconds
Were you asked something else? Share it A person checks every question before it goes on the site. No name is shown.
For the call itself

The questions above are the prep. The call has ten more.

ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.

Download ClapAssist with 10 free minutes
Mac and Windows · Stays out of screen share · No card