Top iOS Interview Questions

author image Hirely
at 07 Jan, 2025

Question: What is the difference between strong, weak, and unowned references in Swift?

Answer:

In Swift, memory management is handled using Automatic Reference Counting (ARC), which tracks and manages the memory of objects. The way objects are referenced in memory is critical to avoid memory leaks and crashes. Swift provides three types of references: strong, weak, and unowned. These references define how objects are held in memory and how they are deallocated.

Here’s the key difference between them:


1. Strong References

  • Description: A strong reference means that the object being referenced will not be deallocated as long as there is a strong reference pointing to it. By default, all references in Swift are strong.

  • Memory Management: The object is retained (kept in memory) as long as there is at least one strong reference to it.

  • Use case: Used when you want to ensure that the object stays in memory as long as it’s needed.

  • Example:

    class MyClass {
        var name: String
        init(name: String) {
            self.name = name
        }
    }
    
    var object1 = MyClass(name: "Object 1") // Strong reference

    In the above example, object1 is a strong reference to an instance of MyClass. As long as object1 exists, the MyClass object won’t be deallocated.


2. Weak References

  • Description: A weak reference does not prevent the object from being deallocated. If there are no strong references to the object, it will be deallocated. Weak references are typically used to avoid retain cycles, especially in cases where one object holds a reference to another, but you don’t want that reference to keep the other object alive.

  • Memory Management: The object is not retained. If there are no strong references to the object, it will be deallocated, and the weak reference will automatically become nil.

  • Use case: Used for delegates or objects that should not prolong the lifetime of another object, preventing retain cycles.

  • Note: Weak references can only be applied to optional types because they may become nil if the referenced object is deallocated.

  • Example:

    class MyClass {
        var name: String
        init(name: String) {
            self.name = name
        }
    }
    
    class MyDelegate {
        weak var myObject: MyClass? // Weak reference to avoid retain cycle
        
        init(myObject: MyClass) {
            self.myObject = myObject
        }
    }
    
    var object1 = MyClass(name: "Object 1")
    var delegate = MyDelegate(myObject: object1)

    In this example, myObject is a weak reference to the object1 instance. If object1 is deallocated, myObject will automatically be set to nil.


3. Unowned References

  • Description: An unowned reference is similar to a weak reference in that it does not prevent the referenced object from being deallocated. However, unlike a weak reference, an unowned reference is expected to always refer to an object. If the object being referred to is deallocated and the unowned reference is accessed, it will lead to a runtime crash (this is called a dangling pointer).

  • Memory Management: The object is not retained, but the reference is expected to never be nil during its lifetime. If the object is deallocated, and you try to access the unowned reference, it will trigger a runtime error.

  • Use case: Used in situations where one object has a reference to another object, but it is guaranteed that the referenced object will not be deallocated as long as the referencing object is still alive (like in closures or parent-child relationships).

  • Example:

    class MyClass {
        var name: String
        init(name: String) {
            self.name = name
        }
    }
    
    class MyDelegate {
        unowned var myObject: MyClass // Unowned reference to avoid retain cycle
        
        init(myObject: MyClass) {
            self.myObject = myObject
        }
    }
    
    var object1 = MyClass(name: "Object 1")
    var delegate = MyDelegate(myObject: object1)

    In this example, myObject is an unowned reference to the object1 instance. If object1 is deallocated, trying to access myObject would result in a runtime error because unowned references cannot be nil.


Summary of Differences:

Reference TypeRetentionDeallocationUse CaseOptional?
StrongRetains the objectObject stays in memory until all strong references are removedUse when you want to ensure the object stays in memory while it is needed.No
WeakDoes not retain the objectObject is deallocated if there are no strong references, reference becomes nilPrevent retain cycles, used in delegates and cases where object can be deallocated.Yes
UnownedDoes not retain the objectObject is deallocated, reference is not automatically nil and causes a runtime error if accessedUsed in situations where the referenced object is expected to outlive the reference.No

