Categoría: Architecture

  • Rules to write Clean Code in Swift

    Rules to write Clean Code in Swift

    In this post we will focusfocus on showing how principles like KISS, DRY, and SOLID directly improve Swift code in everyday situations—such as structuring a ViewModel, reducing boilerplate in SwiftUI views, or designing protocols for networking.

    In this post we will revieew some roles principles by examples in Swift. This is could be considerered a continuation of a past post called ‘S.O.L.I.D. principles in Swift’

    Clean Code

    Clean code is code that is simple to read, easy to understand, and straightforward to maintain. It avoids unnecessary complexity by following principles like clarity, consistency, and separation of concerns, ensuring that each part of the code has a clear purpose. Clean code favors meaningful names, small and focused functions, and eliminates duplication, making it easier for other developers (and your future self) to work with. The goal is not just to make the computer run the program, but to make the code itself communicate its intent clearly to humans.

    Clean Code is the overarching goal of writing software that is easy to read, understand, and maintain, while the SOLID principles are a concrete set of guidelines that help achieve this goal by structuring classes and dependencies in a clear, flexible way. In essence, Clean Code defines what good code should feel like, and SOLID offers how to design object-oriented code that stays true to that vision, ensuring maintainability, scalability, and clarity.

    SOC – Separation Of Concepts

    Separation of Concerns (SoC) is a clean code principle that says each part of your code should focus on a single, well-defined responsibility. By splitting concerns, you reduce complexity, improve readability, and make code easier to maintain and test. In iOS/Swift, this often means separating data models, networking logic, business logic (ViewModel), and UI (View) instead of mixing them in one place.

    import SwiftUI
    import Foundation
    
    // MARK: - Model (data only)
    struct Post: Decodable, Identifiable {
        let id: Int
        let title: String
    }
    
    // MARK: - Networking (concern: fetching data)
    final class PostService {
        func fetchPosts() async throws -> [Post] {
            let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
            let (data, _) = try await URLSession.shared.data(from: url)
            return try JSONDecoder().decode([Post].self, from: data)
        }
    }
    
    // MARK: - ViewModel (concern: state & business logic)
    @MainActor
    final class PostListViewModel: ObservableObject {
        @Published var posts: [Post] = []
        @Published var errorMessage: String?
    
        private let service = PostService()
    
        func loadPosts() async {
            do {
                posts = try await service.fetchPosts()
            } catch {
                errorMessage = "Failed to load posts"
            }
        }
    }
    
    // MARK: - View (concern: UI)
    struct PostListView: View {
        @StateObject private var vm = PostListViewModel()
    
        var body: some View {
            NavigationView {
                List(vm.posts) { post in
                    Text(post.title)
                }
                .navigationTitle("Posts")
                .task { await vm.loadPosts() }
            }
        }
    }
    

    Where’s the SoC?

    • Model (Post) → defines the data only.

    • Service (PostService) → knows how to fetch posts from the network.

    • ViewModel (PostListViewModel) → manages state and error handling.

    • View (PostListView) → displays UI and reacts to state changes.

    Each layer does one thing well, instead of mixing networking, logic, and UI together.

    DRY – Do not repeat yourself

    DRY (Don’t Repeat Yourself) is a clean code principle that says you should avoid duplicating logic across your codebase. Instead, extract common behavior into reusable functions, extensions, or abstractions. This reduces errors, makes updates easier, and improves maintainability.

    ❌ Without DRY (duplicated logic)

    struct User {
        let name: String
        let email: String
    }
    
    struct Product {
        let title: String
        let price: Double
    }
    
    func printUser(_ user: User) {
        print("----")
        print("Name: \(user.name)")
        print("Email: \(user.email)")
        print("----")
    }
    
    func printProduct(_ product: Product) {
        print("----")
        print("Title: \(product.title)")
        print("Price: \(product.price)")
        print("----")
    }
    

    The logic for printing the separators (----) and formatting output is repeated in both functions.

    ✅ With DRY (reusable abstraction)

    protocol Printable {
        var descriptionLines: [String] { get }
    }
    
    struct User: Printable {
        let name: String
        let email: String
        
        var descriptionLines: [String] {
            [
                "Name: \(name)",
                "Email: \(email)"
            ]
        }
    }
    
    struct Product: Printable {
        let title: String
        let price: Double
        
        var descriptionLines: [String] {
            [
                "Title: \(title)",
                "Price: \(price)"
            ]
        }
    }
    
    // Generic reusable function
    func printEntity(_ entity: Printable) {
        print("----")
        entity.descriptionLines.forEach { print($0) }
        print("----")
    }
    
    // Usage
    let user = User(name: "Alice", email: "alice@email.com")
    let product = Product(title: "iPhone", price: 999.99)
    
    printEntity(user)
    printEntity(product)
    

    Now, the separator and printing logic exist in one place (printEntity). If we want to change the format, we update it once, not in every function.

    KISS-Keep it simple stupid

    KISS (Keep It Simple, Stupid) is a clean code principle that emphasizes writing code in the simplest way possible, avoiding unnecessary complexity or over-engineering. The goal is clarity: solve the problem directly instead of anticipating extra cases you don’t need yet.

    ❌ Not KISS (too complex for a simple counter)

    import SwiftUI
    
    struct ComplexCounterView: View {
        @State private var count = 0
        
        var body: some View {
            VStack {
                Text("Count: \(count)")
                    .font(.largeTitle)
                
                HStack {
                    Button("Increment") { updateCount(by: 1) }
                    Button("Decrement") { updateCount(by: -1) }
                }
            }
        }
        
        // Over-abstracted logic for a simple task
        private func updateCount(by value: Int) {
            let newValue = count + value
            if newValue >= 0 {
                count = newValue
            } else {
                count = 0
            }
        }
    }
    

    Too much abstraction (updateCount) and rules for a trivial counter.


    ✅ KISS (straightforward and clear)

    import SwiftUI
    
    struct SimpleCounterView: View {
        @State private var count = 0
        
        var body: some View {
            VStack {
                Text("Count: \(count)")
                    .font(.largeTitle)
                
                HStack {
                    Button("−") { if count > 0 { count -= 1 } }
                    Button("+") { count += 1 }
                }
            }
        }
    }
    

    The code is direct, easy to read, and does only what’s needed.

    DYC-Document your code

    DYC (Document Your Code) is a clean code principle that emphasizes writing code that is self-explanatory whenever possible, but also adding clear, concise documentation when something isn’t obvious. In Swift, this is often done with /// doc comments, which Xcode uses to show inline documentation when hovering over symbols. The idea is to make the code not just work, but also easy to understand and use for other developers (or your future self).

    ✅ Example in Swift using DYC

    import Foundation
    
    /// Represents a user in the system.
    struct User {
        /// Unique identifier for the user.
        let id: Int
        
        /// The user's display name.
        let name: String
        
        /// The user's email address.
        let email: String
    }
    
    /// Protocol defining operations to fetch users.
    protocol UserRepository {
        /// Fetches all available users from a data source.
        /// - Returns: An array of `User` objects.
        /// - Throws: An error if fetching fails.
        func fetchUsers() async throws -> [User]
    }
    
    /// Remote implementation of `UserRepository` that calls an API.
    final class RemoteUserRepository: UserRepository {
        private let baseURL: URL
        
        /// Creates a new repository with the given API base URL.
        /// - Parameter baseURL: The base URL of the remote API.
        init(baseURL: URL) {
            self.baseURL = baseURL
        }
        
        /// Calls the `/users` endpoint and decodes the response into a list of `User`.
        /// - Throws: `URLError` if the network call fails or the response is invalid.
        /// - Returns: An array of users retrieved from the server.
        func fetchUsers() async throws -> [User] {
            let url = baseURL.appendingPathComponent("users")
            let (data, response) = try await URLSession.shared.data(from: url)
            
            guard (response as? HTTPURLResponse)?.statusCode == 200 else {
                throw URLError(.badServerResponse)
            }
            
            return try JSONDecoder().decode([User].self, from: data)
        }
    }
    

    Why this follows DYC

    • Each type and method has a short description of what it does.

    • Methods document parameters, return values, and possible errors.

    • Xcode can show these comments in Quick Help (Option + Click) for better developer experience.

    YAGNI-You’re not going to need it

    YAGNI (You Aren’t Gonna Need It) is a clean code principle that reminds developers not to add functionality until it’s actually needed. Writing “just in case” code leads to complexity, unused features, and harder maintenance. Instead, implement only what the current requirements demand, and extend later when there’s a real need.


    ❌ Violating YAGNI (adding unnecessary features)

    import SwiftUI
    
    struct OverEngineeredCounterView: View {
        @State private var count = 0
        
        var body: some View {
            VStack {
                Text("Count: \(count)")
                    .font(.largeTitle)
                
                HStack {
                    Button("Increment") { updateCount(by: 1) }
                    Button("Decrement") { updateCount(by: -1) }
                    Button("Reset") { resetCount() }          // Not needed (yet)
                    Button("Multiply ×2") { multiplyCount() } // Not needed (yet)
                }
            }
        }
        
        private func updateCount(by value: Int) {
            count += value
        }
        
        private func resetCount() { count = 0 }
        private func multiplyCount() { count *= 2 }
    }
    

    Only increment and decrement are required, but reset and multiply were added “just in case.”


    ✅ Following YAGNI (keep it simple until needed)

    import SwiftUI
    
    struct SimpleCounterView: View {
        @State private var count = 0
        
        var body: some View {
            VStack {
                Text("Count: \(count)")
                    .font(.largeTitle)
                
                HStack {
                    Button("−") { count -= 1 }
                    Button("+") { count += 1 }
                }
            }
        }
    }
    

    The code contains only the features needed today. If requirements change, new functionality can be added later.

    LoD-Law of Demeter

    The Law of Demeter (LoD), also called the principle of least knowledge, says:
    A unit of code should only talk to its immediate collaborators (“friends”), not to the internals of those collaborators (“friends of friends”)’.

    In practice, this means avoiding long chains like order.customer.address.city, because it creates tight coupling and makes the code fragile if internal structures change. Instead, each object should expose only what is necessary.

    ❌ Violating the Law of Demeter

    struct Address {
        let city: String
    }
    
    struct Customer {
        let address: Address
    }
    
    struct Order {
        let customer: Customer
    }
    
    func printOrderCity(order: Order) {
        // ❌ Too many “hops” (order → customer → address → city)
        print(order.customer.address.city)
    }
    

    If the internal structure of Customer or Address changes, this function breaks.


    ✅ Following the Law of Demeter

    struct Address {
        let city: String
    }
    
    struct Customer {
        private let address: Address
        
        init(address: Address) {
            self.address = address
        }
        
        /// Expose only what’s needed
        func getCity() -> String {
            return address.city
        }
    }
    
    struct Order {
        private let customer: Customer
        
        init(customer: Customer) {
            self.customer = customer
        }
        
        /// Expose only what’s needed
        func getCustomerCity() -> String {
            return customer.getCity()
        }
    }
    
    func printOrderCity(order: Order) {
        // ✅ Only talks to Order, not Order’s internals
        print(order.getCustomerCity())
    }
    

    Now printOrderCity only knows about Order. The details of Customer and Address are hidden, so changes in those types won’t ripple outward.

    Lesson:

    • ❌ Don’t chain through multiple objects (a.b.c.d).

    • ✅ Provide methods that expose the needed behavior directly.

    Conclusions

    We have demonstrated how the Clean principles align with Swift, so there is no excuse for not applying them. 

  • iOS Dev between Combine and Delegate/Closure

    iOS Dev between Combine and Delegate/Closure

    When to use Combine versus the delegate and closure approaches is a great point to clarify because many developers struggle to choose the right tool for different scenarios, especially as Apple continues to expand its reactive frameworks while traditional patterns remain widely used. This post pretends to highlight the trade-offs—such as Combine’s power in managing complex, asynchronous data streams and chaining operators, versus the simplicity and low-overhead of closures for one-off callbacks or delegates for structured, reusable communication.

    The dilemma

    Combine, delegates, and closures all solve the same core problem: communicating results or events between components asynchronously.

    Here’s what they share in common:

    • Asynchronous communication → all three let one part of your app notify another when “something happened” without blocking.

    • Decoupling → the caller doesn’t need to know how the callee works, only what result or event it sends back.

    • Custom API design → you can choose the style (reactive stream, callback, or protocol) that best fits your use case, but under the hood they’re all just passing messages across boundaries.

    • Memory management concerns → all require care with retain cycles ([weak self] in closures, weak delegates, cancellables in Combine).

    • Interoperability → you can wrap one into another (e.g., turn a delegate or closure into a Publisher, or call a closure inside a delegate method).

    In short: they’re different abstractions over the same idea of event handling and result delivery, just with different levels of structure and power.

    The rule of thumb

    Use Combine when…

    • You’re dealing with streams of values over time (state updates, network retries, text search with debounce, multi-source merges).

    • You need operator chains (map/filter/flatMap/throttle/retry/catch) and cancellable pipelines you can pause or tear down with a single AnyCancellable.

    • You’re deep in SwiftUI (@Published, ObservableObject, view models) or want fan-out to multiple subscribers.

    • Testing with schedulers and time (virtual clocks) matters.

    Use delegates/closures when…

    • It’s a one-shot or simple callback (“user picked photo”, “request finished”, “button tapped”).

    • You want minimal overhead & maximal clarity in local APIs; fewer allocations and less indirection.

    • The interaction is structured and two-way (classic UIKit delegate lifecycles) or you’re writing a library that shouldn’t force a reactive dependency.

    Examples

    Example (type-ahead search with debouncing & cancellation):

    import Combine
    import UIKit
    
    final class SearchViewModel {
        @Published var query: String = ""
        @Published private(set) var results: [Repo] = []
    
        private var bag = Set<AnyCancellable>()
        private let api = GitHubAPI()
    
        init() {
            $query
                .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
                .removeDuplicates()
                .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
                .filter { !$0.isEmpty }
                .flatMap { q in
                    self.api.searchRepos(query: q)              // -> AnyPublisher<[Repo], Error>
                        .replaceError(with: [])                 // surface errors as empty for UX
                }
                .receive(on: RunLoop.main)
                .assign(to: &$results)
        }
    }
    
    struct Repo: Decodable { let name: String }
    
    final class GitHubAPI {
        func searchRepos(query: String) -> AnyPublisher<[Repo], Error> {
            let url = URL(string: "https://api.github.com/search/repositories?q=\(query)")!
            return URLSession.shared.dataTaskPublisher(for: url)
                .map(\.data)
                .decode(type: SearchResponse.self, decoder: JSONDecoder())
                .map(\.items)
                .eraseToAnyPublisher()
        }
    }
    
    struct SearchResponse: Decodable { let items: [Repo] }
    

    Example (delegate for a child picker reporting a single selection):

    protocol ColorPickerViewControllerDelegate: AnyObject {
        func colorPicker(_ picker: ColorPickerViewController, didSelect color: UIColor)
    }
    
    final class ColorPickerViewController: UIViewController {
        weak var delegate: ColorPickerViewControllerDelegate?
    
        // call once when a color is chosen
        private func didChoose(_ color: UIColor) {
            delegate?.colorPicker(self, didSelect: color)
            dismiss(animated: true)
        }
    }
    
    // Parent sets itself as delegate
    final class SettingsViewController: UIViewController, ColorPickerViewControllerDelegate {
        func presentPicker() {
            let picker = ColorPickerViewController()
            picker.delegate = self
            present(picker, animated: true)
        }
    
        func colorPicker(_ picker: ColorPickerViewController, didSelect color: UIColor) {
            view.backgroundColor = color
        }
    }
    

    Closure variant (simple one-shot completion):

    final class ImageLoader {
        func load(_ url: URL, completion: @escaping (Result<UIImage, Error>) -> Void) {
            URLSession.shared.dataTask(with: url) { data, _, error in
                if let e = error { completion(.failure(e)); return }
                guard let data = data, let image = UIImage(data: data) else {
                    completion(.failure(NSError(domain: "decode", code: -1)))
                    return
                }
                completion(.success(image))
            }.resume()
        }
    }
    

    Quick decision checklist

    • Do I expect multiple values over time that I want to transform/combine? → Combine

    • Is it a single, directed callback (child → parent or one network response)? → Delegate/Closure

    • Do I need easy cancellation, debouncing, retries, or operator chains? → Combine

    • Is simplicity, minimal dependencies, or pre–iOS 13 support important? → Delegate/Closure

    💡Tip: don’t force reactive patterns everywhere—use Combine where its composability shines, and keep the rest lightweight.

    Bridge between them

    Combine and delegates/closures can absolutely be mixed in the same project, and in fact that’s often the most pragmatic approach. They aren’t mutually exclusive, they just solve different layers of the problem:

    • Frameworks & SDKs: Many Apple and third-party APIs still expose delegates or completion handlers (e.g. CLLocationManager, URLSession).

    • Your app architecture: You might want to stay “Combine-first” in your view models, but still need to talk to APIs that rely on delegate or closure callbacks.

    Example: turning a delegate into a Combine publisher:
    import Combine
    import CoreLocation
    
    final class LocationManagerDelegateProxy: NSObject, CLLocationManagerDelegate {
        let subject = PassthroughSubject<CLLocation, Never>()
    
        func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            if let loc = locations.last {
                subject.send(loc)
            }
        }
    }
    
    // Usage
    let locationManager = CLLocationManager()
    let proxy = LocationManagerDelegateProxy()
    locationManager.delegate = proxy
    
    let cancellable = proxy.subject
        .sink { location in
            print("Got location:", location)
        }
    

    Example: wrapping a closure in a publisher

    func loadImagePublisher(from url: URL) -> AnyPublisher<UIImage, Error> {
        Future { promise in
            URLSession.shared.dataTask(with: url) { data, _, error in
                if let e = error {
                    promise(.failure(e))
                } else if let data, let image = UIImage(data: data) {
                    promise(.success(image))
                } else {
                    promise(.failure(NSError(domain: "decode", code: -1)))
                }
            }.resume()
        }
        .eraseToAnyPublisher()
    }
    

    Conclusions

    I hope this helped clarify when it makes sense to use Combine and when a delegate or closure is the better fit. Most importantly, remember that these approaches are not mutually exclusive—you don’t need to force reactive patterns into every situation. Instead, use Combine where its strengths truly shine, and rely on delegates or closures when simplicity is all you need.

    References

    • Combine

      Apple Developer Documentation

  • Dependency Injection implementations in Swift

    Dependency Injection implementations in Swift

    How to use dependency injection both manually and with a library like Swinject is valuable because it helps developers understand the core principles behind DI, such as decoupling and testability, before introducing them to more scalable, flexible solutions. By comparing both approaches, you empower readers to make informed architectural decisions based on project complexity and team needs. It appeals to a broad audience—from beginners learning the basics to experienced developers seeking to streamline their code using frameworks—and highlights the real-world tradeoffs between control and convenience, making it a practical and educational resource.

    DIP-Dependency injection principle

    Dependency injection is a software design principle in which an object or function receives the resources or dependencies it needs from external sources rather than creating them itself, promoting loose coupling and greater flexibility in code. By separating the responsibility of constructing dependencies from their usage, dependency injection makes programs easier to test, maintain, and modify, since dependencies can be swapped or mocked without changing the dependent code. This approach is closely related to the inversion of control principle, as it shifts the creation and management of dependencies to an external entity, often called an injector or container, allowing for more modular and configurable systems.

    For our example the interface will be ‘UserService’:

    protocol UserService {
        func fetchUsers() -> [User]
    }
    
    class DefaultUserService: UserService {
        func fetchUsers() -> [User] {
            return [
                User(id: 1, name: "Alice"),
                User(id: 2, name: "Bob")
            ]
        }
    }
    

    This is how View model uses the UserService interface:

    class UserListViewModel: ObservableObject {
        @Published var users: [User] = []
    
        private let userService: UserService
    
        init(userService: UserService) {
            self.userService = userService
            loadUsers()
        }
    
        func loadUsers() {
            self.users = userService.fetchUsers()
        }
    }

    The view that presents user list:

    struct UserListView: View {
        @ObservedObject var viewModel: UserListViewModel
    
        var body: some View {
            List(viewModel.users) { user in
                Text(user.name)
            }
        }
    }

    …but where is dependency injection implemented? You’re probably thinking that right now. Hold on a sec…

    @main
    struct ManualDIApp: App {
        var body: some Scene {
            WindowGroup {
                let userService = DefaultUserService()
                let viewModel = UserListViewModel(userService: userService)
                UserListView(viewModel: viewModel)
            }
        }
    }
    

    At that point in the code, an instance of DefaultUserService—which implements the UserService protocol—is being injected into the viewModel.

    Dependency injection by using Swinject

    Using a dependency injection library like Swinject becomes especially beneficial in larger or more complex iOS applications where managing dependencies manually can become tedious, error-prone, and hard to scale. Libraries automate the resolution and lifecycle of dependencies, support advanced features like scopes, circular dependencies, and conditional bindings, and reduce boilerplate code—making them ideal when your project has many services, view models, or interconnected modules. They also promote consistency and cleaner architecture across teams, especially in projects following patterns like MVVM or Clean Architecture, where dependency graphs can quickly grow intricate.

    First thing to do is add ‘https://github.com/Swinject/Swinject’ as SPM package:

    Look out! Add Swinject to SwinjectDI target, but Swinject-Dynamic to none. I faced compilations issues due to that.

    Using a dependency injection library like Swinject becomes especially beneficial in larger or more complex iOS applications where managing dependencies manually can become tedious, error-prone, and hard to scale. Libraries automate the resolution and lifecycle of dependencies, support advanced features like scopes, circular dependencies, and conditional bindings, and reduce boilerplate code—making them ideal when your project has many services, view models, or interconnected modules. They also promote consistency and cleaner architecture across teams, especially in projects following patterns like MVVM or Clean Architecture, where dependency graphs can quickly grow intricate.

    We have to declare a new component that will be responsible for resolving dependencies:

    import Swinject
    
    class DIContainer {
        static let shared = DIContainer()
        let container: Container
    
        private init() {
            container = Container()
    
            container.register(UserService.self) { _ in DefaultUserService() }
            container.register(UserListViewModel.self) { r in
                UserListViewModel(userService: r.resolve(UserService.self)!)
            }
        }
    }
    

    The code defines a singleton dependency injection container using the Swinject library to manage and resolve dependencies within an iOS application. The DIContainer class initializes a shared Container instance and registers two types: UserService, which is mapped to its concrete implementation DefaultUserService, and UserListViewModel, which depends on UserService and retrieves it from the container using resolution. By centralizing the creation of these objects, the code promotes loose coupling, testability, and maintainability, while Swinject handles the instantiation and dependency resolution automatically.

    import Swinject
    
    class DIContainer {
        static let shared = DIContainer()
        let container: Container
    
        private init() {
            container = Container()
    
            container.register(UserService.self) { _ in DefaultUserService() }
            container.register(UserListViewModel.self) { r in
                UserListViewModel(userService: r.resolve(UserService.self)!)
            }
        }
    }
    

    The code defines a singleton dependency injection container using the Swinject library to manage and resolve dependencies within an iOS application. The DIContainer class initializes a shared Container instance and registers two types: UserService, which is mapped to its concrete implementation DefaultUserService, and UserListViewModel, which depends on UserService and retrieves it from the container using resolution. By centralizing the creation of these objects, the code promotes loose coupling, testability, and maintainability, while Swinject handles the instantiation and dependency resolution automatically.

    @main
    struct SwinjectDIApp: App {
        var body: some Scene {
            WindowGroup {
                let viewModel = DIContainer.shared.container.resolve(UserListViewModel.self)!
                UserListView(viewModel: viewModel)
            }
        }
    }

    To implement dependency injection, we simply call the resolver to fetch the appropriate instance to be injected into UserListView. Notice that UserListViewModel also depends on UserService, but this dependency is also resolved by the DIResolver. In conclusion, we can observe that the lines of code required to construct the dependency stack have been reduced to a single line.

    Handling different Protocol implementations

    What we explained in the previous section covers most cases where dependency injection needs to be implemented. However, what happens when we have different protocol implementations? For example, consider a scenario where the same view is used in different application flows, but the data sources differ—one fetches data from a database, while the other uses a REST API.

    class DefaultUserService: UserService {
        func fetchUsers() -> [User] {
            return [User(id: 1, name: "Alice")]
        }
    }
    
    class DefaultUserServiceV2: UserService {
        func fetchUsers() -> [User] {
            return [User(id: 2, name: "Charlie")]
        }
    }
    

    We now have two classes that implement the UserService protocol. The following changes are required to build the dependency injection stack:

                // Screen 1
                let service1 = DefaultUserService()
                let viewModel1 = UserListViewModel(userService: service1)
    
                // Screen 2
                let service2 = DefaultUserServiceV2()
                let viewModel2 = UserListViewModel(userService: service2)

    View model is the same what it differs is the injected userService.

    Dependency injection by using Swinject

    The dependency injection stack usually consists of the same set of dependencies. This consistency is why third-party libraries like Swinject are beneficial—they take advantage of this common pattern.

    However, occasionally, you may encounter a rare case in your app where the dependency stack for a particular screen needs to be set up differently—for instance, when the data source differs.

    Here’s how the DIContainer resolves dependencies:

    class DIContainer {
        static let shared = DIContainer()
        let container: Container
    
        private init() {
            container = Container()
    
            container.register(UserService.self, name: "v1") { _ in DefaultUserService() }
            container.register(UserService.self, name: "v2") { _ in DefaultUserServiceV2() }
    
            container.register(UserListViewModel.self, name: "v1") { r in
                let service = r.resolve(UserService.self, name: "v1")!
                return UserListViewModel(userService: service)
            }
    
            container.register(UserListViewModel.self, name: "v2") { r in
                let service = r.resolve(UserService.self, name: "v2")!
                return UserListViewModel(userService: service)
            }
        }
    }

    The solution was implemented by registering the instance type with a tag name. When implementing dependency injection, we need to provide an additional name tag parameter.

    @main
    struct SwinjectDIApp: App {
        var body: some Scene {
            WindowGroup {
                let viewModelV1 = DIContainer.shared.container.resolve(UserListViewModel.self, name: "v1")!
                UserListView(viewModel: viewModelV1)
                
    //            let viewModelV2 = DIContainer.shared.container.resolve(UserListViewModel.self, name: "v2")!
    //            UserListView(viewModel: viewModelV2)
            }
        }
    }

    In the previous code chunk, "v1" is hardcoded, but it should be dynamic. Ideally, it should instantiate either viewModelV1 or viewModelV2 depending on the situation.

    Unit tests

    Dependency injection in unit testing typically involves injecting a mock that implements a protocol, allowing for deterministic and controlled responses.

    import Foundation
    @testable import ManualDI
    
    class DefaultUserServiceMock: UserService {
        func fetchUsers() -> [User] {
            return [
                User(id: 99, name: "Mocked User")
            ]
        }
    }

    Unit tests will look something like this:

    import Testing
    @testable import ManualDI
    
    struct ManualDITests {
    
        @Test func example() async throws {
            // Write your test here and use APIs like `#expect(...)` to check expected conditions.
            let mock = DefaultUserServiceMock()
            let viewModel1 = UserListViewModel(userService: mock)
            
            #expect(viewModel1.users.count == 0)
        }
    
    }

    Dependency injection by using Swinject

    Unit test will have to be implemented in following way:

    import Testing
    import Swinject
    @testable import SwinjectDI
    
    struct SwinjectDITests {
    
        @Test func example() async throws {
            // Write your test here and use APIs like `#expect(...)` to check expected conditions.
            let testContainer = Container()
            
            testContainer.register(UserService.self) { _ in DefaultUserServiceMock() }
    
             testContainer.register(UserListViewModel.self) { r in
                 UserListViewModel(userService: r.resolve(UserService.self)!)
             }
    
             let viewModel = testContainer.resolve(UserListViewModel.self)!
            #expect(viewModel.users.first?.name == "Mocked User")
        }
    }

    Never use the app-wide DIContainer.shared in tests — always use a local test container so you can inject mocks safely and independently.

    @Injected properly wrapper

    One more thing… By using following property wrapper:

    import Swinject
    
    @propertyWrapper
    struct Injected<T> {
        private var service: T
    
        init(name: String? = nil) {
            if let name = name {
                self.service = DIContainer.shared.container.resolve(T.self, name: name)!
            } else {
                self.service = DIContainer.shared.container.resolve(T.self)!
            }
        }
    
        var wrappedValue: T {
            service
        }
    }

    DIContiner keeps more simplified:

    class DIContainer {
        static let shared = DIContainer()
        let container: Container
    
        private init() {
            container = Container()
    
            container.register(UserService.self, name: "v1") { _ in DefaultUserService() }
            container.register(UserService.self, name: "v2") { _ in DefaultUserServiceV2() }
    
            container.register(UserListViewModel.self, name: "v1") { _ in UserListViewModel() }
            container.register(UserListViewModel.self, name: "v2") { _ in UserListViewModel() }
        }
    }

    And also viewmodel:

     class UserListViewModel: ObservableObject {
         @Published var users: [User] = []
    
         @Injected(name: "v1") private var userService: UserService
    
         func loadUsers() {
             self.users = userService.fetchUsers()
         }
     }

    Conclusions

    I did not try to convince you how useful Dependency Injection is; you can easily find information about it on the internet. Instead, I aim to show how, with a third-party library like Swinject, the process of setting up the Dependency Injection stack can be simplified.

    You can find source code used for writing this post in following repository

    References