iOS Interview Question (Part 6)
- Dependency injection in Swift ios
- GCD VS NSOperation
- Difference b/w delegate and protocol
- Explain Difference between UIWindow and UIView ?
- What are benefits of Guard ?
- Swift Optionals
- Static vs Class functions
- Stored and computed properties
- Can we willSet/didSet with computed property?
Dependency injection in Swift ios
Dependency injection is a software design pattern used to decouple the components of an application by providing their dependencies from external sources. Dependency injection is commonly used to make code more modular, testable, and maintainable.
There are several ways to implement dependency injection in Swift iOS:
- Constructor Injection: In this approach, dependencies are passed into a class through its initializer (constructor). The class declares properties to hold the dependencies and expects them to be provided during instantiation.
Example:
protocol DatabaseService {
func fetchData() -> String
}
class MySQLService: DatabaseService {
func fetchData() -> String {
return "Data from MySQL"
}
}
class DataManager {
private var databaseService: DatabaseService
// Dependency Injection via the initializer
init(databaseService: DatabaseService) {
self.databaseService = databaseService
}
func getData() -> String {
return databaseService.fetchData()
}
}
// Inject the dependency
let mysqlService = MySQLService()
let dataManager = DataManager(databaseService: mysqlService)
// Fetch data
print(dataManager.getData()) // Output: Data from MySQL
In this example, DataManager
depends on DatabaseService
, but the specific service (MySQLService
) is injected into DataManager
, making the code flexible and testable.
2. Property Injection: In this approach, dependencies are injected through properties after the object is created.
Example:
protocol DatabaseService {
func fetchData() -> String
}
class MySQLService: DatabaseService {
func fetchData() -> String {
return "Data from MySQL"
}
}
class DataManager {
var databaseService: DatabaseService? // Property Injection
func getData() -> String {
return databaseService?.fetchData() ?? "No data source available"
}
}
// Injecting the dependency via property
let dataManager = DataManager()
dataManager.databaseService = MySQLService()
// Fetch data
print(dataManager.getData()) // Output: Data from MySQL
In this example, the databaseService
is injected into DataManager
through a property, allowing flexibility in setting the dependency after initialization.
3. Method Injection: In this approach, dependencies are passed as parameters to specific methods that require them.
Example:
protocol DatabaseService {
func fetchData() -> String
}
class MySQLService: DatabaseService {
func fetchData() -> String {
return "Data from MySQL"
}
}
class DataManager {
func getData(from service: DatabaseService) -> String {
return service.fetchData()
}
}
// Inject the dependency via method
let mysqlService = MySQLService()
let dataManager = DataManager()
// Fetch data using method injection
print(dataManager.getData(from: mysqlService)) // Output: Data from MySQL
In this example, the DatabaseService
is injected through the getData(from:)
method, providing flexibility in deciding the service at the method level.
4. Dependency Injection Containers: Dependency injection containers are frameworks or libraries that handle the management and resolution of dependencies. They centralize the configuration of dependencies and provide an easy way to access them throughout the app.
Popular dependency injection containers in Swift iOS development include Swinject and DaggerSwift.
Regardless of the approach you choose, using dependency injection helps improve the testability of your code and promotes better separation of concerns. It also enables you to easily replace implementations, manage dependencies, and make your codebase more maintainable and scalable.
GCD VS NSOperation
GCD advantage over NSOperation:
i. Implementation
For GCD implementation is very simple, light-weight and have lock free Algo.
NSOperationQueue is complex, heavy-weight and involves a lot of locking
NSOperation advantages over GCD:
i. Control On Operation
you can Pause, Cancel, Resume an NSOperation
ii. Dependencies
you can set up a dependency between two NSOperations
operation will not started until all of its dependencies return true for finished.
iii. State of Operation
We can check the state of Operation, like queue is executing, finished or pending.
iv. Max Number of Operation
you can specify the maximum number of queued operations that can run simultaneously
v. Observable
The NSOperation and NSOperationQueue classes have a number of properties that can be observed, using KVO (Key Value Observing). This is another important benefit if you want to monitor the state of an operation or operation queue.
When to Go for GCD or NSOperation
when you want more control over queue (all above mentioned) use NSOperation and for simple cases where you want less overhead (you just want to do some work “into the background” with very little additional work) use GCD
Difference b/w delegate and protocol
🔹 What is a Protocol?
A protocol defines a blueprint (set of rules) that a class, struct, or enum must conform to.
🔹 Use Case
- Ensures that a type implements specific methods or properties.
- Helps in writing flexible, reusable, and decoupled code.
🔹 Example
protocol Animal {
var name: String { get }
func makeSound()
}
Here, Animal
is a protocol that requires:
- A
name
property. - A
makeSound()
method.
🔸 Conforming to a Protocol
class Dog: Animal {
var name: String = "Buddy"
func makeSound() {
print("Woof! Woof!")
}
}
Now, Dog
implements the Animal
protocol.
🔹 When to Use?
✅ When you want multiple types (classes, structs, enums) to follow a common blueprint.
✅ To ensure consistency across different types.
✅ 2. Delegate in Swift
🔹 What is a Delegate?
A delegate is a design pattern where one object (A) assigns a task to another object (B), allowing object B to notify object A when something happens.
Delegation is implemented using protocols!
🔹 Use Case
- Used to pass data or events between objects.
- Commonly used in UIKit (e.g., UITableViewDelegate, UITextFieldDelegate).
🔹 Example
protocol PaymentDelegate {
func paymentDidComplete()
}
class PaymentProcessor {
var delegate: PaymentDelegate?
func processPayment() {
print("Processing payment...")
delegate?.paymentDidComplete() // Notify delegate after completion
}
}
🔸 Delegate Implementation
class Order: PaymentDelegate {
func paymentDidComplete() {
print("Order confirmed! 🎉")
}
}
// Usage
let order = Order()
let paymentProcessor = PaymentProcessor()
paymentProcessor.delegate = order // Assign delegate
paymentProcessor.processPayment() // Output: Processing payment... Order confirmed! 🎉
🔹 When to Use?
✅ When you need one-to-one communication between objects.
✅ When you want an object to notify another object without tight coupling.
Explain Difference between UIWindow and UIView ?
Below is a diagram to help you understand their relationship better:
📱 iOS Screen (Physical Device Screen)
│
└── 🖼 UIWindow (Top-Level Container)
│
├── 📦 Root UIViewController (Manages Views)
│ │
│ ├── 🎨 UIView (Background, Buttons, Labels)
│ │ ├── 🔘 UIButton (Tap Me)
│ │ ├── 🏷 UILabel (Hello, World!)
│ │ ├── 🖼 UIImageView (Logo)
│ │
│ └── 🎬 Other Views (TextFields, TableViews, etc.)
│
└── 🏞 Another UIWindow (For Popups or Overlays)
🔹 What is UIWindow
?
- A
UIWindow
is the top-level container that manages and displays all the views of an iOS app. - Every iOS app has at least one
UIWindow
, which holds the root view controller. - It acts as an interface between the app’s UI and the system.
🔹 What is UIView
?
- A
UIView
is the building block of the user interface. - It represents everything you see on the screen (buttons, labels, images, etc.).
UIView
lives inside a UIWindow.
Summary
1️⃣ UIWindow
is the top-level container that holds and manages all views.
2️⃣ UIView
is the building block of the UI, responsible for displaying elements.
3️⃣ UIView
lives inside a UIWindow
.
What are benefits of Guard ?
The guard
statement is used for early exit in Swift, improving code readability, safety, and reducing nesting. Here are the key benefits:
Example:
// 1 Avoids Deep Nesting (Cleaner Code)
func processUser(name: String?) {
guard let unwrappedName = name else {
print("Invalid user")
return
}
print("User: \(unwrappedName)") // No extra indentation
}
// 2 Ensures Safe Unwrapping of Optionals
// guard let helps safely unwrap optionals before using them.
func greetUser(_ name: String?) {
guard let name = name else {
print("No name provided")
return
}
print("Hello, \(name)!")
}
// 3 Works Well with return, throw, break, continue
// Swift requires guard to exit the scope if the condition fails.
func validate(age: Int?) throws {
guard let age = age, age >= 18 else {
throw NSError(domain: "Invalid Age", code: 401, userInfo: nil)
}
print("User is eligible")
}
// 4 Improves Readability in Loops
// Instead of checking conditions deep inside a loop, guard can skip unwanted cases early.
let numbers: [Int?] = [1, nil, 3, nil, 5]
for num in numbers {
guard let num = num else { continue }
print(num) // Skips `nil` values
}
Swift Optionals
Optionals in Swift are used when a value might be absent (nil
). They help prevent runtime crashes due to null references.
1️⃣ Declaring an Optional
You declare an optional using ?
, meaning the variable can be nil
.
var name: String? = "Niraj"
var age: Int? = nil // Can hold a value or nil
2️⃣ Unwrapping Optionals
Since optionals can be nil
, you need to unwrap them before use.
// Forced Unwrapping (!)
// Use ! if you're sure the optional has a value.
// Risk: If nil, it will crash.
var name: String? = "Niraj"
print(name!) // Niraj ✅
//************************************************
// Optional Binding
// Safely unwraps an optional.
if let unwrappedName = name {
print(unwrappedName) // Niraj
} else {
print("No name found")
}
//************************************************
Or using guard let (preferred for early exit):
func printName(_ name: String?) {
guard let unwrappedName = name else {
print("No name found")
return
}
print(unwrappedName)
}
//************************************************
// Nil-Coalescing (??)
// Provides a default value if nil.
let userName: String? = nil
print(userName ?? "Guest") // Guest
//************************************************
// Optional Chaining (?.)
// Calls properties/methods only if not nil.
struct User {
var address: String?
}
let user: User? = User(address: "New York")
print(user?.address ?? "No Address") // New York
//************************************************
// Implicitly Unwrapped Optionals (!)
// Used when a variable is guaranteed to have a value after initialization.
var email: String! = "niraj@example.com"
print(email) // No need to unwrap
- Static functions are defined using the static keyword, while class functions are defined using the class keyword
- Class functions can be overridden by subclasses, while static functions cannot be overridden
- Both can be called by direct class name or struct name, so you do not need to create instances
- class functions can be inherited but static function can not be inherite
- If you will try to inherit a static function you will get compile time error.
You can find more detail here.
Stored and computed properties
In iOS (Swift), stored properties and computed properties are two types of properties used in classes, structures, and enumerations.
Stored Property
- Stores a value in memory.
- Can be a constant (
let
) or variable (var
). - Can have default values or be initialized in an initializer.
Example:
struct User {
var name: String // Stored property
let age: Int // Stored property (constant)
}
var user = User(name: "Niraj", age: 30)
print(user.name) // Niraj
Computed Property
- Does not store a value.
- Instead, it calculates and returns a value dynamically.
- It requires a getter and optionally a setter.
- Each time you access it, it recomputes the value.
Example:
struct Rectangle {
var width: Double
var height: Double
var area: Double { // Computed Property
return width * height // No storage, calculated every time
}
}
var rect = Rectangle(width: 5, height: 10)
print(rect.area) // 50.0 ✅
// Here, area is not stored in memory.
// It is computed each time it is accessed.
Example with Getter & Setter
struct Circle {
var radius: Double
var diameter: Double {
get {
return radius * 2
}
set {
radius = newValue / 2
}
}
}
var circle = Circle(radius: 5)
print(circle.diameter) // 10
circle.diameter = 20
print(circle.radius) // 10
Question:
Can we willSet/didSet with computed property?
No, willSet
and didSet
cannot be used with computed properties because computed properties do not store a value in memory—they are dynamically calculated each time they are accessed.
Why?
willSet/didSet
are designed for stored properties to track actual value changes in memory.- Computed properties don’t store values, so there’s no actual change to observe.
Example (Why it Fails)
🚨 This will cause a compilation error:
var area: Double {
willSet { // ❌ ERROR: Property observers cannot be used on a computed property
print("Area will change to \(newValue)")
}
return width * height
}
✅ Correct Way: If you need to track changes, use willSet/didSet
on the stored properties that influence the computed property.
struct Rectangle {
var width: Double {
didSet {
print("Width changed to \(width), area is now \(area)")
}
}
var height: Double {
didSet {
print("Height changed to \(height), area is now \(area)")
}
}
var area: Double { // Computed Property
return width * height
}
}
var rect = Rectangle(width: 5, height: 10)
rect.width = 6 // ✅ didSet gets called
rect.height = 12
//***********************************************************
struct Circle {
var radius: Double {
willSet {
print("Radius will change from \(radius) to \(newValue)")
}
didSet {
print("Radius changed from \(oldValue) to \(radius)")
}
}
var diameter: Double {
get {
return radius * 2
}
set {
radius = newValue / 2
}
}
}
// Example Usage
var circle = Circle(radius: 5)
circle.radius = 10