Beginner Level (0–1 Years)

1. What is the difference between `let` and `var` in Swift, and can a `let` constant point to a mutable object?

Answer:

let declares a constant, meaning the variable cannot be reassigned after initialization, while var declares a variable that can be reassigned. For reference types (like class instances), a let constant prevents changing the reference itself, but mutable properties of the object can still be modified. For value types (like structs), a let constant makes the entire instance immutable, including its properties.

class Dog {
    var name = "Fido"
}
let dog = Dog()
dog.name = "Rex" // Allowed: mutates property
// dog = Dog() // Error: Cannot assign to 'dog'

struct Point {
    var x: Int
}
let point = Point(x: 10)
// point.x = 20 // Error: Cannot modify 'point'

2. What does it mean that Swift is a “type-safe” language?

Answer:

Swift is type-safe, meaning it performs type checks at compile time. You can’t assign a value of one type to a variable of another type unless you explicitly convert it. This reduces runtime errors and makes code safer.

3. How does optional binding differ from optional chaining?

Answer:

Optional binding uses if let or guard let to safely unwrap an optional, providing a non-optional value within a scope. Optional chaining accesses properties or methods of an optional using ?, returning nil if any part of the chain is nil, avoiding the need for explicit unwrapping and safely handling nested properties.

let name: String? = "John"

// Optional binding
if let unwrappedName = name {
    print(unwrappedName) // Prints: John
}

// Optional chaining
let count = name?.count // Returns 4 (optional Int)

4. What’s the difference between `==` and `===` in Swift?

Answer:

== checks for value equality. === checks if two references point to the same object instance (only for class types).

5. When would using a `struct` be more appropriate than a `class` in Swift?

Answer:

Use a struct when you want value semantics, meaning each copy is independent. This is ideal for lightweight models like coordinates or settings. Use class when reference semantics or inheritance is needed.

6. What will happen if you access an optional without unwrapping it?

Answer:

Accessing an optional variable itself (e.g., printing it) is allowed and shows its optional wrapper (e.g., Optional("value")). However, using an optional in a context requiring a non-optional value (like arithmetic or string concatenation) causes a compile-time error unless unwrapped. Force unwrapping with ! when the value is nil causes a runtime crash.

7. Can a Swift `enum` have stored properties?

Answer:

No, Swift enums cannot have stored properties. They can have computed properties and methods, and associated values can store data specific to each case.

enum Color {
    case rgb(red: Int, green: Int, blue: Int)
    var description: String { // Computed property
        switch self {
        case .rgb(let r, let g, let b):
            return "RGB(\(r), \(g), \(b))"
        }
    }
}

8. What does `defer` do in Swift?

Answer:

defer executes code just before exiting the current scope, useful for cleanup tasks like closing files or releasing resources, ensuring they run regardless of how the scope is exited.

func processFile() {
    print("Opening file")
    defer { print("Closing file") }
    print("Processing file")
}

9. What is the output when you create a copy of an array, append an element to the copy, and print the original array?

Answer:

Consider this code:

var a = [1, 2, 3]
var b = a
b.append(4)
print(a)

Output: [1, 2, 3]. Arrays are structs in Swift (value types), so b is a copy of a. Changing b does not affect a.

10. Is Swift pass-by-value or pass-by-reference?

Answer:

Swift uses pass-by-value for value types (struct, enum) and pass-by-reference for reference types (class). When passing class instances, the reference is passed by value, so the pointer is copied, but it still points to the same object.

11. What’s a retain cycle and how can you prevent it?

Answer:

A retain cycle occurs when two objects hold strong references to each other, preventing deallocation. Use weak or unowned references to break the cycle.

12. What’s the difference between `weak` and `unowned` references?

Answer:

weak references are optional and become nil when the referenced object is deallocated. unowned references are non-optional and assume the object will always be alive while in use, causing a crash if accessed after deallocation.

13. What’s the default access level of Swift properties?

Answer:

By default, Swift properties and methods are internal, meaning they are accessible within the same module.

14. Will converting a string to an integer using `Int(str)` compile successfully?

Answer:

Consider this code:

let str = "123"
let num = Int(str)

Yes. num will be of type Int? because Int(str) returns an optional, since the conversion might fail.

