Complete iOS Interview Guide - Part 1

 

Complete iOS Interview Guide - Part 1

1. CLEAN Architecture

CLEAN Architecture is a software design philosophy that emphasizes separation of concerns and independence of frameworks, UI, and databases. Created by Robert C. Martin (Uncle Bob), it's widely used in iOS development.

Core Principles

1. Independence:

  • Independent of frameworks (UIKit, SwiftUI)
  • Independent of UI
  • Independent of database
  • Independent of external agencies
  • Testable without UI, database, or external elements

2. Separation of Concerns: Each layer has a specific responsibility and doesn't know about layers above it.

The Layers (from innermost to outermost)

Entities (Enterprise Business Rules):

  • Core business objects
  • Most stable, least likely to change
  • Pure Swift objects with no framework dependencies
  • Example: User, Product, Order models
// Entity
struct User {
    let id: String
    let name: String
    let email: String
    
    func isValidEmail() -> Bool {
        // Business logic
        return email.contains("@")
    }
}

Use Cases (Application Business Rules):

  • Contains application-specific business rules
  • Orchestrates flow of data to/from entities
  • Implements business logic
  • Also called Interactors
// Use Case / Interactor
protocol FetchUserUseCase {
    func execute(userId: String) async throws -> User
}

class FetchUserUseCaseImpl: FetchUserUseCase {
    private let userRepository: UserRepository
    
    init(userRepository: UserRepository) {
        self.userRepository = userRepository
    }
    
    func execute(userId: String) async throws -> User {
        // Business logic here
        let user = try await userRepository.getUser(id: userId)
        
        // Additional business rules
        guard user.isValidEmail() else {
            throw UserError.invalidEmail
        }
        
        return user
    }
}

Interface Adapters (Controllers, Presenters, Gateways):

  • Converts data between use cases and external agencies
  • ViewModels, Presenters live here
  • Repository implementations
// Presenter/ViewModel
class UserViewModel: ObservableObject {
    @Published var userName: String = ""
    @Published var isLoading: Bool = false
    @Published var errorMessage: String?
    
    private let fetchUserUseCase: FetchUserUseCase
    
    init(fetchUserUseCase: FetchUserUseCase) {
        self.fetchUserUseCase = fetchUserUseCase
    }
    
    func loadUser(id: String) {
        isLoading = true
        
        Task {
            do {
                let user = try await fetchUserUseCase.execute(userId: id)
                await MainActor.run {
                    self.userName = user.name
                    self.isLoading = false
                }
            } catch {
                await MainActor.run {
                    self.errorMessage = error.localizedDescription
                    self.isLoading = false
                }
            }
        }
    }
}

Frameworks & Drivers (UI, Database, Network):

  • Outermost layer
  • UIKit/SwiftUI views
  • Network clients (URLSession, Alamofire)
  • Database frameworks (CoreData, Realm)
// View (SwiftUI)
struct UserView: View {
    @StateObject var viewModel: UserViewModel
    
    var body: some View {
        VStack {
            if viewModel.isLoading {
                ProgressView()
            } else {
                Text(viewModel.userName)
            }
        }
        .onAppear {
            viewModel.loadUser(id: "123")
        }
    }
}

Dependency Rule

Critical rule: Dependencies point inward. Outer layers depend on inner layers, never the reverse.

Frameworks/UI → Interface Adapters → Use Cases → Entities

Inner layers never import or know about outer layers.

Data Flow

  1. User interaction → View calls ViewModel
  2. ViewModel → Calls Use Case
  3. Use Case → Calls Repository (through protocol)
  4. Repository → Fetches from Network/Database
  5. Data returns through the same chain
  6. ViewModel → Updates published properties
  7. View → Automatically updates

Protocols for Inversion

Use protocols to invert dependencies:

// Domain layer defines protocol
protocol UserRepository {
    func getUser(id: String) async throws -> User
}

// Data layer implements it
class UserRepositoryImpl: UserRepository {
    private let networkService: NetworkService
    
    func getUser(id: String) async throws -> User {
        let response = try await networkService.fetch(endpoint: "/users/\(id)")
        return User(from: response)
    }
}

Advantages

1. Testability:

// Mock repository for testing
class MockUserRepository: UserRepository {
    var mockUser: User?
    var shouldThrowError = false
    
    func getUser(id: String) async throws -> User {
        if shouldThrowError {
            throw NSError(domain: "test", code: 0)
        }
        return mockUser ?? User(id: id, name: "Test", email: "test@test.com")
    }
}

// Test use case in isolation
func testFetchUser() async throws {
    let mockRepo = MockUserRepository()
    mockRepo.mockUser = User(id: "1", name: "John", email: "john@test.com")
    
    let useCase = FetchUserUseCaseImpl(userRepository: mockRepo)
    let user = try await useCase.execute(userId: "1")
    
    XCTAssertEqual(user.name, "John")
}

2. Maintainability: Clear separation makes code easier to understand and modify

3. Scalability: Easy to add features without affecting existing code

4. Flexibility: Can swap implementations (different databases, APIs) without changing business logic

Disadvantages

  • More files and boilerplate
  • Steeper learning curve
  • May be overkill for small apps
  • Initial setup takes longer

When to Use

Good for:

  • Large, complex applications
  • Apps with multiple developers
  • Long-term projects requiring maintainability
  • Apps with changing requirements

Overkill for:

  • Prototypes or MVPs
  • Very simple apps
  • Solo projects with clear, stable requirements

2. Dependency Injection (DI)

Dependency Injection is a design pattern where objects receive their dependencies from external sources rather than creating them internally.

What Problem Does It Solve?

Without DI (Tight Coupling):

class UserViewModel {
    let repository = UserRepository() // Creates dependency
    let analytics = AnalyticsService()
    
    func loadUser() {
        // Now UserViewModel is tightly coupled to specific implementations
        // Hard to test, hard to change
    }
}

