Feature Design

Module 1 of 11 0%

Feature Design

On a basic level, features are separated into Presentation, Data, and Domain layers. Let’s take a look at each one of these and see what they’re all about:

example.png


🎨 Presentation Layer

This layer handles user interaction and UI logic. It translates raw data from the Domain layer into something the user can see and interact with.

View:

struct LoginView: View {

		// MARK: - Properties
    @State private var viewModel: LoginViewModel

		// MARK: - Body
    var body: some View {
        VStack {
            TextField("Email", text: $viewModel.email)
            SecureField("Password", text: $viewModel.password)
            Button("Login", action: viewModel.onLogin)
        }
    }
}

ViewModel:

import Foundation

@Observable
final class LoginViewModel: LoginViewModelProtocol {

    // MARK: - Observables
    var email: String = ""
    var password: String = ""

    // MARK: - Dependencies
    private let loginUseCase: LoginUseCaseProtocol

    // MARK: - Initializers
    init(loginUseCase: LoginUseCaseProtocol) {
        self.loginUseCase = loginUseCase
    }

    // MARK: - Actions
    func onLogin() async {
        do {
            let user = try await loginUseCase.execute(email: email, password: password)
            onSuccess(user)
        } catch {
            onFailure(error)
        }
    }

    private func onSuccess(_ user: User) {
	    //...
    }

    private func onFailure(_ error: Error) {
	    //...
    }
}

βš™οΈ Domain Layer

This is the heart of your app. It contains the business logic, rules, and models. It’s platform-agnostic and knows nothing about UIKit, SwiftUI, or persistence mechanisms.

UseCase (aka Interactor)

final class LoginUseCase: LoginUseCaseProtocol {

    // MARK: - Dependencies
    private let authRepository: AuthRepositoryProtocol

		// MARK: - Initializers
    init(authRepository: AuthRepositoryProtocol) {
        self.authRepository = authRepository
    }

		// MARK: - Protocol Conformance
    func execute(email: String, password: String) async throws -> User {
        guard isValidEmail(email) else {
            throw LoginError.invalidEmailFormat
        }

        guard password.count >= 6 else {
            throw LoginError.passwordTooShort
        }

        let user = try await authRepository.login(email: email, password: password)
        return user
    }

    // MARK: - Helpers
    private func isValidEmail(_ email: String) -> Bool {
        return email.contains("@") && email.contains(".")
    }
}

RepositoryProtocol

protocol AuthRepositoryProtocol {
    func login(email: String, password: String) async throws -> User
}

Entity:

struct User {
    let id: String
    let name: String
    let email: String
}

πŸ’Ώ Data Layer:

This layer is responsible for fetching, saving, and managing data. It implements the abstractions defined in the Domain layer and connects to concrete systems like APIs, databases, and file storage.

Repository:

final class AuthRepository: AuthRepositoryProtocol {

		// MARK: - Dependencies
    private let authService: AuthServiceProtocol

		// MARK: - Initializers
    init(authService: AuthServiceProtocol) {
        self.authService = authService
    }

		// MARK: - Protocol Conformance
    func login(email: String, password: String) async throws -> User {
        let dto = try await authService.login(email: email, password: password)
        return User(id: dto.id, name: dto.name, email: dto.email)
    }
}

Service:

final class AuthService: AuthServiceProtocol {

    // MARK: - Dependencies
    private let apiService: ApiServiceProtocol

		// MARK: - Initializers
    init(apiService: ApiServiceProtocol) {
        self.networkClient = networkClient
    }

		// MARK: - Protocol Conformance
    func login(email: String, password: String) async throws -> UserDTO {
        let request = LoginRequestDTO(
	        email: email,
	        password: password
        )

        return try await networkClient.send(request)
    }
}

Organizing our XCode project

Finally, let’s talk about how to efficiently organize our files inside our XCode project. Our approach will follow the feature-first principle - organize our file structure around features:

πŸ“‚ Features
└── πŸ“‚ LoginFeature
		β”œβ”€β”€ πŸ“‚ LoginPresentation
		β”‚		β”œβ”€β”€ πŸ“„ LoginView
		β”‚		β”œβ”€β”€ πŸ“„ LoginViewModel
		β”‚		└── πŸ“„ LoginViewModelProtocol
		β”‚  	
		β”œβ”€β”€ πŸ“‚ LoginDomain
		β”‚		β”œβ”€β”€ πŸ“„ LoginUseCase
		β”‚		β”œβ”€β”€	πŸ“„ LoginRepositoryProtocol
		β”‚		└──	πŸ“‚ LoginEntities
		β”‚				└── πŸ“„ User
		β”‚		
		└── πŸ“‚ LoginData
				β”œβ”€β”€ πŸ“„ LoginRepository
				β”œβ”€β”€ πŸ“„ LoginService
				β”œβ”€β”€ πŸ“„ LoginServiceProtocol
				└── πŸ“‚ DTOs
						β”œβ”€β”€ πŸ“„ UserDTO
						└── πŸ“„ LoginRequestDTO