15. What happens if you call a method on an implicitly unwrapped optional that is nil?

Answer:

It will crash with a runtime error. Implicitly unwrapped optionals (T!) are not safe unless you are certain the value is never nil.

16. What is wrong with using a C-style for loop in Swift?

Answer:

Consider this code:

for var i = 0; i < 5; i++ {
  print(i)
}

Swift does not support C-style for loops since Swift 3, as they were removed to favor safer, more expressive range-based loops. Use:

for i in 0..<5 {
    print(i)
}

17. What happens when you try to add an integer and a double in Swift?

Answer:

Consider this code:

let a = 10
let b = 0.1
let c = a + b

This won’t compile. a is an Int, and b is a Double. Swift requires explicit type conversion. Fix it with: let c = Double(a) + b or let c = a + Int(b) (though the latter truncates b).

18. What’s the difference between `map`, `filter`, and `reduce`?

Answer:

map transforms each element, filter selects elements that match a condition, and reduce combines all elements into a single value.

19. What is a computed property in Swift?

Answer:

A computed property does not store a value. Instead, it calculates one each time it’s accessed using a get block, and optionally a set block.

20. What is the significance of using `guard` statements?

Answer:

guard is used for early exit if conditions aren’t met. It improves readability by handling failure cases up front.

21. How does Swift handle memory management?

Answer:

Swift uses Automatic Reference Counting (ARC) to manage memory. It tracks how many strong references point to an object, deallocating it when the count reaches zero.

22. What is the output when you modify an array during a for-in loop with a break condition?

Answer:

Consider this code:

var array = [1, 2, 3]
for item in array {
  array.append(item)
  if array.count > 6 { break }
}
print(array)

Output: [1, 2, 3, 1, 2, 3]. Swift’s for-in loop iterates over a snapshot of the array’s elements, not the live array, so modifications don’t affect the iteration. The loop completes one full pass before the break condition is met.

23. What is a closure in Swift, and how is it used?

Answer:

A closure is a self-contained block of code that can be passed around and used in Swift. It can capture and store references to variables and constants from its surrounding context. Closures are commonly used in iOS for tasks like handling asynchronous operations, such as network requests or animations.

let greet = { (name: String) -> String in
    return "Hello, \(name)!"
}
print(greet("Alice")) // Prints: Hello, Alice!

// Example with a completion handler
func performTask(completion: (Bool) -> Void) {
    // Simulate task
    completion(true)
}
performTask { success in
    print("Task completed: \(success)") // Prints: Task completed: true
}

24. What is a view controller in iOS, and what is its role?

Answer:

A view controller in iOS, typically a subclass of UIViewController, manages a view and its subviews, handling user interactions and coordinating the presentation of data. It acts as the intermediary between the app’s data (model) and the user interface (view) in the Model-View-Controller (MVC) pattern.

import UIKit
class MyViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        // Set up UI elements
    }
}

25. What is Auto Layout, and why is it important in iOS development?

Answer:

Auto Layout is a constraint-based system in iOS that defines the position and size of UI elements relative to each other or their parent view. It ensures user interfaces adapt to different screen sizes, orientations, and devices. It’s important for creating responsive and dynamic layouts without hardcoding positions.

import UIKit
class MyViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let button = UIButton()
        button.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(button)
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
}

👋 Need top iOS Swift developers for your project? Interview this week!

Fill out the form to book a call with our team. We’ll match you to the top developers who meet your requirements, and you’ll be interviewing this week!

Request for Service


Intermediate Level (1–3 Years)

1. How does value type behavior impact performance in Swift collections?

Answer:

Value types like structs are copied when assigned or passed, which could impact performance for large collections. However, Swift optimizes this with copy-on-write for collections like Array and Dictionary, where copying only occurs if the copy is mutated, reducing memory overhead.

var array1 = [1, 2, 3]
var array2 = array1 // No copy yet
array2.append(4) // Copy occurs here
print(array1) // [1, 2, 3]

2. Why might using a class over a struct introduce unintended side effects?

Answer:

Classes are reference types, so assigning one class instance to another creates a shared reference to the same instance. Modifying one reference affects all others, leading to unintended mutations. Structs, being value types, create independent copies, avoiding this issue.

