DEV Community

Harsh Prajapat
Harsh Prajapat

Posted on

iOS Interview Knowledge Prep

What semantics in swift

In Swift, "semantics" refers to how types behave when they are assigned to variables, passed to functions, or modified in memory. Swift relies heavily on three core data-handling behaviors: Value Semantics, Reference Semantics, and Move Semantics.

1. Value Semantics (The Default)

Value semantics mean that data is copied when it is assigned to a new variable or passed to a function. Each variable holds its own independent instance of the data, ensuring that changes to one variable never accidentally impact another.

Types: Structs (struct), enumerations (enum), and tuples (tuple).

Standard Library: Most core Swift types—like Int, String, Array, and Dictionary—are structs and exhibit value semantics.

Benefits: Provides excellent thread safety and highly predictable code behavior

💡 Optimization: Copy-on-Write (COW)

### 2. Reference Semantics

Reference semantics mean that variables do not hold the actual data directly; instead, they hold a pointer or reference to the shared instance stored elsewhere in memory (on the heap).

*** Types:** Classes (class), actors (actor), and closures.

*** Behavior:** If you assign a class instance to a new variable, both variables point to the exact same object. Mutating one will affect the other.

*** Benefits:** Supports inheritance, object identity, and sharing live application state.

class vs actor in swift

The primary difference is that actors provide built-in compile-time thread safety by serializing access to their mutable state, while classes do not protect against data races when accessed concurrently across multiple threads. Both are reference types allocated on the heap, but actors achieve this protection through a feature called actor isolation.

Key Differences Explained

1. Concurrency and Thread Safety

Classes: Multiple threads can modify a class instance at the same time. This causes data races, memory corruption, or crashes if you do not use manual synchronization like DispatchQueue or locks

Actors: Swift guarantees that only one task can execute inside an actor at any given time. It automatically serializes incoming calls to prevent simultaneous mutations.

2. Accessing State (await)

Classes: You can read and write to class properties instantly from anywhere in your code.

Actors: If you are outside the actor, you must use the await keyword to access its properties or methods. This suspends your code if the actor is busy executing another task.

3. Inheritance

Classes: Can inherit properties, methods, and initializers from a superclass.

Actors: Do not support subclassing or inheritance. They can only conform to protocols to share behavior.

Code Example: Non-Thread-Safe Class

class ClassCounter {
    var value = 0
    func increment() { value += 1 }
}

let classCounter = ClassCounter()
// Calling this from multiple threads simultaneously causes a data race
classCounter.increment() 
Enter fullscreen mode Exit fullscreen mode

Code Example: Thread-Safe Actor

class ClassCounter {
    var value = 0
    func increment() { value += 1 }
}

let classCounter = ClassCounter()
// Calling this from multiple threads simultaneously causes a data race
classCounter.increment() 
Enter fullscreen mode Exit fullscreen mode

GCD

In Swift, the term GCD can refer to two entirely different concepts: Grand Central Dispatch (for multi-threading) or the Greatest Common Divisor (the mathematical algorithm).

1. Grand Central Dispatch (Multi-Threading)
**
**Grand Central Dispatch (GCD)
is Apple’s low-level framework used to handle asynchronous tasks and concurrent code execution using DispatchQueue.

Main Queue vs. Background Queue

The most common use case is sending a heavy task to a background thread to prevent the UI from freezing, then updating the user interface on the main thread.

import Foundation

// 1. Move heavy work to a background queue
DispatchQueue.global(qos: .userInitiated).async {
    // Example: Fetch data from a network or process an image
    let processedData = "Some heavy data results"

    // 2. Bounce back to the main queue to update the UI
    DispatchQueue.main.async {
        print("Update UI with: \(processedData)")
    }
}
Enter fullscreen mode Exit fullscreen mode

Creating Custom Queues

You can create your own queues depending on how you want your tasks to execute:

  • Serial Queue: Executes tasks one at a time, in order.

  • Concurrent Queue: Executes multiple tasks at the same time.

// Serial Queue (default)
let serialQueue = DispatchQueue(label: "com.app.serialQueue")
serialQueue.async { print("Task 1") }
serialQueue.async { print("Task 2") } // Waits for Task 1 to finish

// Concurrent Queue
let concurrentQueue = DispatchQueue(label: "com.app.concurrentQueue", attributes: .concurrent)
concurrentQueue.async { print("Task A") }
concurrentQueue.async { print("Task B") } // Runs at the same time as Task A
Enter fullscreen mode Exit fullscreen mode

Serial queue vs actor in swift

In Swift/iOS, Serial DispatchQueue and Actor both help protect shared mutable data, but they solve the problem at different levels.

Does serial Queue is thread safe

Yes, a serial queue is thread-safe for the tasks it executes because it guarantees that only one task runs at a time, preventing race conditions.

An actor is Swift's modern way of protecting mutable state from concurrent access.

1. Serial Queue

A serial queue executes one task at a time.

final class Counter {
    private var value = 0

    private let queue = DispatchQueue(label: "counter.queue")

    func increment() {
        queue.async {
            self.value += 1
        }
    }

    func getValue(completion: @escaping (Int) -> Void) {
        queue.async {
            completion(self.value)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Because the queue is serial:

Task 1 → finish
Task 2 → finish
Task 3 → finish
Enter fullscreen mode Exit fullscreen mode

Only one task accesses value at a time.

When to use Serial Queue

Use it when:

  • Working with existing GCD-based code
  • You need precise queue control
  • You have legacy UIKit/iOS code
  • You need sync, async, .barrier, QoS, etc.
  • You are protecting a small resource

2. Actor

An actor is Swift's modern way of protecting mutable state from concurrent access.

actor Counter {
    private var value = 0

    func increment() {
        value += 1
    }

    func getValue() -> Int {
        return value
    }
}

Usage:

let counter = Counter()

Task {
    await counter.increment()

    let value = await counter.getValue()

    print(value)
}

Notice:

await counter.increment()

Enter fullscreen mode Exit fullscreen mode

3. Main difference with example

Imagine you have a shared cache.

Serial Queue

final class ImageCache {
    private var cache: [String: UIImage] = [:]

    private let queue = DispatchQueue(
        label: "image.cache.queue"
    )

    func set(_ image: UIImage, for key: String) {
        queue.async {
            self.cache[key] = image
        }
    }

    func get(
        _ key: String,
        completion: @escaping (UIImage?) -> Void
    ) {
        queue.async {
            completion(self.cache[key])
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

You have to manually decide:

"Every access to cache must happen through queue."

Actor

actor ImageCache {
    private var cache: [String: UIImage] = [:]

    func set(_ image: UIImage, for key: String) {
        cache[key] = image
    }

    func get(_ key: String) -> UIImage? {
        cache[key]
    }
}

Usage:

let cache = ImageCache()

Task {
    await cache.set(image, for: "profile")

    let image = await cache.get("profile")
}
Enter fullscreen mode Exit fullscreen mode

Swift understands that cache is actor-isolated.

Top comments (0)