- Arrays
- Strings
- Linked List
- Stack
- Queue
- Deque
- Hash Table
Author: Bishal Ram
Big O Notation (Time & Space Complexity)
Understanding time and space complexity is crucial for programmers to write efficient code. Time complexity measures how long a program takes to run, while space complexity measures the memory space it uses.
Writing efficient code or inefficient code and somebody who’s aiming to enter into a product based company must always keep this in the back of their mind that it is not about writing code it is always about writing efficient code which takes the least amount of time and least amount of memory to perform its operation now understood why time and space complexity is required let’s begin by laying a solid foundation.
Different approaches to solving a programming problem can have the same output but vary in efficiency, impacting the time taken for execution.
Time Complexity: Time complexity helps programmers estimate how long a program will take to execute by analysing the frequency of statements in the code.
Space Complexity: Space complexity involves analysing the memory space a program consumes during execution, aiding in writing efficient code with minimal memory usage.
Time Complexity:
Understanding time complexity in code involves analyzing how the code’s execution time increases with input size. By using Big O notation, we can simplify the time complexity to the highest order term, like O(n), representing linear time increase.
Different approaches to solving a programming problem can have the same output but vary in efficiency, impacting the time taken for execution.
Time complexity helps programmers estimate how long a program will take to execute by analyzing the frequency of statements in the code.
Time complexity helps us estimate how much longer it will take when input grows.
Example 1: Constant Time → O(1)
func getFirstElement(arr: [Int]) -> Int {
return arr[0]
}
- No matter if the array has 10 elements or 1 million, this takes the same time.
- Because we just look at one element.
Complexity: O(1) (constant time)
Example 2: Linear Time → O(n)
func printAll(arr: [Int]) {
for num in arr {
print(num)
}
}
- If array has 5 elements → 5 prints
- If array has 1000 elements → 1000 prints
- Time grows directly with input size
Complexity: O(n) (linear time)
Example 3: Quadratic Time → O(n²)
func printPairs(arr: [Int]) {
for i in 0..<arr.count {
for j in 0..<arr.count {
print("(\(arr[i]), \(arr[j]))")
}
}
}
- If array has 5 elements → 25 pairs
- If array has 100 elements → 10,000 pairs
- Time grows much faster as input increases (square growth).
Complexity: O(n²) (quadratic time)
Logarithmic Time → O(log n)
Imagine searching for a word in a dictionary:
- You don’t start from page 1.
- You open the middle, check, then cut half the dictionary and repeat.
func binarySearch(arr: [Int], target: Int) -> Bool {
var left = 0, right = arr.count - 1
while left <= right {
let mid = (left + right) / 2
if arr[mid] == target { return true }
if arr[mid] < target {
left = mid + 1
} else {
right = mid - 1
}
}
return false
}
How to calculate time complexity of program ?
Example: Star Pattern
for (int i = 0; i < n; i++) { // Loop 1
for (int j = 0; j < n; j++) { // Loop 2 (nested inside Loop 1)
System.out.print("*"); // Statement A
}
System.out.println(); // Statement B
}
Time complexity calculation
for i in 0..<n → n + 1
for j in 0..<n → n * (n + 1)
print("*") → n * n
print() → n
Total = n+1 + n² + n + n² + n
Conditions:
1. Eliminate Constants
2. Retain the highest ordered term
Constant eliminated that is 1
n + n² + n + n² + n = 3n + 2n²
Eliminate the constants again
n + n²
From n + n² retain the highest order term i.e n²
So the final time complexity will be n², it can be represent as O(n²).
Example 2: Matrix Multiplication
func multiplyMatrices(_ A: [[Int]], _ B: [[Int]]) -> [[Int]]? {
let m = A.count // rows in A
let n = A[0].count // cols in A
let nB = B.count // rows in B
let p = B[0].count // cols in B
var C = Array(repeating: Array(repeating: 0, count: p), count: m)
for i in 0..<m {
for j in 0..<p {
for k in 0..<n {
C[i][j] += A[i][k] * B[k][j]
}
}
}
return C
}
Calculation of time complexity
for i in 0..<m → n + 1
for j in 0..<p → n * (n + 1)
C[i][j] = 0 → n * n
for k in 0..<n → n * n *(n + 1)
C[i][j] += A[i][k] * B[k][j] → n * n * n
n + 1 + n² + n + n² + n³ + n² + n³
2n + 3n² + 2n³ (Remove All Constants)
n + n² + n³ (Remove lower order terms and preserve higher order term)
n³
Hence time complexity is O(n³)