3. What’s the role of escaping closures in Swift, and when are they necessary?

Answer:

Escaping closures outlive the function they’re passed into, typically used in asynchronous operations like network requests. They require the @escaping keyword to indicate they may be stored or executed later. Non-escaping closures, the default, are executed within the function’s scope.

// Escaping closure
func fetchData(completion: @escaping () -> Void) {
    DispatchQueue.global().async {
        // Do some work
        completion()
    }
}

// Non-escaping closure
func processData(completion: () -> Void) {
    completion() // Executes immediately
}

4. What is the significance of the `@MainActor` attribute in Swift concurrency?

Answer:

@MainActor ensures that code runs on the main thread, which is critical for updating UI elements safely in a multithreaded environment, preventing data races and ensuring thread-safe UI updates.

5. How do actor types help in preventing data races in Swift?

Answer:

Actors serialize access to their mutable state, ensuring only one task accesses the state at a time. This makes them thread-safe by default, eliminating data races without requiring manual synchronization like locks.

6. What issues can arise from force unwrapping optionals?

Answer:

Force unwrapping an optional that is nil causes a runtime crash. Safer alternatives include optional binding (if let, guard let) or the nil-coalescing operator (??) to provide default values.

7. Why might using a strong reference in a closure cause a memory leak?

Answer:

If a closure captures self strongly and is retained by self, a retain cycle occurs, preventing deallocation. Using [weak self] or [unowned self] breaks the cycle.

class ViewController {
    var closure: (() -> Void)?
    func setup() {
        closure = { // Retain cycle if self is captured strongly
            print(self.description)
        }
        // Fix with weak self
        closure = { [weak self] in
            print(self?.description ?? "No self")
        }
    }
}

8. What is the key difference between `weak` and `unowned` when avoiding retain cycles?

Answer:

weak creates an optional reference that becomes nil when the referenced object is deallocated. unowned is non-optional and assumes the object exists, causing a crash if accessed after deallocation.

9. How can protocol-oriented programming be more beneficial than class inheritance?

Answer:

Protocols promote composition over inheritance, allowing types to conform to multiple protocols for flexible, reusable code. This avoids tight coupling and enables value types like structs to participate, unlike class-only inheritance.

10. What is type erasure, and why is it needed in Swift?

Answer:

Type erasure hides concrete generic types behind a protocol, enabling heterogeneous collections or abstractions. It’s needed when protocols with associated types (e.g., in generics) cannot be used directly as types, as in SwiftUI’s AnyView.

11. Why can you not use a stored property in a Swift protocol?

Answer:

Protocols define behavior, not storage. Stored properties require memory allocation, which protocols cannot manage. Use computed properties or require conforming types to implement storage.

12. Why is using a `Result` type preferable to a completion handler with separate success and failure parameters?

Answer:

Result encapsulates success and failure cases in a single return value, improving readability and enforcing handling of both cases via pattern matching. For example:

enum NetworkError: Error {
    case notConnected, timeout
}
func load(completion: (Result<Data, NetworkError>) -> Void) {
    // Example usage
    completion(.success(Data()))
}

13. How can protocol extensions provide default behavior?

Answer:

Protocol extensions allow methods and computed properties to be implemented once, providing default behavior for all conforming types, reducing boilerplate code.

14. Why is `DispatchQueue.main.async` commonly used in iOS development?

Answer:

It schedules tasks to run on the main thread, ensuring thread-safe UI updates and maintaining app responsiveness, as UI operations must occur on the main thread.

15. What happens if a class does not conform to `Codable` when trying to encode it?

Answer:

A compile-time error occurs. All stored properties must also conform to Codable, or you must implement custom encoding/decoding logic using encode(to:) and init(from:).

16. How does dependency injection improve testability in Swift?

Answer:

Dependency injection decouples components by passing dependencies externally, allowing mocks or fakes to be used in unit tests, improving test isolation and flexibility.

17. What is a `deinit` method, and when is it called?

Answer:

deinit is a deinitializer called when a class instance is deallocated, used for cleanup tasks like releasing resources or notifying observers.

18. How can you implement custom decoding behavior in Swift?

Answer:

Conform to Decodable and implement init(from decoder: Decoder) to manually parse nested or mismatched JSON data, allowing custom logic for complex structures.