Problems:

  • Can't easily swap implementations
  • Hard to test (can't use mock repository)
  • Creates dependencies in constructor (tight coupling)

With DI (Loose Coupling):

class UserViewModel {
    private let repository: UserRepositoryProtocol
    private let analytics: AnalyticsProtocol
    
    init(repository: UserRepositoryProtocol, 
         analytics: AnalyticsProtocol) {
        self.repository = repository
        self.analytics = analytics
    }
    
    func loadUser() {
        // Uses injected dependencies
    }
}

Benefits:

  • Easy to swap implementations
  • Easy to test with mocks
  • Loose coupling
  • Single Responsibility Principle

Types of Dependency Injection

1. Constructor Injection (Most Common in iOS):

class LoginViewController: UIViewController {
    private let viewModel: LoginViewModel
    
    // Dependencies injected through initializer
    init(viewModel: LoginViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Advantages:

  • Dependencies explicit and required
  • Immutable (let)
  • Clear what object needs

2. Property Injection:

class ProfileViewController: UIViewController {
    var viewModel: ProfileViewModel! // Injected after initialization
    
    func configure(with viewModel: ProfileViewModel) {
        self.viewModel = viewModel
    }
}

Use when:

  • Storyboard/XIB initialization required
  • Circular dependencies (rare, usually design smell)

3. Method Injection:

class ImageProcessor {
    func process(image: UIImage, with filter: ImageFilter) {
        // Filter injected as method parameter
    }
}

Use when:

  • Dependency varies per method call
  • Not all methods need the dependency

Dependency Injection Container

For complex apps with many dependencies, use a DI container:

protocol DIContainer {
    func resolve<T>() -> T
}

class AppDIContainer: DIContainer {
    // Singleton dependencies
    private lazy var networkService: NetworkService = NetworkServiceImpl()
    private lazy var userRepository: UserRepository = UserRepositoryImpl(
        networkService: networkService
    )
    
    func resolve<T>() -> T {
        switch T.self {
        case is NetworkService.Type:
            return networkService as! T
        case is UserRepository.Type:
            return userRepository as! T
        case is FetchUserUseCase.Type:
            return FetchUserUseCaseImpl(
                userRepository: resolve()
            ) as! T
        default:
            fatalError("No registration for \(T.self)")
        }
    }
}

Usage:

let container = AppDIContainer()
let useCase: FetchUserUseCase = container.resolve()
let viewModel = UserViewModel(fetchUserUseCase: useCase)

Protocol-Oriented DI

// Define protocol
protocol UserService {
    func fetchUser(id: String) async throws -> User
}

// Production implementation
class ProductionUserService: UserService {
    func fetchUser(id: String) async throws -> User {
        // Real API call
    }
}

// Test implementation
class MockUserService: UserService {
    var mockUser: User?
    
    func fetchUser(id: String) async throws -> User {
        return mockUser ?? User(id: id, name: "Mock", email: "mock@test.com")
    }
}

// Injection
class UserViewModel {
    private let userService: UserService
    
    init(userService: UserService) {
        self.userService = userService
    }
}

// Production
let viewModel = UserViewModel(userService: ProductionUserService())

// Testing
let viewModel = UserViewModel(userService: MockUserService())

Environment Objects (SwiftUI DI Pattern)

class AppState: ObservableObject {
    let userRepository: UserRepository
    let authService: AuthService
    
    init(userRepository: UserRepository, authService: AuthService) {
        self.userRepository = userRepository
        self.authService = authService
    }
}

// App level
@main
struct MyApp: App {
    @StateObject var appState = AppState(
        userRepository: UserRepositoryImpl(),
        authService: AuthServiceImpl()
    )
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(appState)
        }
    }
}

// View level
struct UserProfileView: View {
    @EnvironmentObject var appState: AppState
    
    var body: some View {
        Text("Profile")
            .onAppear {
                // Access injected dependencies
                appState.userRepository.fetchUser()
            }
    }
}

Third-Party DI Frameworks

Swinject (Popular Swift DI Container):

import Swinject

let container = Container()

// Registration
container.register(UserRepository.self) { _ in
    UserRepositoryImpl()
}

container.register(FetchUserUseCase.self) { resolver in
    FetchUserUseCaseImpl(
        userRepository: resolver.resolve(UserRepository.self)!
    )
}

// Resolution
let useCase = container.resolve(FetchUserUseCase.self)!

Best Practices

  1. Prefer constructor injection for required dependencies
  2. Use protocols for abstraction and testability
  3. Inject interfaces, not implementations
  4. Keep constructors simple (no logic, just assignment)
  5. Avoid service locator pattern (anti-pattern where objects fetch their own dependencies)
  6. Use DI container for complex dependency graphs
  7. Document dependencies if not obvious from types

Common Pitfalls

1. Over-injection:

// Too many dependencies (code smell)
class ViewController {
    init(dep1: A, dep2: B, dep3: C, dep4: D, dep5: E, dep6: F) {
        // Too many! Split into smaller classes
    }
}

2. Circular Dependencies:

// A depends on B, B depends on A (BAD)
class A {
    let b: B
    init(b: B) { self.b = b }
}

class B {
    let a: A
    init(a: A) { self.a = a }
}

Solution: Use protocols or rethink design


3. ARC (Automatic Reference Counting)

ARC is Swift's memory management system that automatically tracks and manages app memory by counting references to objects.

How ARC Works

1. Reference Counting:

Every time you create a new reference to a class instance, ARC increments the reference count. When a reference goes away, ARC decrements it. When count reaches zero, the object is deallocated.

class Person {
    let name: String
    
    init(name: String) {
        self.name = name
        print("\(name) is initialized")
    }
    
    deinit {
        print("\(name) is deinitialized")
    }
}

var reference1: Person? = Person(name: "John") // Count: 1
var reference2: Person? = reference1            // Count: 2
var reference3: Person? = reference1            // Count: 3

reference1 = nil // Count: 2
reference2 = nil // Count: 1
reference3 = nil // Count: 0 → deinit called

2. Only Reference Types:

ARC applies only to class instances (reference types), not structs or enums (value types).

struct MyStruct { } // Value type - copied, not counted
class MyClass { }   // Reference type - counted by ARC

Strong References (Default)

class Apartment {
    let unit: String
    var tenant: Person?
    
    init(unit: String) {
        self.unit = unit
    }
}

class Person {
    let name: String
    var apartment: Apartment?
    
    init(name: String) {
        self.name = name
    }
}

var john: Person? = Person(name: "John")
var unit4A: Apartment? = Apartment(unit: "4A")

john?.apartment = unit4A  // Person → Apartment (strong)
unit4A?.tenant = john     // Apartment → Person (strong)

// Strong reference cycle! Both keep each other alive
john = nil      // Person still has 1 reference (from Apartment)
unit4A = nil    // Apartment still has 1 reference (from Person)
// Memory leak! Neither is deallocated

Weak References

Weak references don't increase reference count and automatically become nil when the referenced object is deallocated.

class Apartment {
    let unit: String
    weak var tenant: Person? // weak reference
    
    init(unit: String) {
        self.unit = unit
    }
}

class Person {
    let name: String
    var apartment: Apartment?
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) deinitialized")
    }
}

