When building an iOS app, it's important to organize your code in a way that keeps things clean, testable, and easy to maintain. One of the best ways to do that is by combining MVVM (Model–View–ViewModel) with Clean Architecture.
1. Presentation Layer — The SwiftUI Frontline
This is where your app interacts with the user.
In SwiftUI, the View and ViewModel live in this layer.
-
View (SwiftUI View):
These are your UI screens — for example,ContentView,LoginView, orProfileView.
SwiftUI Views are simple and reactive. They observe data and automatically update the UI when something changes.struct ContentView: View { @StateObject var viewModel = ContentViewModel() var body: some View { VStack { if viewModel.isLoading { ProgressView() } else { List(viewModel.items) { item in Text(item.title) } } } .onAppear { viewModel.fetchItems() } } } -
ViewModel:
The ViewModel acts as the middle layer between your UI and your business logic.
It fetches data from the Use Case (in the domain layer), handles any transformation needed for display, and exposes it to the SwiftUI View.class ContentViewModel: ObservableObject { @Published var items: [ItemUIModel] = [] @Published var isLoading = false private let getItemsUseCase: GetItemsUseCase init(getItemsUseCase: GetItemsUseCase = GetItemsUseCase()) { self.getItemsUseCase = getItemsUseCase } func fetchItems() { isLoading = true getItemsUseCase.execute { result in DispatchQueue.main.async { self.items = result.map { ItemUIModel(from: $0) } self.isLoading = false } } } }
2. Domain Layer — The Brain of Your App
This layer holds your business logic — what the app actually does.
It doesn’t care about how data is shown or where it comes from.
-
Use Cases:
A Use Case defines a specific piece of app logic — for example, “Get all items”, “Login user”, or “Save favorite”.
It talks to the Repository in the Data Layer to get or save information.struct GetItemsUseCase { private let repository = ItemRepository() func execute(completion: @escaping ([Item]) -> Void) { repository.fetchItems { items in completion(items) } } } -
Entities:
These are your core business models — pure data structures that represent real-world things in your app.struct Item: Identifiable { let id: Int let title: String }
3. Data Layer — The Data Engine
This layer handles how and where your data is stored or retrieved — such as from an API, local database, or secure storage.
-
Repository:
The Repository acts as a bridge between the Domain Layer and the actual data sources.
It decides whether to get data from the network, cache, or database.class ItemRepository { private let dataSource = ItemDataSource() func fetchItems(completion: @escaping ([Item]) -> Void) { dataSource.getItemsFromAPI { items in completion(items) } } } -
Data Source:
This is where data fetching actually happens — API calls, database queries, or file reads.class ItemDataSource { func getItemsFromAPI(completion: @escaping ([Item]) -> Void) { // Simulate network fetch let items = [Item(id: 1, title: "Apple"), Item(id: 2, title: "Banana")] completion(items) } }
4. Service Layer — The Foundation
This is an optional lower layer that includes reusable services like:
-
Network Service: Handles API requests.
-
Database Service: Manages local storage (e.g., Core Data, Realm).
-
Secure Storage: For keychain or sensitive data.
These services are used by the Data Source.
Why This Architecture Is Great for SwiftUI
-
Separation of concerns: Each layer has one clear responsibility.
-
Easier testing: You can test Use Cases or Repositories without running the UI.
-
Scalability: As your app grows, your architecture stays organized.
-
Reusability: Logic and models can be reused in other platforms (like watchOS or macOS).
By combining MVVM with Clean Architecture, your SwiftUI app becomes not just functional — but clean, testable, and ready to scale for years to come. 🚀