Data Structure & Algorithms in iOS
- Big-O Notation basics ( Time & Space Complexity)
- Linear Data Structures
- Problem Solving Techniques
- Non-Linear Data Structures
- Searching Algorithms
- Sorting Algorithms
- Greedy Algorithms
- Divide and Conquer
- Advanced DSA
Introduction to DSA in iOS
In the tutorial I will introduces data structures and algorithms, emphasising the importance of data structure algorithms using Swift as step by step procedures.
What are the benefits of learning algorithm?
Algorithms are the blueprint for solving problems with software. Here are the main benefits of learning them:
1. Problem-Solving Discipline
Break down complex tasks into clear, actionable steps.
2. Efficiency and Performance
Time complexity awareness: Understand how resource usage grows with input size.
Space complexity awareness: Evaluate memory requirements.
How Algorithms Impact Everyday Technology?
Algorithms are essential in both everyday life and computer operations, enabling us to execute complex tasks efficiently. They guide us in searching, sorting and processing vast amounts of data seamlessly.
MVVM with Co-ordinator
MVVM (Model-View-ViewModel)
Purpose: Separate business logic, UI and data to improve testability and structure.
1. Model
- Represents the data and business logic.
- Example: Network responses, database entities, business rules.
2. View
- Represents UI components (like
UIViewController). - Displays data from the
ViewModel.
3. ViewModel
- Transforms data from the Model to a format the View can use.
- Contains logic to respond to user input and update the view.
- Binds to the View using Observables (like RxSwift, Combine or custom bindings).
Coordinator Pattern
Purpose: Decouples navigation logic from view controllers. This helps to:
- Avoid massive view controllers
- Improve reusability and testability
- Centralize and control navigation flow
Coordinator Responsibilities:
- Handle navigation (push, pop, modal etc.)
- Own and start view controllers
- Communicate between modules