var john: Person? = Person(name: "John")
var unit4A: Apartment? = Apartment(unit: "4A")

john?.apartment = unit4A  // Person → Apartment (strong)
unit4A?.tenant = john     // Apartment → Person (WEAK)

john = nil    // Person count: 0 → deallocated
              // Apartment's tenant automatically becomes nil

Rules:

  • Must be var (can change to nil)
  • Must be optional type
  • Used for optional parent-child relationships

Common use case - Delegates:

protocol DataSourceDelegate: AnyObject {
    func didReceiveData()
}

class DataSource {
    weak var delegate: DataSourceDelegate? // Prevent retain cycle
}

class ViewController: UIViewController, DataSourceDelegate {
    let dataSource = DataSource()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        dataSource.delegate = self // VC → DataSource (strong)
                                   // DataSource → VC (weak)
    }
    
    func didReceiveData() {
        print("Data received")
    }
}

Unowned References

Like weak, but assumes the reference will never be nil during its lifetime.

class Customer {
    let name: String
    var card: CreditCard?
    
    init(name: String) {
        self.name = name
    }
}

class CreditCard {
    let number: String
    unowned let customer: Customer // Will always exist
    
    init(number: String, customer: Customer) {
        self.number = number
        self.customer = customer
    }
}

var john: Customer? = Customer(name: "John")
john?.card = CreditCard(number: "1234", customer: john!)

// Customer → CreditCard (strong)
// CreditCard → Customer (unowned)

john = nil // Both are deallocated
           // Card can't exist without customer

Rules:

  • Can be let or var
  • Not optional
  • Accessing deallocated unowned reference crashes
  • Use when you're certain the reference will outlive or be deallocated simultaneously

Weak vs Unowned

Feature weak unowned
Optionality Always optional Can be non-optional
Mutability Must be var Can be let
Safety Becomes nil safely Crashes if accessed after deallocation
Use case May become nil Never nil during lifetime

Choose weak when:

  • Reference might become nil
  • Parent-child where child can outlive parent

Choose unowned when:

  • Reference should never be nil
  • Referenced object same or longer lifetime
  • Closure captures in non-escaping contexts

Closures and Capture Lists

Closures capture strong references by default, causing retain cycles:

class ViewController: UIViewController {
    var name = "VC"
    
    func setupHandler() {
        apiService.fetchData { data in
            // Closure captures self strongly
            print(self.name) // Retain cycle if apiService holds closure
        }
    }
}

Solution - Capture Lists:

// 1. Weak self (most common)
apiService.fetchData { [weak self] data in
    guard let self = self else { return }
    print(self.name) // Safe, self might be nil
}

// 2. Unowned self
apiService.fetchData { [unowned self] data in
    print(self.name) // Crashes if self is deallocated
}

// 3. Weak capture of specific properties
apiService.fetchData { [weak self] data in
    self?.updateUI() // Optional chaining
}

When to use each:

// weak - asynchronous, self might not exist
URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
    self?.handleResponse(data)
}.resume()

// unowned - self definitely exists during closure execution
func delayedExecution() {
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [unowned self] in
        self.someMethod() // OK if VC definitely exists
    }
}

// No capture needed - doesn't use self
let numbers = [1, 2, 3]
numbers.map { $0 * 2 } // No retain cycle

Common Memory Issues

1. Retain Cycles with Closures:

class ViewController: UIViewController {
    var onComplete: (() -> Void)?
    
    func setup() {
        onComplete = {
            self.dismiss(animated: true) // Cycle! VC → closure → VC
        }
    }
}