19. Why might an enum with associated values be better than a struct in some models?

Answer:

Enums with associated values enable type-safe modeling of distinct cases, ideal for state machines or network responses, where structs might require more boilerplate to achieve similar safety.

20. What is a KeyPath in Swift, and what are its common use cases?

Answer:

KeyPaths reference a property dynamically, allowing type-safe access without direct invocation. They’re used in SwiftUI for bindings, in KVO, or for functional programming. For example:

struct Person {
    var name: String
}
let keyPath = \Person.name
let person = Person(name: "Alice")
print(person[keyPath]) // Prints: Alice

21. What does the `mutating` keyword do in a Swift struct?

Answer:

mutating allows a method to modify a struct’s properties, as structs are value types and immutable by default in methods.

22. How can you prevent subclassing of a Swift class?

Answer:

Mark the class as final to prohibit inheritance, which also enables compiler optimizations for performance.

23. Why would you use a lazy property in Swift?

Answer:

Lazy properties are initialized only when first accessed, useful for expensive computations or when initialization depends on self being fully initialized.

24. What is a tuple, and when would you use it?

Answer:

A tuple groups multiple values into a single compound value, useful for returning multiple values from a function without defining a custom type.

25. What’s the purpose of the `@discardableResult` attribute?

Answer:

@discardableResult allows a function’s return value to be ignored without a compiler warning, useful when the result is optional.

26. How do you implement a singleton pattern in Swift?

Answer:

Use a static constant to create a single shared instance, with a private initializer to prevent additional instances. Ensure thread-safety for concurrent access. Example:

class Logger {
    static let shared = Logger()
    private init() { }
}

27. What is the difference between `flatMap` and `compactMap`?

Answer:

compactMap transforms elements and removes nil results, ideal for optional transformations. flatMap flattens nested collections, but for optionals, it’s less precise than compactMap.

28. How do `Set`, `Array`, and `Dictionary` differ in Swift?

Answer:

Array is ordered and allows duplicates. Set is unordered with unique elements. Dictionary maps unique keys to values, unordered.

29. Why might using force try (`try!`) be dangerous?

Answer:

try! causes a runtime crash if the function throws an error. Safer alternatives include do-catch blocks or try? to handle errors gracefully.

30. What is the `Never` type used for in Swift?

Answer:

Never indicates a function never returns, either due to a crash (e.g., fatalError) or an infinite loop. It’s used to mark unreachable code or error states.

31. How does the `@available` attribute help with platform version control?

Answer:

@available marks APIs as available only on specific OS versions, preventing runtime crashes on older systems by enforcing compile-time checks.

32. What’s the difference between `throw`, `throws`, and `rethrows`?

Answer:

throw raises an error. throws marks a function that can throw errors. rethrows indicates a function only throws if its closure parameter throws.

33. How do generics improve code reusability in Swift?

Answer:

Generics allow functions and types to work with any data type while maintaining type safety, reducing code duplication and enabling reusable, flexible APIs.

34. Why can’t stored properties be used in protocol extensions?

Answer:

Protocol extensions apply to all conforming types, but protocols cannot manage storage. Only computed properties or methods can be implemented in extensions.

35. How does SwiftUI differ from UIKit in state management?

Answer:

SwiftUI uses declarative state management with property wrappers like @State, @Binding, and @ObservedObject, automatically updating the UI when state changes. UIKit uses an imperative approach, manually updating views. Example:

import SwiftUI
struct ContentView: View {
    @State private var count = 0
    var body: some View {
        Button("Increment: \(count)") { count += 1 }
    }
}

36. How can you handle unknown cases in a Swift enum switch?

Answer:

Use a default case or @unknown default to handle future enum cases not known at compile time, ensuring forward compatibility.

37. Why is `Codable` a type alias in Swift?

Answer:

Codable is a type alias for Encodable & Decodable, requiring a type to implement both protocols for JSON serialization and deserialization.

38. What is the advantage of using `defer` in error handling?

Answer:

defer ensures cleanup code runs regardless of whether an error is thrown, simplifying resource management in do-catch blocks.

39. How can enums conform to protocols?

Answer:

Enums conform to protocols by implementing required methods or properties, enabling polymorphic behavior similar to classes or structs.

40. What does the `inout` keyword do in function parameters?

Answer:

inout allows a function to modify a parameter’s value directly, with changes reflected outside the function scope.

41. How does Swift resolve function overloading with default parameters?

Answer:

Swift generates multiple function signatures for default parameters, but overloading with defaults can cause ambiguity, requiring careful design to avoid conflicts.

42. What is function currying, and how is it applied in Swift?

Answer:

Currying transforms a function with multiple arguments into a series of functions, each taking one argument. Swift supports this via nested functions for functional programming patterns.

43. How can the `@objc` attribute be useful in Swift code?

Answer:

@objc exposes Swift methods and properties to the Objective-C runtime, enabling use with #selector, Key-Value Observing (KVO), or interoperability with legacy Objective-C APIs.

44. What is the effect of using `final` on a method?

Answer:

Marking a method final prevents it from being overridden in subclasses, improving safety and enabling compiler optimizations.

45. How does Swift’s ARC differ from garbage collection?

Answer:

ARC (Automatic Reference Counting) is deterministic, deallocating objects when their reference count reaches zero. Garbage collection is periodic and non-deterministic, potentially delaying deallocation.

46. How do you make a struct conform to `Equatable`?

Answer:

Swift automatically synthesizes Equatable conformance if all properties conform to it. Otherwise, implement the == function manually.

47. What are property wrappers, and how do they work?

Answer:

Property wrappers encapsulate custom logic around property access, simplifying code reuse. They’re used in SwiftUI (e.g., @State) or for custom behaviors. Example:

@propertyWrapper
struct Clamped {
    private var value: Int
    private let range: ClosedRange
    init(wrappedValue: Int, range: ClosedRange) {
        self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
        self.range = range
    }
    var wrappedValue: Int {
        get { value }
        set { value = min(max(newValue, range.lowerBound), range.upperBound) }
    }
}

48. Why are enums considered powerful in Swift?

Answer:

Enums support associated values, methods, protocol conformance, and exhaustive pattern matching in switch statements, making them versatile for modeling state and logic.

49. How can you restrict a generic function to work only with class types?

Answer:

Use the AnyObject constraint to limit a generic function to class types, ensuring reference semantics. Example:

func update(_ object: T) {
    // Works only with class instances
}

50. What is the difference between `@StateObject` and `@ObservedObject` in SwiftUI?

Answer:

@StateObject creates and owns a view model, managing its lifecycle. @ObservedObject observes an external model passed from elsewhere, without ownership.


LATAM-Image-V2.png

Hire Top LATAM Developers: Guide

We’ve prepared this guide that covers  benefits, costs, recruitment, and remote team management to a succesful hiring of developers in LATAM. 

Fill out the form to get our guide.

Gated content


Advanced Level (3+ Years)

1. How would you prevent thread explosion when using `DispatchQueue.concurrentPerform`?

Answer:

DispatchQueue.concurrentPerform creates threads up to the system’s logical core count. To prevent thread explosion, ensure tasks are lightweight and avoid spawning additional work on global queues within iterations. Use coarse-grained tasks to minimize overhead.

DispatchQueue.concurrentPerform(iterations: 100) { index in
    // Lightweight task, e.g., process small data chunk
    let result = compute(index)
    // Avoid spawning new global queue tasks here
}

2. What are some limitations of Swift’s `Codable` protocol in real-world API parsing?

Answer:

Codable assumes a 1:1 mapping between JSON and model, which breaks with inconsistent or dynamic APIs. Custom decoding with CodingKeys or init(from:) handles transformations, such as missing or mismatched keys.

struct User: Decodable {
    let id: Int
    let name: String
    enum CodingKeys: String, CodingKey {
        case id
        case name = "full_name" // Handle key mismatch
    }
}

3. Explain how Swift’s existential types can lead to performance issues.

Answer:

Existential types like any Hashable use type erasure, requiring dynamic dispatch and boxing, which disables optimizations like inlining. This can slow down tight loops or large collections due to increased runtime overhead.

4. What is the difference between `Sendable` and `@unchecked Sendable`?

Answer:

Sendable ensures types are safe to pass across concurrency boundaries, with compiler verification. @unchecked Sendable bypasses this check when the developer manually guarantees safety, risking runtime issues if misused.

5. How would you design a thread-safe caching system in Swift?

Answer:

Use an actor to guard a dictionary cache, ensuring thread-safe access to mutable state. This is the modern Swift approach, avoiding manual locks or queues.

actor Cache {
    private var storage: [String: Data] = [:]
    func set(_ value: Data, for key: String) {
        storage[key] = value
    }
    func get(for key: String) -> Data? {
        storage[key]
    }
}

6. What’s the difference between `async let` and `Task {}` in structured concurrency?

Answer:

async let creates child tasks tied to the parent’s lifetime, ensuring structured cancellation. Task {} creates unstructured tasks that can outlive their context, requiring manual management.

7. How does Swift’s memory layout differ between structs and classes?

Answer:

Structs are stack-allocated (unless captured or stored in heap-based structures) with inline data, making them cache-friendly. Classes are heap-allocated with reference counting, incurring overhead for ARC management.

8. How would you implement dependency inversion in a Swift app architecture?

Answer:

Define protocols for services and inject them via constructors or factories. Consumers depend on abstractions, not concrete types, improving testability and decoupling.

9. How would you debug memory leaks in a Swift iOS app?

Answer:

Use Xcode’s Instruments with the Leaks or Allocations tool to identify retained instances. Analyze reference cycles in the memory graph debugger and verify deallocation with breakpoints in deinit.

10. How would you avoid duplicate network requests in a Swift networking layer?

Answer:

Implement request coalescing with a dictionary of in-flight tasks. For duplicate requests, attach new completions to the existing task instead of creating a new one.

11. How can you detect and fix retain cycles involving `Timer` or `NotificationCenter`?

Answer:

Timer and NotificationCenter retain their targets, causing cycles if they reference self. Use [weak self] in closures or invalidate/unregister manually.

class ViewController {
    var timer: Timer?
    func startTimer() {
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
            print(self?.description ?? "No self")
        }
    }
    deinit {
        timer?.invalidate()
    }
}

12. What are associatedtype constraints in Swift protocols, and why are they important?

Answer:

Associated types make protocols generic, with where clauses constraining relationships (e.g., type equality). They enhance flexibility and type safety in protocol-based designs.

13. How can you use Combine to debounce user input in a search field?

Answer:

Use a PassthroughSubject or @Published property with .debounce(for:scheduler:) to delay emissions until typing pauses, reducing unnecessary updates.

import Combine
class SearchViewModel {
    @Published var searchText = ""
    private var cancellables = Set()
    init() {
        $searchText
            .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
            .sink { text in
                print("Search: \(text)")
            }
            .store(in: &cancellables)
    }
}

14. What challenges arise with deep SwiftUI view hierarchies, and how can you mitigate them?

Answer:

Deep view hierarchies impact performance and readability due to excessive redraws. Mitigate by using ViewBuilder for composition, factoring out reusable views, and using conditional rendering to flatten the hierarchy.

15. How would you model a polymorphic JSON API in Swift?

Answer:

Use an enum with associated values and a custom Decodable initializer, decoding based on a discriminator key in the JSON.

enum Media: Decodable {
    case movie(title: String, duration: Int)
    case book(title: String, pages: Int)
    private enum CodingKeys: String, CodingKey {
        case type, title, duration, pages
    }
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let type = try container.decode(String.self, forKey: .type)
        switch type {
        case "movie":
            self = .movie(
                title: try container.decode(String.self, forKey: .title),
                duration: try container.decode(Int.self, forKey: .duration)
            )
        case "book":
            self = .book(
                title: try container.decode(String.self, forKey: .title),
                pages: try container.decode(Int.self, forKey: .pages)
            )
        default:
            throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown type")
        }
    }
}

16. Explain the difference between a strong reference cycle and an unowned reference cycle.

Answer:

A strong reference cycle prevents deallocation due to mutual strong references. An unowned reference cycle causes a crash when accessing a deallocated unowned reference. Use weak to safely break cycles.

17. What’s the best approach to sharing state across SwiftUI views?

Answer:

Use @EnvironmentObject for global state or @StateObject/@ObservedObject for local state. @EnvironmentObject avoids tight coupling for app-wide data.

