Swift interview questions: a practical Swift & iOS guide
If you're preparing for a senior or mid-level iOS role, you'll encounter Swift interview questions that probe how well you understand the language's core principles. These aren't trick questions—they're testing whether you can write safe, performant code and avoid common pitfalls in production apps. We've collected the questions that come up repeatedly across e-commerce, SaaS, and real-time platforms, along with the nuanced answers that separate experienced engineers from the rest.
Struct vs class: understanding value vs reference semantics
This is the foundational question in any Swift interview. The answer sounds simple—structs are value types, classes are reference types—but the real test is whether you understand what that means for your code.
Value semantics mean each instance gets its own copy of the data. When you assign a struct to another variable or pass it to a function, you're creating a new copy. This is memory-safe by default: mutations to one copy don't affect others. Classes, by contrast, use reference semantics. Multiple variables can point to the same object in memory. When you modify that object through one reference, all references see the change.
Here's where it matters in real work:
// STRUCT: Value type
struct User {
var name: String
var email: String
}
var user1 = User(name: "Alice", email: "alice@example.com")
var user2 = user1
user2.name = "Bob"
print(user1.name) // "Alice" — unchanged
print(user2.name) // "Bob"
// CLASS: Reference type
class Account {
var balance: Double
init(balance: Double) { self.balance = balance }
}
let acc1 = Account(balance: 1000)
let acc2 = acc1
acc2.balance = 500
print(acc1.balance) // 500 — both reference the same object
print(acc2.balance) // 500In a payment processing system, you'd use a struct for immutable data like an order item (it shouldn't change once created) and a class for mutable state like a shopping cart that multiple screens need to observe and update. The rule of thumb: use structs by default unless you need shared, mutable state or reference identity.
Interview follow-up: "When would you use a class over a struct in a production app?" Answer honestly—thread safety, lazy initialization, inheritance, or deinit cleanup are valid reasons. But recognize that Swift's preference for value types is intentional: fewer reference cycles, more predictable memory behavior, and thread-safety through immutability.
What are optionals: safely handling nil
Optionals are Swift's way of making nil explicit in the type system. An optional is a container that either holds a value of some type or is nil. The syntax String? means "a String or nothing."
The interview question really asks: do you understand the problem optionals solve, and can you handle them safely?
// Without optionals (other languages):
// func fetchUser(id: Int) -> User
// What if the user doesn't exist? Crash? Return a dummy user? Confusing.
// With optionals (Swift):
func fetchUser(id: Int) -> User? {
// Returns User or nil
if userExists(id) {
return User(id: id, name: "Alice")
}
return nil
}
// Safe unwrapping with guard:
guard let user = fetchUser(id: 123) else {
print("User not found")
return
}
print(user.name) // We know user is not nil here
// Optional chaining:
let userName = fetchUser(id: 123)?.name // Returns String? or nil
// Nil coalescing:
let displayName = fetchUser(id: 123)?.name ?? "Guest" // Default to "Guest" if nilIn a real CRM platform, you're constantly dealing with nullable fields: a user might not have a phone number, a customer might not have a stored payment method. The optional type forces you to handle both cases. It's a compile-time safety net—you catch nil errors before they reach production.
Interviewers want to hear you explain: optionals eliminate the billion-dollar null pointer exception problem. They make nil handling explicit and type-safe. If you see String? in a function signature, you immediately know it might be nil. If you see String, it's guaranteed to have a value.
How do you avoid retain cycles: weak and unowned
A retain cycle happens when two objects hold strong references to each other, preventing either from being deallocated. In Swift, this usually occurs with closures and delegates.
// RETAIN CYCLE: class holds a closure that captures self
class APIClient {
var onComplete: (() -> Void)?
func fetchData() {
URLSession.shared.dataTask(with: URL(string: "https://api.example.com/data")!) { data, _, _ in
self.processData(data) // self is retained by the closure
}.resume()
// If APIClient is deallocated before the request finishes,
// the closure still holds self, creating a cycle
}
}
// FIX 1: Use weak self for optional references
class APIClient {
var onComplete: (() -> Void)?
func fetchData() {
URLSession.shared.dataTask(with: URL(string: "https://api.example.com/data")!) { [weak self] data, _, _ in
guard let self = self else { return }
self.processData(data) // self can be deallocated
}.resume()
}
}
// FIX 2: Use unowned when self is guaranteed to live longer
class ViewController: UIViewController {
let button = UIButton()
func setupButton() {
button.addAction(UIAction { [unowned self] _ in
self.handleTap() // self outlives the button
}, for: .touchUpInside)
}
}weak is safer: it becomes nil if the object is deallocated, so you must check it. unowned is more performant but assumes the object outlives the closure; if it doesn't, you'll crash. In a subscription-based SaaS platform with complex view hierarchies, view controllers often own buttons that own closures—here, unowned is correct because the button outlives the closure.
The real-world scenario: a networking layer managing long-lived requests. If a request's completion handler captures self (the API client) strongly, and the app deallocates the client before the request finishes, you've leaked memory. With [weak self], the handler releases self as soon as the client goes away.
What are actors: serializing state safely
Actors are a concurrency primitive introduced in Swift 5.5. They're reference types (like classes) that serialize access to their mutable state. All methods and properties on an actor are protected by an implicit lock—only one thread can access the actor's state at a time.
// Without actors: risk of race conditions
class UserCache {
private var users: [Int: User] = [:]
func cacheUser(_ user: User) {
// Multiple threads could modify users simultaneously
users[user.id] = user
}
func getUser(id: Int) -> User? {
return users[id]
}
}
// WITH ACTORS: thread-safe by default
actor UserCache {
private var users: [Int: User] = [:]
func cacheUser(_ user: User) {
users[user.id] = user // Serialized access
}
func getUser(id: Int) -> User? {
return users[id]
}
}
// Usage with async/await
let cache = UserCache()
await cache.cacheUser(user) // Awaits the actor's lock
let cachedUser = await cache.getUser(id: 123)Actors eliminate data races. If you're building a ride-hailing platform with concurrent ride requests, booking state changes, and payment processing, you need thread safety. An actor wrapping the booking state ensures that two payment handlers can't simultaneously modify the same ride record.
Key insight: actors are isolation boundaries. You access an actor's state through async/await, which enforces that you're aware of the serialization cost. You can't just grab a reference and mutate it on a background thread—the actor forces you to think about concurrency.
Common mistakes
- Using classes for everything: We see teams default to classes because they're familiar from other languages. Swift favors structs. If you're not sharing state or using inheritance, use a struct.
- Force-unwrapping optionals:
user!feels fast in a prototype, but in production, it's a crash waiting to happen. Use guard, if-let, or nil coalescing instead. - Forgetting [weak self] in view controller callbacks: A common leak: a view controller owns a button, the button owns a closure with a strong capture of the view controller. The view controller is dismissed but never deallocated because the closure won't release it.
- Treating actors like regular classes: Forgetting to await actor methods or trying to mutate an actor across threads. Actors are isolation boundaries; respect that.
- Mixing weak and unowned carelessly: Unowned is faster (no nil check), but if the captured object is deallocated first, you crash. Use weak unless you're certain of the lifetime relationship.
Key takeaways
- Structs are value types (copy semantics); classes are reference types (shared mutable state). Default to structs unless you need identity or shared state.
- Optionals make nil explicit in the type system, catching null pointer errors at compile time instead of runtime.
- Avoid retain cycles by using
[weak self]in closures that capture the owning object, or[unowned self]when the owner outlives the closure. - Actors serialize mutable state for thread safety, eliminating data races in concurrent code. Access actor state through async/await.
- These aren't theoretical—they're the foundation of safe, scalable iOS apps. Know them well enough to explain trade-offs, not just definitions.






Comments
Loading comments…