SOLID Principle
The SOLID principles are five design principles that help create maintainable, scalable, and testable software.
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
1. Single Responsibility Principle (SRP)
A class should have only one reason to change (i.e only one responsibility).
Example (Violating SRP)
class UserManager {
func saveUserData() {
print("Saving user data to database")
}
func sendEmailVerification() {
print("Sending verification email")
}
}
Example (Following SRP)
class UserDataManager {
func saveUserData() {
print("Saving user data to database")
}
}
class EmailService {
func sendEmailVerification() {
print("Sending verification email")
}
}
// Usage
let userManager = UserDataManager()
userManager.saveUserData()
let emailService = EmailService()
emailService.sendEmailVerification()
2.Open/Closed Principle
A class should be open for extension but closed for modification.
Example (Violating OCP)
class Payment {
func processPayment(type: String) {
if type == "CreditCard" {
print("Processing Credit Card payment")
} else if type == "UPI" {
print("Processing UPI payment")
}
}
}
- Every time a new payment method is introduced, we need to modify this class.
- This violates OCP because the class is not closed for modification.
Example (Following OCP)
We refactor the design to follow OCP using protocols and polymorphism.
// Step 1: Create a Payment protocol
protocol Payment {
func processPayment()
}
// Step 2: Implement different payment methods
class CreditCardPayment: Payment {
func processPayment() {
print("Processing Credit Card payment")
}
}
class UPIPayment: Payment {
func processPayment() {
print("Processing UPI payment")
}
}
class PayPalPayment: Payment {
func processPayment() {
print("Processing PayPal payment")
}
}
// Step 3: Used Dependency Injection in the PaymentProcessor class
class PaymentProcessor {
func process(payment: Payment) {
payment.processPayment()
}
}
// Usage
let paymentProcessor = PaymentProcessor()
let creditCardPayment = CreditCardPayment()
let upiPayment = UPIPayment()
let paypalPayment = PayPalPayment()
paymentProcessor.process(payment: creditCardPayment) // Output: Processing Credit Card payment
paymentProcessor.process(payment: upiPayment) // Output: Processing UPI payment
paymentProcessor.process(payment: paypalPayment) // Output: Processing PayPal payment
- No need to
PaymentProcessorwhen adding a new payment method. - New payment types can be added without changing existing code.
- The system is open for extension but closed for modification.
3.Liskov Substitution Principle (LSP)
Subclasses should be able to replace their base classes without breaking the application. Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.
Example (Violating LSP)
class Bird {
func fly() {
print("Flying...")
}
}
class Penguin: Bird {
override func fly() {
fatalError("Penguins can't fly!")
}
}
Example (Following LSP)
protocol Bird {
func move()
}
class Sparrow: Bird {
func move() {
print("Flying...")
}
}
class Penguin: Bird {
func move() {
print("Swimming...")
}
}
4.Interface Segregation Principle (ISP)
A class should not be forced to implement methods it doesn’t need.
Example (Violating ISP)
protocol Worker {
func work()
func eat()
}
class Robot: Worker {
func work() {
print("Working...")
}
func eat() {
// not needed
}
}
Example (Following ISP)
protocol Workable {
func work()
}
protocol Eatable {
func eat()
}
class Human: Workable, Eatable {
func work() {
print("Working...")
}
func eat() {
print("Eating...")
}
}
class Robot: Workable {
func work() {
print("Working...")
}
}
Now, Robot only implements what it needs.
Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules; both should depend on abstractions.
Example (Violating DIP)
class LightBulb {
func turnOn() {
print("Light is ON")
}
func turnOff() {
print("Light is OFF")
}
}
class Switch {
let bulb = LightBulb() // Direct dependency on a concrete class
func operate() {
bulb.turnOn()
}
}
The Switch class depends on LightBulb, making it hard to switch to LED, Smart Bulbs.
Example (Following DIP)
protocol Switchable {
func turnOn()
func turnOff()
}
class LightBulb: Switchable {
func turnOn() {
print("Light is ON")
}
func turnOff() {
print("Light is OFF")
}
}
class SmartLight: Switchable {
func turnOn() {
print("Smart Light is ON")
}
func turnOff() {
print("Smart Light is OFF")
}
}
class Switch {
let device: Switchable
init(device: Switchable) {
self.device = device
}
func operate() {
device.turnOn()
}
}
SRP: Keeps code organized and maintainable.
OCP: Makes it easy to extend functionality.
LSP: Prevents unexpected behavior in subclasses.
ISP: Keeps interfaces clean and specific.
DIP: Makes code loosely coupled and more flexible.
Design Patterns in iOS
Design patterns help in structuring code efficiently and making it reusable, maintainable, and scalable. Here are some commonly used design patterns in Swift with examples:
- Creational Patterns → Deal with object creation.
- Structural Patterns → Focus on class and object composition.
- Behavioural Patterns → Define communication between objects.
1. Creational Design Patterns (Object Creation)
1.1 Singleton Pattern
Ensures only one instance of a class exists.
class DatabaseManager {
static let shared = DatabaseManager() // Single shared instance
private init() {} // Private constructor prevents multiple instances
func fetchData() {
print("Fetching data from database")
}
}
// Usage
DatabaseManager.shared.fetchData()
1.2 Factory Pattern
Creates objects without specifying the exact class.
protocol Notification {
func send()
}
class EmailNotification: Notification {
func send() { print("Sending Email") }
}
class SMSNotification: Notification {
func send() { print("Sending SMS") }
}
class NotificationFactory {
static func createNotification(type: String) -> Notification? {
switch type {
case "Email": return EmailNotification()
case "SMS": return SMSNotification()
default: return nil
}
}
}
// Usage
let notification = NotificationFactory.createNotification(type: "Email")
notification?.send() // Output: Sending Email
1.3 Builder Pattern
Creates complex objects step by step.
class User {
var isLoggedIn: Bool = false
var isRegistered: Bool = false
}
class UserState {
private var user = User()
func setUserLoginState() -> UserState {
user.isLoggedIn = true
return self
}
func setUserRegnState() -> UserState {
user.isRegistered = true
return self
}
func build() -> User {
return user
}
}
// Usage
let myUser = UserState().setUserLoginState().setUserRegnState().build()
print(myUser.isLoggedIn) // Output: true
2. Structural Design Patterns (Class & Object Composition)
These patterns define object relationships.
2.1 Adapter Pattern
protocol ButtonProtocol {
func onClick()
}
class ThirdPartyButton {
func press() {
print("Third-party button was pressed")
}
}
class ButtonAdapter: ButtonProtocol {
private let thirdPartyButton: ThirdPartyButton
init(thirdPartyButton: ThirdPartyButton) {
self.thirdPartyButton = thirdPartyButton
}
func onClick() {
print("Adapter converting button press...")
thirdPartyButton.press() // Adapting the method
}
}
// Usage
let thirdPartyButton = ThirdPartyButton()
let adaptedButton = ButtonAdapter(thirdPartyButton: thirdPartyButton)
adaptedButton.onClick()
//Output
Adapter converting button press...
Third-party button was pressed
2.2 Decorator Pattern
Adds functionality to an object dynamically.
protocol Coffee {
func cost() -> Double
}
class BasicCoffee: Coffee {
func cost() -> Double { return 5.0 }
}
class MilkDecorator: Coffee {
private let base: Coffee
init(base: Coffee) {
self.base = base
}
func cost() -> Double {
return base.cost() + 2.0
}
}
// Usage
let coffee = BasicCoffee()
print(coffee.cost()) // Output: 5.0
let milkCoffee = MilkDecorator(base: coffee)
print(milkCoffee.cost()) // Output: 7.0
3. Behavioral Design Patterns (Communication Between Objects)
3.1 Observer Pattern
Allows multiple objects to observe changes in another object.
protocol Observer {
func update(temperature: Double)
}
class WeatherStation {
var temperature: Double = 0 {
didSet {
notifyObservers()
}
}
private var observers: [Observer] = []
func addObserver(_ observer: Observer) {
observers.append(observer)
}
private func notifyObservers() {
observers.forEach { $0.update(temperature: temperature) }
}
}
class DisplayScreen: Observer {
func update(temperature: Double) {
print("Temperature updated: \(temperature)°C")
}
}
// Usage
let weatherStation = WeatherStation()
let display = DisplayScreen()
weatherStation.addObserver(display)
weatherStation.temperature = 30
// Output: Temperature updated: 30°C
Dependency Injection in iOS
Dependency Injection (DI) in Swift is a design pattern that allows to inject dependencies into a class instead of letting the class create them itself. This improves testability, modularity and flexibility.
There are three main types of Dependency Injection in Swift:
1. Constructor Injection (Initialiser Injection)
2. Property Injection
3. Method Injection
1. Constructor Injection (Initialiser Injection)
Dependencies are passed via the initialiser.
protocol DataService {
func fetchData() -> String
}
class APIService: DataService {
func fetchData() -> String {
return "Data from API"
}
}
class DataManager {
private let service: DataService
init(service: DataService) { // Injected Dependency via Initialiser
self.service = service
}
func getData() -> String {
return service.fetchData()
}
}
let apiService = APIService()
let dataManager = DataManager(service: apiService)
print(dataManager.getData()) // Output: "Data from API"
2. Property Injection
The dependency is injected via a property after the object is initialised.
protocol DataService {
func fetchData() -> String
}
class DataManager {
var service: DataService?
func getData() -> String {
return service?.fetchData() ?? "No data"
}
}
let dataManager = DataManager()
dataManager.service = APIService()
print(dataManager.getData()) // Output: "Data from API"
3. Method Injection
Dependencies are passed as parameters to methods instead of being stored in properties.
class DataManager {
func getData(using service: DataService) -> String {
return service.fetchData()
}
}
let dataManager = DataManager()
let apiService = APIService()
print(dataManager.getData(using: apiService)) // Output: "Data from API"
Dependency Inversion Principle(DIP)
This principle helps in achieving loose coupling between components, making the code more maintainable, scalable and testable.
Example Without Dependency Inversion (Tightly Coupled Code)
class APIService {
func fetchData() -> String {
return "Data from API"
}
}
class DataManager {
private let apiService = APIService() // Tight coupling
func getData() -> String {
return apiService.fetchData()
}
}
Here, the DataManager directly depends on APIService creating a tight coupling
If we want to replace APIService with another data source ( MockService for testing) we need to modify DataManager.
Difficult to test because DataManager is tightly coupled to APIService.
To follow DIP, we introduce an abstraction (protocol) so that DataManager depends on an interface, not a concrete class.
// Abstraction (Protocol)
protocol DataService {
func fetchData() -> String
}
// Concrete Implementation
class APIService: DataService {
func fetchData() -> String {
return "Data from API"
}
}
// Another Implementation (For Testing or Alternate Source)
class MockService: DataService {
func fetchData() -> String {
return "Mock Data"
}
}
// High-Level Module (Now depends on abstraction)
class DataManager {
private let service: DataService
init(service: DataService) {
self.service = service
}
func getData() -> String {
return service.fetchData()
}
}
// Usage
let apiService = APIService()
let dataManager = DataManager(service: apiService)
print(dataManager.getData()) // Output: "Data from API"
// For Testing
let mockService = MockService()
let testDataManager = DataManager(service: mockService)
print(testDataManager.getData()) // Output: "Mock Data"
Architecture Patterns in iOS
1. MVC (Model-View-Controller)
MVC is the default architecture pattern where
- Model: Handles data and business logic.
- View: Displays UI components.
- Controller: Acts as an intermediary between Model and View.