// Fix
func setup() {
    onComplete = { [weak self] in
        self?.dismiss(animated: true)
    }
}

2. Capture List in Escaping Closures:

class ImageDownloader {
    var completion: ((UIImage) -> Void)?
    
    func download(from url: URL) {
        URLSession.shared.dataTask(with: url) { data, _, _ in
            if let data = data, let image = UIImage(data: data) {
                self.completion?(image) // Potential cycle
            }
        }.resume()
    }
}

// Fix
func download(from url: URL) {
    URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
        guard let self = self,
              let data = data,
              let image = UIImage(data: data) else { return }
        self.completion?(image)
    }.resume()
}

3. Timer Retain Cycles:

class ViewController: UIViewController {
    var timer: Timer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Timer strongly retains target (self)
        timer = Timer.scheduledTimer(
            timeInterval: 1.0,
            target: self,
            selector: #selector(update),
            userInfo: nil,
            repeats: true
        )
    }
    
    @objc func update() {
        print("Update")
    }
    
    deinit {
        timer?.invalidate() // Must invalidate to break cycle
    }
}

// Better: Use block-based timer (iOS 10+)
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
    self?.update()
}

Debugging Memory Issues

1. Instruments - Leaks:

  • Detects memory leaks
  • Shows leaked objects
  • Provides call stack

2. Instruments - Allocations:

  • Tracks memory allocations
  • Shows memory growth over time
  • Identifies temporary vs persistent allocations

3. Debug Memory Graph:

  • Xcode → Debug → View Memory Graph Hierarchy
  • Shows all objects in memory
  • Identifies retain cycles visually
  • Purple exclamation marks indicate cycles

4. Print deinit:

class MyClass {
    let name: String
    
    init(name: String) {
        self.name = name
        print("✅ \(name) initialized")
    }
    
    deinit {
        print("❌ \(name) deinitialized")
    }
}

If deinit doesn't print when expected, you have a memory leak.

Best Practices

  1. Use weak for delegates
  2. Use [weak self] in escaping closures
  3. Invalidate timers in deinit
  4. Use unowned only when absolutely certain
  5. Prefer structs over classes when possible (no ARC needed)
  6. Profile regularly with Instruments
  7. Add deinit logging during development
  8. Be careful with two-way strong references

4. Grand Central Dispatch (GCD)

GCD is Apple's low-level API for managing concurrent operations. It uses dispatch queues to manage tasks.

Core Concepts

1. Queues:

A queue is a data structure that manages tasks. GCD has two types:

Serial Queue:

  • Executes one task at a time
  • Tasks execute in FIFO order
  • Each task completes before next starts
let serialQueue = DispatchQueue(label: "com.app.serialQueue")

serialQueue.async {
    print("Task 1 starts")
    sleep(2)
    print("Task 1 ends")
}

serialQueue.async {
    print("Task 2 starts") // Waits for Task 1
    sleep(1)
    print("Task 2 ends")
}

// Output:
// Task 1 starts
// Task 1 ends
// Task 2 starts
// Task 2 ends

Concurrent Queue:

  • Can execute multiple tasks simultaneously
  • Tasks start in FIFO order
  • Tasks can execute in parallel
let concurrentQueue = DispatchQueue(label: "com.app.concurrent", attributes: .concurrent)

concurrentQueue.async {
    print("Task 1 starts")
    sleep(2)
    print("Task 1 ends")
}

concurrentQueue.async {
    print("Task 2 starts") // Doesn't wait
    sleep(1)
    print("Task 2 ends")
}

// Output (order may vary):
// Task 1 starts
// Task 2 starts
// Task 2 ends
// Task 1 ends

2. System Queues:

Main Queue (Serial):

DispatchQueue.main.async {
    // Update UI here - always on main thread
    self.label.text = "Updated"
}
  • Always use for UI updates
  • Serial queue
  • Runs on main thread

Global Queues (Concurrent):

DispatchQueue.global(qos: .userInitiated).async {
    // Background work
    let data = downloadData()
    
    DispatchQueue.main.async {
        // Update UI with result
        self.imageView.image = UIImage(data: data)
    }
}

Quality of Service (QoS) levels:

