iOS · 13 min read

Swift interview questions and answers, as they are scored

Swift interview questions with spoken answers: value and reference types, protocols, generics, memory and errors, and the follow-up each one leads to.

This guide is for iOS engineers preparing the language part of a loop: the technical screen, and the Swift questions that turn up inside every other round. It gives twelve Swift interview questions and answers, each written the way you would say it out loud, with the trap the interviewer is setting and the follow-up that comes next. There is a worked example for a question candidates often answer with a confident demonstration that proves the opposite.

Long lists of Swift questions already exist, and this page does not try to be longer. It shows the part those lists leave out: the second question. A Swift question in an interview is almost never about syntax. It is about what happens when the interviewer pushes on your first answer.

Value and reference semantics

1. “When do you pick a struct over a class?”

What is scored: whether you know value semantics are shallow.

Spoken answer: “My default is a struct, since a copy behaves the way I expect and I can reason about it locally. I reach for a class when the thing needs one shared identity, inheritance, a deinit, or Objective-C interop. One caveat: if the struct stores a class instance, every copy points at the same instance, so a struct is not automatically safe to share across threads.”

The trap: “structs are thread-safe.”

The follow-up: “How would you implement copy-on-write yourself?” Wrap a class buffer, check isKnownUniquelyReferenced before mutating, and copy only if the buffer is shared. The standard collections already do this, which is why passing an Array is cheap.

2. “Does a closure freeze a struct at the moment it captures it?”

What is scored: whether you have debugged capture semantics or only read about them. This is the question in the worked example below.

Spoken answer: “No, not by default. With no capture list, the closure holds on to the variable itself and sees every change made after. Put the value in a capture list and it is copied at the point the closure is created, and even that copy is shallow: a class inside the struct is still shared.”

3. “What does a capture list really do, and when can you leave it out?”

What is scored: understanding, not a reflex.

Spoken answer: “A capture list controls how a closure captures: [weak self] to break a cycle, or [value] to snapshot something. Escaping closures stored on self need it, because a strong capture plus storage is a cycle. Non-escaping closures, like the one passed to map, cannot create a cycle, so I do not add [weak self] there.”

The trap: [weak self] on every closure. It adds optional handling where no cycle is possible, and it tells the interviewer you do not know which closures escape.

Protocols and generics

4. “How do some and any differ?”

What is scored: knowing existentials are not free.

Spoken answer: “some keeps one concrete type, known at compile time and hidden from the caller. any boxes the value in an existential, so the concrete type is gone: calls go through a witness table, large values may be heap-allocated, and the optimiser cannot specialise as much. I pay that only when I truly need mixed types in one collection or a type decided at runtime. Swift made any explicit so that cost is visible where you write it.”

The follow-up: “What does some mean in parameter position?” It is a short way to write a generic parameter: the caller picks one concrete type.

5. “What are associated types, and what problem do they create?”

What is scored: whether you can choose between generics, opaque types and type erasure.

Spoken answer: “An associated type is a placeholder in the protocol that each conforming type fills in, like Element on Collection. The catch is that such a protocol cannot always be used as a plain existential. So I reach for a generic with constraints, an opaque some return, or a type-erasing wrapper when I really need to store mixed conformers.”

The follow-up: “Why is [any Equatable] awkward?” Because == needs two values of the same type, and an existential does not guarantee that.

6. “When do you reach for generics instead of a protocol type?”

Spoken answer: “Most of the time. A generic keeps the type identity through the call, so the compiler can specialise and inline, and constraints like Hashable or Comparable say exactly what the type must support. I give that up for any only when I need values of different types in one place.”

Optionals, enums and errors

7. “How do you handle optionals, and when is a force unwrap acceptable?”

What is scored: taste, and how you treat the unhappy path.

Spoken answer: “I unwrap early with guard let so the rest of the function reads as the happy path, and I use optional chaining and ?? where they say what I mean. A force unwrap is acceptable only when nil is a programmer error I want to crash on, and then I prefer a precondition with a message.”

The follow-up: “What goes wrong with try? in production code?” It throws away the error, which is the information the bug report will need.

8. “How do enums with associated values help you model state?”

What is scored: design maturity.

Spoken answer: “They make illegal states unrepresentable. Instead of isLoading, error and items as three separate properties, which allow loading and failed at the same time, I model idle, loading, loaded(items) and failed(message). The compiler makes every switch exhaustive, and the view becomes a switch over the state.”

The follow-up: “A model has three optional properties. Why is that a problem?” That is eight combinations, and most are states that should never exist. An enum makes those states impossible to write.

9. “Throwing function, Result, or an optional?”