import SwiftUI
class AppState: ObservableObject {
    @Published var userId: String?
}
struct ContentView: View {
    @EnvironmentObject var appState: AppState
    var body: some View {
        Text(appState.userId ?? "No user")
    }
}

18. How would you unit test code that involves `DispatchQueue` or `Task.sleep`?

Answer:

Inject schedulers or dispatch queues via dependency injection. For Task.sleep, wrap delay logic in an abstraction to mock in tests, ensuring deterministic behavior.

19. How would you ensure consistency between your Core Data models and Swift code?

Answer:

Use Xcode’s code generation for NSManagedObject subclasses or manually create model classes matching the data model. Validate attribute types and relationships to avoid runtime errors.

20. What problems can arise from accessing UIKit code off the main thread?

Answer:

UIKit is not thread-safe, so off-main-thread access can cause race conditions, inconsistent UI, or crashes. Always dispatch UI updates to DispatchQueue.main.

21. How would you design an offline-first architecture for a Swift app?

Answer:

Store data locally using Core Data, SQLite, or Realm. Sync with the backend via queues and background tasks, tracking pending operations for retries and conflict resolution.

22. What are the performance implications of using closures over delegates?

Answer:

Closures capture context, potentially retaining large objects, which can cause memory issues if not managed with [weak self]. Delegates use reference semantics, avoiding capture-related overhead.

23. What is function specialization in Swift, and how does it impact performance?

Answer:

Swift compiles optimized versions of generic functions for specific types, improving runtime performance but increasing binary size. Use @inlinable or @_specialize sparingly to balance size and speed.

24. How would you perform data migration in Core Data?

Answer:

Use lightweight migrations for simple changes via Xcode’s model editor. For complex migrations, implement custom mapping models and migration policies to transform data.

25. What are some pitfalls of using the main thread for heavy computations?

Answer:

Heavy computations block UI rendering, causing lag or unresponsiveness. Move intensive work to background threads or use structured concurrency to keep the main thread free.

26. How can you detect layout performance issues in SwiftUI?

Answer:

Use the “SwiftUI Views” instrument in Xcode, debugPrint, or onAppear to log redraws. Inspect the debug hierarchy for redundant updates or deep view trees.

27. How would you monitor Swift concurrency tasks for memory or performance issues?

Answer:

Use Instruments to track memory allocations and leaks. Log task creation and use profiling to detect over-scheduling, orphan tasks, or task starvation.

28. What is the purpose of `NSPersistentContainer`, and how does it simplify Core Data?

Answer:

NSPersistentContainer encapsulates Core Data stack setup, including the managed object context and persistent store coordinator, reducing boilerplate and simplifying initialization.

29. How can Swift’s new macros reduce boilerplate in large codebases?

Answer:

Macros (Swift 5.9+) enable compile-time code generation, synthesizing repetitive code like logging, error handling, or accessors, reducing boilerplate and improving maintainability.

30. How would you implement multitenant user data separation in a Swift app?

Answer:

Use separate data stores, namespaces, or partitions keyed by user ID. Avoid global singletons and design services to accept a user context for all operations.

31. What is the role of `@inlinable`, and when should it be used?

Answer:

@inlinable allows cross-module inlining, improving performance by reducing function call overhead. Use it for small, performance-critical functions, but avoid for sensitive or large code.

32. What is a race condition, and how would you prevent one in Swift concurrency?

Answer:

Race conditions occur when multiple threads access shared state unsafely. Prevent them using actor, NSLock, or serial queues to ensure exclusive access.

33. How would you optimize app launch performance in Swift?

Answer:

Defer heavy work, preload data asynchronously, minimize storyboard usage, use lazy loading, and profile with Instruments to identify and optimize bottlenecks.

34. What are async sequences, and how do they relate to Combine?

Answer:

AsyncSequence with for await delivers values over time, offering a native concurrency alternative to Combine publishers with simpler syntax and better integration.

35. How does Swift’s ownership model relate to ARC?

Answer:

Swift’s ownership model (e.g., copy, borrow) enforces explicit memory usage patterns. ARC handles reference counting, but ownership rules help prevent cycles and optimize deallocation.