2. MVVM (Model-View-ViewModel)
- Model: Represents the Data or Entity
- View: UI elements (
UIView,UIViewController). - ViewModel: Acts as a bridge between the Model and View by processing data and handling logic.



MVVM with Dependency Inversion (DI)




Difference Between MVVM and MVC
MVVM (Model-View-ViewModel) and MVC (Model-View-Controller) are two popular architectural patterns used in iOS app development, particularly when working with Swift. Each pattern has its own set of principles and differences. I have highlight the key differences between MVVM and MVC in the context of Swift development:
- Separation of Concerns:
- MVC: In MVC, the controller is responsible for both user interface (View) interaction and data management (Model). This can lead to massive view controllers and difficulties in code organization.
- MVVM: MVVM separates concerns more cleanly. The ViewModel takes charge of data management and transformation, while the View remains responsible for rendering the user interface. This leads to smaller and more maintainable view controllers.
- Data Binding:
- MVC: In MVC, updating the View with changes in the Model typically requires manual synchronization. The controller is responsible for fetching data from the Model and updating the View accordingly.
- MVVM: MVVM encourages two-way data binding between the ViewModel and the View. When data in the ViewModel changes, the View is automatically updated, and user interactions are directly handled by the ViewModel.
- Testing:
- MVC: Testing can be challenging in MVC because the controller often tightly couples the View and Model. It’s difficult to isolate components for unit testing.
- MVVM: MVVM promotes easier testing. Since the ViewModel is independent of the View, it can be unit-tested more effectively. We can also use mock ViewModel instances to simulate different scenarios and test the View separately.
- Code Reusability:
- MVC: Reusing View components can be challenging because they are often tightly coupled with the controller.
- MVVM: MVVM allows for better code reusability since the ViewModel, which contains most of the business logic, can be shared between different views or even across different platforms if implemented correctly.
- View Controller Size:
- MVC: MVC often results in large and complex view controllers, as they handle both the user interface and data management.
- MVVM: MVVM helps keep view controllers smaller and more focused on user interface logic. Business logic is encapsulated in the ViewModel.
- Dependency Injection:
- MVVM: MVVM promotes the use of dependency injection to provide the ViewModel to the View. This makes it easier to test and allows for better separation of concerns.
- Reactive Programming:
- MVVM: MVVM is often paired with reactive programming frameworks like RxSwift or Combine to handle data binding and asynchronous operations more effectively.
View Controller Life Cycle
View Controller Life Cycle Methods
init(coder)/init(nibName:bundle:):
Called when the view controller is created programmatically or via Storyboard.
loadView()
Creates the view for the view controller (usually overridden if creating views manually).
viewDidLoad()
Called once when the view is loaded into memory.
viewWillAppear()
Called before the view appears on the screen
viewDidAppear()
Called after the view appears.
viewWillDisappear()
Called before the view is removed.
viewDidDisappear()
Called after the view disappears.
deinit
Called when the view controller is removed from memory.
Application Life Cycle
The app transitions through the following states in its life cycle:
- Not Running:
- The app is not launched and not running.
- Inactive:
- The app is running in the foreground but not receiving user input.
- Occurs during temporary interruptions (e.g., an incoming phone call or notification).
- Active:
- The app is running in the foreground and actively receiving user input.
- Background:
- The app is no longer visible but still executing code.
- Used for tasks like saving data, fetching content, or playing audio.
- Suspended:
- The app is in memory but not executing any code.
- It can be terminated by the system if memory is needed.
The application life cycle is managed by the AppDelegate and SceneDelegate.
1.application(_:didFinishLaunchingWithOptions:)
- Called when the app is launched.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Initialization code
print("App did finish launching")
return true
}
2.applicationDidBecomeActive(_:)
- Called when the app becomes active (foreground and interactive).
func applicationDidBecomeActive(_ application: UIApplication) {
print("App became active")
}
3.applicationWillResignActive(_:)
- Called when the app is about to go inactive.
func applicationWillResignActive(_ application: UIApplication) {
print("App will resign active")
}
4.applicationDidEnterBackground(_:)
Called when the app enters the background.
func applicationDidEnterBackground(_ application: UIApplication) {
print("App entered background")
}
5.applicationWillEnterForeground(_:)
Called as the app transitions from background to foreground.
func applicationWillEnterForeground(_ application: UIApplication) {
print("App will enter foreground")
}
6.applicationWillTerminate(_:)
Called when the app is about to terminate.
func applicationWillTerminate(_ application: UIApplication) {
print("App will terminate")
}