Interview questions

Swift interview questions for senior developers (2026)

Language-level Swift questions for senior hires: value semantics, generics, opaque and existential types, Swift 6 strict concurrency, actors, ARC and macros.

These Swift interview questions test the language, not a specific Apple framework. They cover value and reference semantics, protocols and generics, opaque and existential types, Swift 6 strict concurrency with actors and Sendable, ARC, result builders and macros, plus a look at Swift on the server. At Ryz, senior Swift engineers are sourced by recruiters who review the code they have shipped, then take structured NTRVSTA AI interviews on topics like these, with recruiters reviewing each candidate before and after. AI scores are advisory and people make every decision. For app lifecycle, UIKit, push and App Store questions, use our iOS set instead.

How to use these questions

Swift rewards depth. A mid-level engineer can use async/await; a senior one can explain why the compiler rejects a capture as non-Sendable and fix it without sprinkling @unchecked. Choose five or six questions, with at least two from the concurrency and memory section for senior roles.

Fundamentals

What is the difference between value and reference semantics, and where does it bite?

Structs and enums are copied on assignment; classes share one instance. The trap is a struct holding a class reference: copying the struct copies the reference, so two "independent" values mutate the same object.

final class Box { var n = 0 }
struct Wrapper { let box = Box() }

let a = Wrapper()
let b = a
b.box.n = 5
print(a.box.n) // 5, shared reference inside a value type

What a strong answer shows: They reason about semantics, not memory location, and spot reference types hiding inside values.

How does copy-on-write work, and how would you add it to your own type?

Standard collections store elements in a heap buffer and copy it only when a mutation happens while the buffer is shared. For a custom type, wrap storage in a class and check isKnownUniquelyReferenced before mutating.

struct Matrix {
private final class Storage { var values: [Double]; init(_ v: [Double]) { values = v } }
private var storage: Storage

init(values: [Double]) { storage = Storage(values) }

mutating func set(_ i: Int, _ v: Double) {
if !isKnownUniquelyReferenced(&storage) {
storage = Storage(storage.values)
}
storage.values[i] = v
}
}

What a strong answer shows: They know copies are cheap until mutation and can implement the pattern correctly.

When is force unwrapping acceptable?

Rarely: when a nil value is a programmer error you want to crash on immediately, such as a bundled resource that must exist. Otherwise use if let x or guard let x (the shorthand form since Swift 5.7), optional chaining, ?? defaults, or throw a meaningful error.

What a strong answer shows: A principled rule tied to invariants, not "never" or "whenever convenient".

How do enums with associated values help model state?

They make illegal states unrepresentable. A loading state enum with .idle, .loading, .loaded([Item]) and .failed(Error) replaces three optionals that could contradict each other, and switch must be exhaustive. For enums from other modules that may grow, @unknown default warns when new cases appear.

What a strong answer shows: They design types so the compiler rules out bad combinations.

What does typed throws add in Swift 6, and when would you use it?

throws(ParseError) declares the exact error type, so callers can switch over it without casting, and generic code can propagate an error type. It suits small, closed domains. For most public APIs, untyped throws stays more flexible because adding a new error later is not a breaking change.

enum ParseError: Error { case empty, invalid(Character) }

func parseDigit(_ s: String) throws(ParseError) -> Int {
guard let c = s.first else { throw .empty }
guard let d = c.wholeNumberValue else { throw .invalid(c) }
return d
}

What a strong answer shows: They know the feature and also its API evolution cost.

Explain Swift's access levels, including package.

private (enclosing declaration and same-file extensions), fileprivate, internal (the default, module-wide), package (visible to other modules in the same Swift package, added in Swift 5.9), public, and open (subclassable and overridable outside the module).

What a strong answer shows: They use package to share internals across modules without making them public API.

Intermediate

What is the difference between some and any?

some Shape is an opaque type: one concrete type, chosen by the implementation, known to the compiler, so calls are statically dispatched and can be specialized. any Shape is an existential box that can hold any conforming type and can change at runtime, at the cost of indirection and dynamic dispatch. Since Swift 5.7, some works in parameter position as shorthand for a generic.

func area(of s: some Shape) -> Double { s.area } // generic, specialized
var shapes: [any Shape] = [Circle(r: 1), Square(side: 2)] // heterogeneous

What a strong answer shows: They default to some or generics and reach for any only when they need heterogeneity.

What does this print, and why?

protocol Greeter { func hello() -> String }
extension Greeter {
func hello() -> String { "protocol" }
func bye() -> String { "protocol bye" }
}
struct Friendly: Greeter {
func hello() -> String { "struct" }
func bye() -> String { "struct bye" }
}
let g: any Greeter = Friendly()
print(g.hello(), g.bye())

It prints struct protocol bye. hello is a protocol requirement, so it is dispatched dynamically through the witness table. bye exists only in the extension, so it is statically dispatched based on the declared type.

What a strong answer shows: They know requirements versus extension-only methods, a frequent source of real bugs.

How do associated types and primary associated types work?