36. What are the key responsibilities of a ViewModel in MVVM architecture?

Answer:

A ViewModel encapsulates UI logic, exposes observable data, coordinates services, and transforms model data for presentation, avoiding direct UIKit/SwiftUI interaction.

37. How would you implement retry with exponential backoff for failed requests?

Answer:

Use a timer or async delay with an increasing delay factor per retry, capping the maximum delay. Add jitter to avoid thundering herd issues.

38. How would you enable high test coverage for asynchronous code?

Answer:

Use XCTestExpectation for legacy async code or async/await with XCTAssert. Mock dependencies and inject predictable behavior for deterministic tests.

39. What are some downsides of using `@Published` directly in models?

Answer:

@Published couples models to Combine, violating separation of concerns. Use ViewModels or abstractions to expose publishers, keeping models framework-agnostic.

40. How would you architect modular features in a large Swift project?

Answer:

Use Swift Packages or frameworks, separate features into modules, define clear protocol-based APIs, and use dependency injection to isolate dependencies.

41. What is `TaskGroup`, and when would you use it?

Answer:

TaskGroup runs multiple child tasks in parallel and waits for completion. Use it for aggregating async operations, like fetching data from multiple endpoints.

func fetchAll() async throws -> [Data] {
    try await withTaskGroup(of: Data.self) { group in
        for endpoint in endpoints {
            group.addTask { try await fetch(endpoint) }
        }
        return try await group.reduce(into: []) { $0.append($1) }
    }
}

42. How would you handle app version-based logic at runtime?

Answer:

Read the app version from Bundle.main.infoDictionary, store the previous version in UserDefaults, and compare to trigger migrations or version-specific logic.

43. How can you mitigate security risks in local data storage?

Answer:

Use Keychain for sensitive data, encrypt files with Data Protection classes, avoid plain text storage of tokens or PII, and implement secure coding practices.

44. What is binary size bloat, and how can you reduce it in Swift apps?

Answer:

Binary size bloat increases app size unnecessarily. Reduce it by stripping debug symbols, pruning unused assets, minimizing dynamic libraries, and optimizing generic usage.

45. How would you refactor a tightly coupled monolithic view controller?

Answer:

Extract logic into ViewModels or Coordinators, decouple services with protocols, and split UI responsibilities into child view controllers or reusable views.

46. What are some gotchas when using Swift’s `@dynamicMemberLookup`?

Answer:

@dynamicMemberLookup sacrifices compile-time safety for flexibility, reducing tooling support and making code harder to debug or refactor. Use sparingly for dynamic APIs.

47. How would you implement feature flags in a Swift app?

Answer:

Use a configuration service or remote config provider, define feature toggles, and inject flag values into view models or services at runtime, optionally caching locally.

48. How can you leverage Swift’s ownership modifiers (`copy`, `consume`, `borrow`) to optimize memory usage?

Answer:

Swift’s ownership modifiers (Swift 5.9+) control memory access explicitly. borrow avoids copies for read-only access, consume transfers ownership to avoid copies, and copy ensures explicit copying, reducing ARC overhead in performance-critical code.

49. What are the challenges of combining Swift concurrency with Combine, and how can you address them?

Answer:

Swift concurrency and Combine have different task lifecycles and cancellation models. Bridge them using withCheckedContinuation for async-to-Combine or publishers for Combine-to-async. Manage cancellation explicitly to avoid leaks.

import Combine
func asyncPublisher() -> AnyPublisher<String, Error> {
    Future { promise in
        Task {
            do {
                let result = try await fetchData()
                promise(.success(result))
            } catch {
                promise(.failure(error))
            }
        }
    }.eraseToAnyPublisher()
}

50. How would you implement a custom memory allocator for performance-critical Swift code?

Answer:

Use UnsafeMutableRawPointer with custom allocation logic, managing memory manually for high-performance scenarios. Ensure proper deallocation to avoid leaks, and use only in low-level, optimized code.

class CustomAllocator {
    private var buffer: UnsafeMutableRawPointer
    private var size: Int
    init(size: Int) {
        self.size = size
        buffer = UnsafeMutableRawPointer.allocate(byteCount: size, alignment: MemoryLayout.alignment)
    }
    deinit {
        buffer.deallocate()
    }
}