Spoken answer: “I pick by what the caller has to do next. If absence is ordinary and needs no explanation, say looking up a user by id in a local dictionary, it returns an optional. If the caller must react differently to different failures, say decoding a payload that can be malformed or from an unsupported version, it throws. If the outcome is kept and handled later, say collected from a batch of jobs, I use Result. Swift 6 adds typed throws, so an API can declare exactly which error type it throws; I use that where the set of errors is closed and stable, and plain throws elsewhere.”

10. “Codable works until one field is missing. What do you do?”

What is scored: whether you have shipped against a real API.

Spoken answer: “I decode defensively. New fields are optional so older or partial responses still decode. Enums get an unknown case so a new server value does not throw. Arrays of uneven quality decode element by element, so one corrupt item drops instead of failing the whole list. And I log the DecodingError, because it names the exact key path that failed.”

Memory

11. “How does ARC work, and where does it stop helping?”

Spoken answer: “ARC counts strong references and frees an object when the count reaches zero. It falls short on cycles: two objects holding each other strongly, or a closure stored by the object it captures. Those need weak or unowned on one side. ARC also has a cost, because retain and release are atomic operations, which shows up in hot loops.”

The follow-up: “How do you find a cycle?” Look for a deinit that never runs, then use the Memory Graph Debugger in Xcode to see the cycle directly. The senior half is proving the fix. In Allocations, use generation analysis: set a baseline, open and close the screen a few times, and check which objects from those visits survived. Then lock it with a test that holds only a weak reference and expects it to be nil once the owning scope ends:

@Test func modelIsReleased() async {
    weak var released: ProfileModel?
    do {
        let model = ProfileModel()
        released = model
        await model.load()
    }
    #expect(released == nil)   // a cycle keeps it alive and fails here
}

The trap next door: “Leak or zombie?” One object lives too long, the other not long enough. A leaked object is never freed, so memory keeps growing. A zombie has already been deallocated and something still sends it messages, which ends in EXC_BAD_ACCESS. Different bugs, different tools: Memory Graph or the Leaks instrument for leaks, the Zombie Objects diagnostic for zombies. The usual source of a zombie in Swift code is an unowned reference that outlived its target.

12. “weak or unowned, precisely?”

Spoken answer: “weak is optional and turns nil once the object is deallocated, so a late call just does nothing. unowned is not optional, and touching it once the object is gone traps. I default to weak, and use unowned only when two objects are created and destroyed together and I can say why in one sentence.”

Recovery line if you said “unowned to avoid the optional”: “That was the wrong reason. unowned promises the object lives at least as long as the reference. If I cannot back that promise, I use weak and handle the nil.”

Concurrency questions (actors, Sendable, @MainActor, reentrancy) are the other half of the Swift screen at senior level. They are covered in depth in senior iOS interview questions.

Worked example: the closure that was not frozen

Question 2 above is the one where a confident demonstration can prove the opposite of what the candidate claims. Here is the code an interviewer will put in front of you.

final class Settings { var delay = 30 }
struct Upload { var settings = Settings(); var attempts = 1 }

var upload = Upload()
let live = { print(upload.attempts) }            // captures the variable
let snapshot = { [upload] in print(upload.attempts, upload.settings.delay) } // copies the struct now

upload.attempts = 5
upload.settings.delay = 60
live()      // 5
snapshot()  // 1 60: the copy is shallow, Settings is shared

This compiles and prints the values in the comments (Swift 6.3.2, Swift 6 language mode). Two separate things are being tested:

  1. Default capture. live has no capture list, so it captures the variable upload itself and prints 5. Candidates who say “structs are copied into closures” and demonstrate it without a capture list are shown this line.
  2. Shallow copies. snapshot copied upload at creation, so attempts is still 1. But settings is a class instance, and copying the struct copied the reference, not the object. The delay reads 60.

The follow-up is usually “where does this bite in a real app?” Good answers: a @State value that holds a reference type, and diffing code that assumes a copied model cannot change underneath it.

How to use this list

Cover the spoken answer, say your own out loud, and time it. If it runs past thirty seconds, it is a lecture, not an answer. Then answer the follow-up without looking. Where your first answer was a definition, add the one production detail that shows you have used it: the bug it caused, the tool that found it, the reason you chose it. For a book to rehearse from, iOS interview books compares them by the time you have left.

What changed in 2024 to 2026

  • Swift 6.2 shipped on 15 September 2025 with what the Swift team calls approachable concurrency (Swift.org, Swift 6.2 released).
  • A module can opt in to main actor isolation by default, so its code runs on the main actor without @MainActor written on every type.
  • The new @concurrent attribute marks a function that should run concurrently, which makes it visible in the code when work leaves the actor.

Sources

What to do next

Free guide: the Phoenix Method