An associated type is a placeholder a conforming type fills in, like Element in Collection. Primary associated types (Swift 5.7) let you constrain it in angle brackets: some Collection<Int> or any Publisher<Data, Error>. Before that you needed a full generic with a where clause.

What a strong answer shows: They can write constrained generic APIs that stay readable.

What is conditional conformance and when do you use it?

A generic type conforms to a protocol only when its parameters do: extension Array: Equatable where Element: Equatable. Wrappers then pick up Hashable, Codable or Sendable exactly when their contents allow.

What a strong answer shows: They propagate capabilities through generic types instead of forcing constraints on every user.

How does a result builder work? Sketch one.

A result builder transforms a closure's statements into calls to static methods such as buildBlock, buildOptional and buildEither. SwiftUI's @ViewBuilder is the best-known example.

@resultBuilder
enum QueryBuilder {
static func buildExpression(_ e: String) -> [String] { [e] }
static func buildBlock(_ parts: [String]...) -> [String] { parts.flatMap { $0 } }
static func buildOptional(_ part: [String]?) -> [String] { part ?? [] }
}

func whereClause(@QueryBuilder _ make: () -> [String]) -> String {
"WHERE " + make().joined(separator: " AND ")
}

What a strong answer shows: They see builders as compile-time transforms and know their cost on type-checking time.

When would you write a Swift macro, and what does it cost?

Macros generate code at compile time from SwiftSyntax: freestanding ones like #expect and attached ones like @Observable. Write one when boilerplate is mechanical and error-prone, such as conformances or memberwise helpers. The cost is a compiler plugin build, a SwiftSyntax dependency that can slow clean builds, and code that is harder to debug, so expand macros in Xcode during review.

What a strong answer shows: They reach for macros last, after functions, generics and property wrappers.

Concurrency and memory

Explain structured concurrency: async let, task groups and unstructured tasks.

async let and withTaskGroup create child tasks bound to a scope: the parent waits for them, cancellation flows down, and errors propagate up. Task { } creates an unstructured task that inherits actor context; Task.detached inherits nothing. Cancellation is cooperative, so long loops should check Task.isCancelled or call try Task.checkCancellation().

func loadProfile(id: User.ID) async throws -> Profile {
async let user = api.user(id)
async let posts = api.posts(for: id)
return try await Profile(user: user, posts: posts)
}

What a strong answer shows: They prefer structured tasks and explain why fire-and-forget tasks leak work.

What is actor reentrancy, and what bug does it cause here?

actor ImageCache {
private var cache: [URL: Image] = [:]
func image(for url: URL) async throws -> Image {
if let hit = cache[url] { return hit }
let img = try await download(url) // suspension point
cache[url] = img
return img
}
}

While one call is suspended at await, other calls run on the actor, so concurrent requests for the same URL all download. Fix it by storing an in-flight Task per URL and awaiting that, and re-check state after every suspension.

What a strong answer shows: They know actors serialize access but do not make multi-step logic atomic across awaits.

What does Sendable mean, and how do you fix a "non-Sendable type captured" error?

Sendable marks types safe to pass across isolation domains: value types with Sendable members, actors, immutable final classes. Fixes, in order: make the type a value type or immutable, isolate it to an actor or @MainActor, use sending parameters where ownership transfers, and only then @unchecked Sendable with real internal synchronization.

What a strong answer shows: They treat the compiler error as a design question, not something to silence.

How would you migrate a large codebase to the Swift 6 language mode?

Module by module. Enable complete concurrency checking as warnings first, fix leaf modules, annotate UI types with @MainActor, use @preconcurrency import for dependencies not yet audited, then flip each module to Swift 6 mode. Swift 6.2's approachable concurrency settings, including default main-actor isolation for app targets, reduce noise for UI-heavy code.

What a strong answer shows: An incremental plan with a clear order, and knowledge of the newer defaults rather than fighting the compiler module-wide at once.

A PR wraps a class in @unchecked Sendable with a lock. How do you review it?

Check that every stored property is accessed only under the lock, that no reference to mutable internals escapes, and that the lock is not held across an await. Then ask whether an actor or the Mutex type from the Synchronization module (Swift 6) would express the same thing with compiler checking.

import Synchronization

final class Counter: Sendable {
private let value = Mutex(0)
func increment() -> Int {
value.withLock { $0 += 1; return $0 }
}
}

What a strong answer shows: They know the compiler stops checking under @unchecked, so the reviewer has to.

Where do retain cycles come from with closures and Tasks?

An object stores a closure that captures self strongly: the classic cycle. Break it with [weak self], or [unowned self] only when the closure cannot outlive the object. A Task capturing self is not a permanent cycle, but it keeps self alive until the task finishes, which matters for infinite loops such as observing an AsyncSequence.

What a strong answer shows: They distinguish permanent cycles from extended lifetimes and know when unowned is unsafe.

What are noncopyable types and borrowing and consuming parameters for?

