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.
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.
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.
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.
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".
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
~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.
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.
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.
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.
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.
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.
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.
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.
@unchecked Sendable or nonisolated(unsafe) by habit.await.[weak self] everywhere without understanding when it is needed.any everywhere and cannot explain the performance difference from generics.Task.detached to "get off the main thread" without thinking about priority, cancellation or isolation.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.
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.
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.
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.
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.