CODESAMPLE

Onion Architecture - Swift

Share on:

The Onion Architecture organizes code into concentric layers, with core business logic at the center and infrastructure concerns at the outer layers. Dependencies point inwards – outer layers depend on inner layers, but inner layers have no knowledge of the outer ones. This promotes testability, maintainability, and flexibility.

This Swift implementation uses protocols to define layer boundaries. The Core layer contains entities and use cases (business logic). The Interface layer defines how other layers interact with the Core. The Framework layer (e.g., UIKit, networking) depends on the Interface and implements details. Entity is a simple struct, UseCase uses protocols for dependency injection, and ViewController (framework layer) uses the UseCase through its protocol. This strict dependency inversion is key to Onion Architecture and idiomatic Swift uses of protocols.

// Core - Entities
struct User {
    let id: Int
    let name: String
}

// Core - Use Cases
protocol UserRepository {
    func getUser(id: Int) -> User?
}

protocol GetUserProfileUseCase {
    func execute(userId: Int) -> String?
}

struct GetUserProfile: GetUserProfileUseCase {
    private let userRepository: UserRepository

    init(userRepository: UserRepository) {
        self.userRepository = userRepository
    }

    func execute(userId: Int) -> String? {
        guard let user = userRepository.getUser(id: userId) else {
            return nil
        }
        return "User Profile: \(user.name)"
    }
}

// Interface - Defines interaction with Core
protocol UserInterface {
    func displayUserProfile(profile: String?)
}

// Framework Layer (UIKit)
class ViewController: UIInterface (superclass), UserInterface {
    private let getUserProfileUseCase: GetUserProfileUseCase

    init(getUserProfileUseCase: GetUserProfileUseCase) {
        self.getUserProfileUseCase = getUserProfileUseCase
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        displayUserProfile(profile: getUserProfileUseCase.execute(userId: 1))
    }

    func displayUserProfile(profile: String?) {
        // Display the profile in a UILabel or similar
        print(profile ?? "User not found")
    }
}

// Framework - Concrete implementation (e.g., Data source)
class MockUserRepository: UserRepository {
    func getUser(id: Int) -> User? {
        if id == 1 {
            return User(id: 1, name: "John Doe")
        }
        return nil
    }
}

// Example Usage
let userRepository = MockUserRepository()
let getUserProfileUseCase = GetUserProfile(userRepository: userRepository)
let viewController = ViewController(getUserProfileUseCase: getUserProfileUseCase)

// When the view loads, it will print "User Profile: John Doe"