// User-Interactive (highest priority)
// - UI updates, animations
// - Work that must complete immediately
DispatchQueue.global(qos: .userInteractive).async {
    // Critical UI work
}

// User-Initiated
// - Work user requested and is waiting for
// - Opening document, loading data user explicitly asked for
DispatchQueue.global(qos: .userInitiated).async {
    // User waiting for result
}

// Default
// - General purpose
DispatchQueue.global(qos: .default).async {
    // Standard background work
}

// Utility
// - Long-running tasks with progress indicator
// - Downloads, computations
DispatchQueue.global(qos: .utility).async {
    // Progress-tracked work
}

// Background (lowest priority)
// - Not time-sensitive
// - Prefetching, maintenance, cleanup
DispatchQueue.global(qos: .background).async {
    // Non-urgent work
}

sync vs async

async (Asynchronous):

  • Returns immediately
  • Doesn't block current thread
  • Task executes in background
print("Before")
DispatchQueue.global().async {
    print("Inside async")
    sleep(2)
}
print("After")

// Output:
// Before
// After
// Inside async (after 2 seconds)

sync (Synchronous):

  • Blocks current thread
  • Waits for task to complete
  • Returns after task finishes
print("Before")
DispatchQueue.global().sync {
    print("Inside sync")
    sleep(2)
}
print("After")

// Output:
// Before
// Inside sync
// (2 second pause)
// After

⚠️ Deadlock Warning:

DispatchQueue.main.sync {
    // DEADLOCK! Main thread waiting for itself
    print("Never executes")
}

Never call sync on the current queue.

Dispatch Work Items

Encapsulates work to execute:

let workItem = DispatchWorkItem {
    print("Work item executing")
    sleep(2)
}

// Execute
DispatchQueue.global().async(execute: workItem)

// Cancel (if not started)
workItem.cancel()

// Wait for completion
workItem.wait()

// Notify when complete
workItem.notify(queue: .main) {
    print("Work item completed")
}

Dispatch Groups

Execute multiple tasks and get notified when all complete:

let group = DispatchGroup()

// Method 1: enter/leave
group.enter()
DispatchQueue.global().async {
    print("Task 1")
    sleep(1)
    group.leave()
}

group.enter()
DispatchQueue.global().async {
    print("Task 2")
    sleep(2)
    group.leave()
}

// Notify when all tasks complete
group.notify(queue: .main) {
    print("All tasks completed")
}

// Or wait synchronously
group.wait() // Blocks until all complete

Method 2: Automatic entry:

let group = DispatchGroup()
let queue = DispatchQueue.global()

queue.async(group: group) {
    print("Task 1")
}

queue.async(group: group) {
    print("Task 2")
}

group.notify(queue: .main) {
    print("Both complete")
}

Real-world example - Loading multiple images:

func loadImages(urls: [URL], completion: @escaping ([UIImage]) -> Void) {
    var images: [UIImage] = []
    let group = DispatchGroup()
    let queue = DispatchQueue.global(qos: .userInitiated)
    
    for url in urls {
        group.enter()
        queue.async {
            if let data = try? Data(contentsOf: url),
               let image = UIImage(data: data) {
                images.append(image)
            }
            group.leave()
        }
    }
    
    group.notify(queue: .main) {
        completion(images)
    }
}

Dispatch Barriers

Ensures exclusive access in concurrent queue:

class ThreadSafeArray<T> {
    private var array: [T] = []
    private let queue = DispatchQueue(label: "com.app.array", attributes: .concurrent)
    
    // Read (concurrent)
    func get(at index: Int) -> T? {
        var result: T?
        queue.sync {
            guard index < array.count else { return }
            result = array[index]
        }
        return result
    }
    
    // Write (exclusive with barrier)
    func append(_ element: T) {
        queue.async(flags: .barrier) {
            self.array.append(element)
        }
    }
}

Barrier blocks execute exclusively:

  • Wait for all previous tasks to complete
  • Execute alone (no other tasks run)
  • Subsequent tasks wait for barrier

Comments

Popular posts from this blog

Complete iOS Developer Guide - Swift 5 by @hiren_syl |  You Must Have To Know 😎 

piano online keyboard

Higher-Order Functions in Swift