Feature Design
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:
-
The Presentation layer involves everything that is related to the user interface. In MVC, this is the
View
and theViewController
. In MVVM, this is theView
and theViewModel
. Basically, anything that has to do with UI and user interaction is part of this layer. It listens to user actions, reacts to state changes, and displays data provided by the underlying layers. -
The Data layer, as the name suggests, is responsible for data management. This includes API calls, local persistence (e.g., Core Data, UserDefaults, file storage), and any other form of data retrieval or storage. It typically consists of
Repositories
,DataSources
, orServices
, which abstract the underlying implementation. The goal is to decouple the data-fetching mechanism from the business logic and the UI. -
The Domain layer sits in the middle and contains the core business logic of the application. This includes
UseCases
orInteractors
, which orchestrate operations using data from the Data layer and prepare it for the Presentation layer. The Domain layer should be pure and free of any UI or platform-specific code, making it ideal for testing and reuse.
π¨ 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:
- Typically a UIView, UIViewController, or SwiftUI View.
- Displays UI and receives user actions (e.g., taps, gestures, inputs).
- Delegates all logic to the ViewModel (in MVVM) or ViewController (in MVC).
- Should remain as dumb and stateless as possible.
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:
- Holds UI state and handles user actions.
- Interacts with
UseCases
from theDomain
layer. - Transforms domain models into view-friendly formats (e.g., date formatting, text formatting).
- Does not contain business logic.
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)
- Encapsulates a specific unit of business logic.
- Coordinates one or more repositories to fulfil a task.
- Exposes a simple interface (often a single
execute()
method).
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
- Defines abstract contracts for data access.
- Hides details about whether the data is coming from the network, cache, or database.
- Implemented in the
Data
layer.
protocol AuthRepositoryProtocol {
func login(email: String, password: String) async throws -> User
}
Entity:
- The core model in your domain.
- Pure Swift types, usually structs.
- Free of dependencies on frameworks like UIKit or Foundation (ideally).
- Represents the state and behavior of a concept in your app.
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:
- Implements the
RepositoryProtocol
from theDomain
layer. - Delegates actual work to Services (like APIs) or local storage.
- May combine multiple data sources (e.g., cache + remote).
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:
- Performs actual data operations.
- Could be a network client (e.g., making HTTP requests) or local database handler (e.g., Core Data).
- Often returns raw DTO (Data Transfer Object) models.
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