~Copyable types (Swift 5.9) cannot be copied, which models unique resources such as file handles or one-shot tokens. borrowing and consuming parameter modifiers state ownership explicitly, and a consuming method ends the value's lifetime.

What a strong answer shows: Awareness of Swift's ownership model and when it is worth the complexity.

Senior and architecture

You maintain a Swift package used by 15 teams. How do you design its public API?

Small public surface, protocols only where callers need substitution, value types for data, and package access for internals shared across its own modules. Follow semantic versioning, document availability, and run API breakage checks (swift package diagnose-api-breaking-changes) in CI. If distributed as a binary framework, library evolution and @frozen decisions become permanent.

What a strong answer shows: They think about callers they will never meet and about changes being cheap later.

A hot path is slower than expected. What Swift-specific causes do you look for?

Existentials (any) preventing specialization, generic code across module boundaries that is not specialized without @inlinable, unexpected copy-on-write copies inside loops, retain and release traffic on classes, and bridging to Foundation types. Profile with Instruments or perf on Linux, build with optimization, and confirm with a benchmark package before and after.

What a strong answer shows: They profile first and know which language features carry runtime cost.

What changes when you run Swift on the server?

Frameworks like Vapor and Hummingbird run on SwiftNIO event loops, so blocking calls (synchronous file or database I/O) stall every request on that loop. Code must build on Linux, where some Apple frameworks are absent, though the open-source swift-foundation narrows that gap. ARC also means no garbage-collector pauses.

What a strong answer shows: They know the event loop rule and Linux portability limits, not just the syntax.

How do you call C or C++ libraries from Swift?

C works through a module map or a system library target, with pointers exposed as UnsafePointer types. C++ interop (Swift 5.9 and later) is enabled per target and imports many C++ types directly. Wrap unsafe APIs in a small safe Swift layer that owns lifetimes, so unsafe pointers never leak into application code.

What a strong answer shows: They keep unsafe code contained and know interop is per target, not global.

How do you test async code with Swift Testing?

Tests are functions marked @Test that can be async throws, with #expect and #require for assertions. Parameterized tests take arguments: to run one test over many inputs. For callback-based events, confirmation checks that something happened the expected number of times.

@Test(arguments: ["", "abc", "x9"])
func rejectsInvalidInput(_ input: String) {
#expect(throws: ParseError.self) { try parseDigit(input) }
}

What a strong answer shows: Deterministic async tests with injected time, and fluency in the newer framework.

How would you model a payment flow's state so invalid transitions cannot compile?

Use distinct types per state (DraftPayment, AuthorizedPayment, CapturedPayment) where each transition is a function that consumes one and returns the next, or an enum with transition methods that return only legal next states. Noncopyable types can prevent reusing a consumed state.

What a strong answer shows: They use the type system to enforce business rules, and know when it becomes over-engineering.

How would you handle global mutable state, such as configuration, in Swift 6?

Global and static mutable variables are errors in Swift 6 mode unless isolated. Options: make them let with Sendable values, isolate them to @MainActor or a global actor, inject configuration as a dependency, or protect them with a Mutex. nonisolated(unsafe) exists but moves the safety burden onto you.

What a strong answer shows: They prefer injection and isolation, and treat unsafe escapes as last resorts with comments.

Red flags to watch for

A practical exercise

Run a 75-minute pairing session on a small Swift package (no UI): a rate-limited HTTP client wrapper that deduplicates concurrent requests for the same URL, caches responses with a time-to-live, and exposes an async API. Provide a skeleton with a mock transport and a failing test. Build it in Swift 6 language mode so strict concurrency errors appear as they work.

Hire senior Swift developers vetted with these questions

To skip running this loop yourself, hire senior Swift developers with Ryz: top 1% of the candidates we interview, on your team and in your repos, working within ±1h of US time zones, with deep experience in Swift 6 concurrency on Apple platforms and the server. Read how we screen in our vetting process, or start from our Swift developer job description.

FAQ

Which Swift version should interview questions target?

Swift 6 and its language mode. Concurrency checking changed how real code is written, so a candidate who only knows Swift 5 patterns will struggle with modern codebases. Ask what they had to change when migrating a module.

Are these questions only for iOS developers?

No. They focus on the language, so they also fit macOS, server-side Swift and package authors. For app roles, combine three or four of these with platform questions on lifecycle, persistence and release.

Should a Swift interview include live coding?

Yes, but keep it close to real work. A small package with tests in Swift 6 mode beats algorithm puzzles, because the compiler's concurrency errors expose understanding quickly and the candidate can use their normal tools.

Questions we didn't answer? Email info@ryzlabs.com.

Explore Ryz Labs

Staff augmentationDedicated development teamsAI pod teamsForward deployed engineersNearshore software developmentAI engineering teamsHire engineers by roleRyz Labs vs competitorsAlternatives guidesBuyer guidesCase studiesHow we vet engineers
Ryz Labs

Senior engineers in your time zone. AI pod teams that ship.

Tell us who you need. You'll get a scoped plan, a price and the names of the people who would do the work.

Start a conversation →