In this article, we will implement MVVM Architecture with SwiftUI using Dependency Injection (DI) and Repository Pattern. Also, we will create a Networking Class which will be modular and reusable.
This guide will provide you a clean and scalable structure for real-world projects. Everything will be explained in Hinglish with step-by-step code examples and folder structure.
Folder Structure
YourProjectName/
│
├── Models/ // Data Models
│ └── User.swift // Example: User data model
│ └── Post.swift // Example: Post data model
│
├── Views/ // SwiftUI Views
│ └── UserListView.swift // List of users
│ └── UserDetailView.swift // User details screen
│
├── ViewModels/ // Business Logic
│ └── UserViewModel.swift // Logic for user-related views
│
├── Repositories/ // Data Access Layer
│ └── UserRepository.swift // API calls for user data
│ └── PostRepository.swift // API calls for posts
│
├── Services/ // Reusable Services
│ └── NetworkService.swift // Generic network calls
│ └── APIEndpoints.swift
│
├── DI
│ └── DIContainer.swift
│
├── Utils/ // Helper Functions and Extensions
│ └── Extensions.swift // Swift extensions
│ └── Constants.swift // App-wide constants
│
├── Resources/ // App Assets
│ └── Images/ // Images
│ └── Fonts/ // Fonts
│
└── App/ // Main App Entry Point
└── ContentView.swift // Root view
└── YourProjectNameApp.swift // SwiftUI app lifecycle
Here, we’ll implement two APIs to demonstrate why building an MVVM architecture is beneficial. With MVVM, you only need to set up the flow once, and after that, adding more APIs or functionality becomes straightforward. This saves time, reduces complexity, and improves code reusability.
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)
}
Leave a Reply