Key Takeaways:

  • Strong references are the default and keep objects alive as long as they exist.
  • Weak references prevent retain cycles and automatically set to nil when the referenced object is deallocated.
  • Unowned references are similar to weak, but are non-optional and will cause a crash if accessed after the object they reference has been deallocated.

Choose the appropriate reference type based on the lifecycle of your objects and how they should interact with each other in your Swift applications.

Question: What is the @escaping keyword in Swift?

Answer:

In Swift, the @escaping keyword is used to indicate that a closure passed as a function argument will escape the function’s scope. This means that the closure will be stored and executed after the function that takes it as a parameter has returned. Closures that escape are typically stored in properties or passed to asynchronous functions, where they might be executed at a later time, outside of the original scope.

In other words, an escaping closure is one that is not executed immediately during the function call but will instead be executed later, possibly after the function has finished executing.

Why is @escaping needed?

The @escaping keyword is required because Swift enforces strict memory management rules. By default, closures passed as function arguments are non-escaping, which means they are expected to execute before the function returns, and they don’t outlive the function call. If a closure is captured and executed outside the function call, it is considered an “escaping closure,” and Swift needs to know this in advance.

Without the @escaping keyword, Swift would assume the closure cannot escape the function’s scope, and you would get a compile-time error if you tried to store or execute it outside the function.

Common use cases for @escaping:

  1. Asynchronous operations: In many asynchronous tasks (e.g., network requests or delayed operations), a closure is passed to a function and executed after the function completes, often after some time. These closures are escaping closures.
  2. Stored closures: If a closure is stored in a property or a collection, and executed later, it must be marked as escaping.

Example:

import Foundation

// A function that takes a closure as an argument
func performAsyncTask(completion: @escaping () -> Void) {
    DispatchQueue.global().async {
        // Simulating an async task
        sleep(2)
        DispatchQueue.main.async {
            completion()  // The closure is called after the function returns
        }
    }
}

// Calling the function
performAsyncTask {
    print("Async task completed!")
}

// Output will be printed after 2 seconds because the closure is escaping.

Key Points:

  • Escaping closure: The closure is stored and executed later, outside the function’s scope.
  • Non-escaping closure: The closure is expected to execute before the function returns, within the function’s scope.
  • @escaping keyword: Marks the closure as escaping, allowing it to be stored and executed later, such as in asynchronous operations or stored properties.

Memory Management:

  • When you use @escaping closures, you need to be aware of memory management because escaping closures may introduce retain cycles. Retain cycles can occur if the closure strongly captures self (typically self refers to a view controller or other class instances), which prevents both the closure and the object it captures from being deallocated.

Example of potential retain cycle:

class MyViewController {
    var name = "MyViewController"
    
    func fetchData(completion: @escaping () -> Void) {
        // Using @escaping, the closure will escape the function's scope.
        DispatchQueue.global().async {
            // Some async task
            completion()
        }
    }
    
    func loadData() {
        fetchData {
            // Capture 'self' strongly inside the closure, creating a retain cycle
            print(self.name)  // This could cause a memory leak
        }
    }
}

In this example, self is captured strongly inside the closure, and if fetchData is called repeatedly, it could prevent MyViewController from being deallocated, leading to a memory leak.

To fix this, you can use [weak self] or [unowned self] to break the retain cycle:

fetchData { [weak self] in
    guard let self = self else { return }
    print(self.name)
}

Summary:

  • The @escaping keyword in Swift marks a closure as one that will be called after the function returns.
  • It is commonly used in asynchronous operations and when closures are stored for later execution.
  • It is important to manage memory carefully when using escaping closures to avoid retain cycles.

Read More

If you can’t get enough from this article, Aihirely has plenty more related information, such as iOS interview questions, iOS interview experiences, and details about various iOS job positions. Click here to check it out.

Related Posts

Trace Job opportunities

Hirely, your exclusive interview companion, empowers your competence and facilitates your interviews.

Get Started Now