SwiftUI with MVVM Architecture: Dependency Injection and Repository Pattern

Folder Structure

DIContainer

DI+PropertyWrapper

import Foundation

protocol Injectable {}

@propertyWrapper
struct Inject<T: Injectable> {
    let wrappedValue: T
    
    init() {
        wrappedValue = Resolver.shared.resolve()
    }
}

DI+Resolver

import Foundation

class Resolver {
    
    private var storage = [String: Injectable]()
    
    static let shared = Resolver()
    private init() {}

    func add<T: Injectable>(_ injectable: T) {
        let key = String(describing: T.self)
        storage[key] = injectable
    }

    func resolve<T: Injectable>() -> T {
        let key = String(describing: T.self)
        
        guard let injectable = storage[key] as? T else {
            fatalError("\(key) has not been added as an injectable object.")
        }
        
        return injectable
    }
}

class MyDependency: Injectable {
    func doSomething() {
        print("Next level injection 💉")
    }
}

RepositoryModule

import Foundation
class RepositoryModule {
  private let userRepository: UserRepository
  init() {
      self.userRepository = UserRepositoryImpl()
      addDependencies()
  }
  private func addDepenecies(){
   let resolver = Resolver.shared
   resolver.add(userRepository)
  }
}

Service

ApiEndpoint

import Foundation

// Enum for defining API endpoints
enum Endpoint {
    case getUsers
    case getPosts
    case postUser(name: String, email: String)

    // Returns the relative URL path for each endpoint
    var path: String {
        switch self {
        case .getUsers:
            return "/users"
        case .getPosts:
            return "/posts"
        case .postUser:
            return "/users" // POST to /users endpoint to create a new user
        }
    }

    // Defines the HTTP method for each endpoint
    var method: HTTPMethod {
        switch self {
        case .getUsers, .getPosts:
            return .get
        case .postUser:
            return .post
        }
    }

    // Additional parameters for the request body (for POST requests)
    var body: Data? {
        switch self {
        case .postUser(let name, let email):
            let user: [String: Any] = ["name": name, "email": email]
            return try? JSONSerialization.data(withJSONObject: user, options: .fragmentsAllowed)
        default:
            return nil
        }
    }

    // Query parameters for GET requests (if any)
    var queryItems: [URLQueryItem]? {
        switch self {
        case .getPosts:
            return [URLQueryItem(name: "limit", value: "10")] // Example of adding query params
        default:
            return nil
        }
    }
}

// Enum for defining HTTP methods
enum HTTPMethod: String {
    case get = "GET"
    case post = "POST"
}

ApiService

import Foundation
import Combine

protocol APIServiceProtocol {
    func request<T: Decodable>(_ endpoint: Endpoint) -> AnyPublisher<T, Error>
}

class APIService: APIServiceProtocol {
    
    private let session: URLSession
    private let baseURL = BaseURL.url
    
    // Initialize with a custom URLSession (default is shared)
    init(session: URLSession = .shared) {
        self.session = session
    }
    
    // Perform the network request for the given endpoint
    func request<T: Decodable>(_ endpoint: Endpoint) -> AnyPublisher<T, Error> {
        // Construct the URL
        guard var urlComponents = URLComponents(string: baseURL + endpoint.path) else {
            return Fail(error: NetworkError.invalidURL).eraseToAnyPublisher()
        }
        
        // Add query parameters to the URL if any
        urlComponents.queryItems = endpoint.queryItems
        
        guard let url = urlComponents.url else {
            return Fail(error: NetworkError.invalidURL).eraseToAnyPublisher()
        }
        
        // Prepare the request
        var request = URLRequest(url: url)
        request.httpMethod = endpoint.method.rawValue
        
        // Add the request body for POST requests
        if let body = endpoint.body {
            request.httpBody = body
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        }
        
        // Perform the network request
        return session.dataTaskPublisher(for: request)
            .map { $0.data }
            .decode(type: T.self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
}

Response

UserResponse

class UserResponse: Codable{
  let name: String?
  let mobileNumber: String?
  let country:  String?
}

Repository

UserRepository

protocol UserRepository: Injectable {
 func getUsers(completion: @escaping GenericResponse<UserResponse>)
}

Impl

UserRepositoryImpl

typealias _UserRepository = UserRepository
   
    func getUser(completion: @escaping GenericResponse<UserResponse>){
    let request: [String: String] = ["mobileNumber" : "1234567890"] 
    ApiService.executeQuery(path:"v1/users", data: request, completion: completion)
}

SwiftUI iOS Development

Comments

2 responses to “SwiftUI with MVVM Architecture: Dependency Injection and Repository Pattern”

  1. boyarka-Inform.Com Avatar

    I must thank you ffor tthe efforts you have put iin peenning this blog.
    I’m hoping to view the same high-grade blog posts by you later on as well.

    In truth, your creative writing abilities has inswpired me to get my very own blog noww 😉 http://boyarka-inform.com/

    1. inspirethedev@gmail.com Avatar

      Thank you so much for your kind words! 😊 I’m truly glad you found the blog helpful and inspiring. Your encouragement means a lot and motivates me to keep writing and sharing. All the best for your own blogging journey — I’d love to read your work too! 🙌✨

Leave a Reply

Your email address will not be published. Required fields are marked *