Implementing Clean Architecture in iOS for Maintainable Swift Apps

Avatar photo Atman Rathod
clock Icon 17 mins Read
Last updated: Oct 16, 2026
Implementing Clean Architecture in iOS for Maintainable Swift Apps
Table of Contents

Quick Summary: Clean Architecture iOS separates a Swift application into the Presentation, Domain, and Data Layers. These layers stay testable and modular. This article highlights how CMARIX uses MVVM, SwiftUI, dependency injection, and testing to manage codebase growth.

Building an iOS app is more than developing user-friendly interfaces and functional features. As an application adds more features, its code becomes tightly coupled, making it difficult to test, update, or add new features. Clean Architecture in iOS application development solves these challenges. Combined with MVVM, SwiftUI, dependency injection, and automated testing, this approach gives Swift apps a structure that can evolve as requirements change.

In this blog, we cover:

  • What is Clean Architecture in iOS, and why does it matter?
  • How do the Presentation, Domain, and Data layers work?
  • What is the difference between Clean Architecture and MVVM?
  • How do MVVM and SwiftUI work together in modern iOS apps?
  • How can you structure a scalable SwiftUI project?
  • How do you implement Clean Architecture with a practical Swift example?
  • How does Clean Swift (VIP) differ from Clean Architecture?
  • How do you implement dependency injection in iOS?
  • What testing strategies help maintain clean Swift code?
  • How do SOLID principles and other iOS architecture patterns improve maintainability?

What Is Clean Architecture in iOS?

Clean Architecture in iOS is a design approach that splits an app into presentation, domain, and data layers. Dependencies point inward toward business rules. This keeps code testable, framework-independent, and scalable as teams and features grow.

Robert C. Martin described the core rule in his article on The Clean Architecture. Source code dependencies point inward only. Inner layers know nothing about outer layers.

Why does this matter? Poor structure is expensive. The Consortium for Information & Software Quality estimates that poor software quality cost the US $2.41 trillion in 2022. Accumulated technical debt reached about $1.52 trillion. Finding and fixing defects is the largest single expense in the software lifecycle. Cybercrime losses linked to software flaws also rose another 42% between 2021 and 2022.

BenefitDescription
Faster testsTest business rules without a simulator.
Safer migrationsSwitch from UIKit to SwiftUI without changing the domain layer.
Parallel workTeams work independently on layers with clear contracts.
Lower reworkKeep changes limited to the relevant layer.

The CMARIX iOS team uses this structure for apps that must survive years of iOS updates.

iOS Architecture Layers Explained

iOS Clean Architecture uses three layers. The presentation layer holds SwiftUI views and ViewModels. The domain layer holds entities, use cases, and repository protocols. The data layer implements repositories, API clients, and local storage such as SwiftData or Core Data.

Presentation (SwiftUI views, ViewModels, coordinators)
      |  depends on
      v
Domain (entities, use cases, repository protocols)
      ^  depends on
      |
Data (API client, DTOs, SwiftData, repository implementations)

LayerResponsibilitiesDepends On
PresentationSwiftUI views, ViewModels, coordinatorsDomain
DomainEntities, use cases, repository protocolsNothing
DataAPI clients, DTOs, persistence, repository implementationsDomain

Presentation Layer

The presentation layer holds SwiftUI views, UIKit controllers, and ViewModels. It formats data for the screen and sends user actions to use cases. It contains no networking and no business rules.

Domain Layer

The domain layer is written purely in Swift. The entities, use cases, and repositories live here. This layer does not import SwiftUI, UIKit, or any third-party SDKs. This is the most portable and testable layer of your app.

Data Layer

The Data layer implements the repository protocol and communicates with REST APIs, SwiftData, Core Data, and the Keychain. DTO maps responses to domain entities so that the API structure is always hidden from above.

Clean Architecture vs MVVM: What Is the Difference?

MVVM structures the presentation layer through the connection between a View and a ViewModel. Clean Architecture structures an entire application with dependencies flowing inward. Many companies use both approaches.

FactorMVVMClean Architecture
ScopeUI layerEntire app
Core ideaBind View to ViewModelDependencies point inward
Main layersModel, View, ViewModelPresentation, Domain, Data
Testing focusViewModel logicUse cases and repositories
Best usedAlone for small appsWith MVVM for large apps

So which one should you pick? You do not have to choose. MVVM gives each screen a clear shape. Clean Architecture gives the whole app clear borders. Our guide to essential Swift design patterns covers the wider toolbox.

What Is MVVM in iOS?

MVVM stands for Model-View-ViewModel. The View displays data. The ViewModel prepares that data and handles user actions. The Model holds the data and its rules. The View never talks to the Model directly.

Model

The Model stores the app’s data and business logic. In iOS development, the Model is normally made up of structs or classes written in Swift. Domain entities have nothing to do with the UI, keeping everything well separated.

View

View is the User Interface itself. This is where information is displayed, and user interaction takes place. In iOS development, Views are implemented by using either SwiftUI Views or UIKit components. While MVC architecture relies heavily on views, MVVM keeps views light and free of business logic.

ViewModel

The ViewModel lies between the View and the Model. It manages data and presentation logic. It contains properties and methods that encapsulate the state and functionality of the View. Because presentation logic resides in this class, you can unit-test it without UI code.

MVVM Clean Architecture iOS: How They Work Together

In this combined architecture, the ViewModel sits in the presentation layer, while the Model becomes a domain entity. Data transfer takes place between the layers in a defined sequence:

  1. The View sends a user action to the ViewModel.
  2. The ViewModel calls a use case.
  3. The use case calls a repository protocol.
  4. The data layer implements that protocol and reaches the API or database.
  5. Domain entities travel back up and update the ViewModel state.

Each step has one job. That is why the pair scales so well.

Cutting Repetitive Code in MVVM

MVVM can create repeated code on every screen. Three habits keep it lean:

  • Share one loading state type across ViewModels.
  • Use small generic protocols for use cases.
  • Move common loading and error views into reusable modifiers.

enum LoadState<Value> {
  case idle
  case loading
  case loaded(Value)
  case failed(String)
}

MVVM-C: Adding Coordinators

Coordinators own navigation. ViewModels stay unaware of the next screen. In SwiftUI, a coordinator can manage a NavigationStack path. This keeps routing out of views and makes flows easy to test.

Building a Scalable iOS App?

Choose an architecture that keeps your Swift code modular, testable, and easy to maintain.

Inquire Today!

MVVM in SwiftUI for iOS 17+: Pros and Cons

MVVM with SwiftUI in iOS 17+ is great with the @Observable macro. The benefits are separation of concerns, unit testability, and reusable logic. Disadvantages include more files for basic views, and ViewModels can become “massive.” Use MVVM with screens that have real business logic.

Apple’s Observation framework powers @Observable. It replaces ObservableObject and @Published for most new code. Views refresh only when the properties they read change.

Pros

  • Clean split between view and logic.
  • ViewModels are easy to unit test.
  • Logic is reusable across screens.
  • @Observable removes most boilerplate.

Cons

  • Simple screens gain extra files.
  • ViewModels can become too large.
  • Teams may duplicate state that SwiftUI already manages.

While a lightweight Model-View pattern with @Observable suffices for a small application, you should switch to MVVM once the logic gets distributed among various views.

Harmonizing SwiftUI and Combine with Clean Architecture

SwiftUI

Apple developed SwiftUI to create interfaces more quickly for all devices, such as iPhone, Mac, Apple Watch, and Apple TV. In this new framework, developers describe UI state with declarative code. You no longer need to specify the behavior of each element separately.

Combine

Combine is Apple’s framework for writing reactive code. Developers use publishers, subscribers, and operators to perform asynchronous, event-driven tasks in a declarative, modular way.

In a Clean Architecture setup, SwiftUI views sit in the presentation layer. Combine publishers and subscribers and move data between layers. Views bind to ViewModel properties and update when published data changes. Changes in the Model layer flow naturally to the presentation layer, keeping responsibilities separate.

On iOS 17 and later, @Observable can replace Combine for most view state, as the section above shows. Combine still fits streams of events, such as search input or sockets.

Clean Architecture SwiftUI: Folder Structure

A clear folder tree makes the layers visible. Start with this layout:

App/
├── Presentation/
│   ├── Features/Users/ (UsersView, UsersViewModel)
│   └── Common/
├── Domain/
│   ├── Entities/
│   ├── UseCases/
│   └── Repositories/ (protocols only)
├── Data/
│   ├── Network/ (API client, DTOs)
│   ├── Persistence/ (SwiftData or Core Data)
│   └── Repositories/ (implementations)
└── DI/ (composition root)

For larger apps, turn each top-level folder into a Swift Package target. The compiler then blocks illegal imports. Keep third-party UI helpers, such as those in our list of top SwiftUI libraries, inside the presentation layer only.

Clean Architecture iOS Example: Fetching a User List

This example loads a list of users. Each step sits in its own layer.

Step 1: Domain layer. 

Define the entity, the repository protocol, and the use case.

struct User: Identifiable, Equatable {  let id: Int
  let name: String
  let email: String
}

protocol UserRepository {
  func fetchUsers() async throws -> [User]
}

protocol FetchUsersUseCase {
  func execute() async throws -> [User]
}

struct DefaultFetchUsersUseCase: FetchUsersUseCase {
  private let repository: UserRepository

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

  func execute() async throws -> [User] {
      try await repository.fetchUsers()
  }
}

Step 2: Data layer. 

Decode a DTO and map it to the entity.

struct UserDTO: Decodable {
  let id: Int
  let name: String
  let email: String

  func toDomain() -> User {
      User(id: id, name: name, email: email)
  }
}

struct RemoteUserRepository: UserRepository {
  let baseURL: URL
  var session: URLSession = .shared

  func fetchUsers() async throws -> [User] {
      let url = baseURL.appendingPathComponent("users")
      let (data, _) = try await session.data(from: url)
      let dtos = try JSONDecoder().decode([UserDTO].self, from: data)
      return dtos.map { $0.toDomain() }
  }
}

Step 3: Presentation layer. The ViewModel exposes one state value.

import SwiftUI
import Observation

@MainActor
@Observable
final class UsersViewModel {
  private(set) var state: LoadState<[User]> = .idle
  private let fetchUsers: FetchUsersUseCase

  init(fetchUsers: FetchUsersUseCase) {
      self.fetchUsers = fetchUsers
  }

  func load() async {
      state = .loading
      do {
          state = .loaded(try await fetchUsers.execute())
      } catch {
          state = .failed(error.localizedDescription)
      }
  }
}

struct UsersView: View {
  @State private var viewModel: UsersViewModel

  init(viewModel: UsersViewModel) {
      _viewModel = State(initialValue: viewModel)
  }

  var body: some View {
      Group {
          switch viewModel.state {
          case .idle, .loading:
              ProgressView()
          case .loaded(let users):
              List(users) { Text($0.name) }
          case .failed(let message):
              Text(message)
          }
      }
      .task { await viewModel.load() }
  }
}

The view knows only the ViewModel. The ViewModel knows only the use case protocol. The use case knows only the repository protocol. Nothing points outward.

Clean Swift (VIP) vs Clean Architecture

Clean Swift is another pattern introduced by Raymond Law. In this case, Clean Architecture principles apply to each screen, called a scene. The process goes through View to Interactor to Presenter and again back to View. A router handles navigation.

In Clean Swift, a Worker is a small class that handles one task, such as fetching data from an API. The Interactor calls the Worker, receives the result, and passes it to the Presenter. The Presenter formats it for the ViewController.

final class UsersWorker {
  private let repository: UserRepository

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

  func fetchUsers() async throws -> [User] {
      try await repository.fetchUsers()
  }
}

Use Clean Swift for UIKit-heavy projects with many scenes. Use MVVM with Clean Architecture for SwiftUI-first apps.

iOS Dependency Injection

What is dependency injection on iOS? It is the technique of injecting objects into the constructor, rather than creating them inside the object. You can achieve it via constructor injection using protocols.

ApproachBest ForTrade-off
Constructor injectionMost projectsMore init parameters
SwinjectLarge apps with many dependenciesExtra library, runtime resolution
SwiftUI EnvironmentView-level servicesMissing values can go unnoticed
Need Help Structuring Your iOS App?

Work with experienced iOS developers to build a maintainable application architecture.

Hire iOS Developers

Types of Dependency Injection

You can apply dependency injection in three ways: constructor, property, and method injection. Constructor injection passes dependencies when the object is created, so everything is available before you use it. Property injection sets dependencies through setters or public properties after construction. Method injection passes them as arguments to specific functions.

Benefits of Dependency Injection

Dependency injection encourages loose coupling because components receive dependencies from outside rather than building them themselves. That makes it easier to update or swap a dependency without affecting the whole system. It also supports inversion of control, so components focus on their main job. Together, these benefits improve extensibility and adaptability.

Build every object in one place, called the composition root. It usually sits in the App struct.

@main
struct UsersApp: App {
  private let viewModel: UsersViewModel = {
      let repository = RemoteUserRepository(baseURL: URL(string: "https://api.example.com")!)
      let useCase = DefaultFetchUsersUseCase(repository: repository)
      return UsersViewModel(fetchUsers: useCase)
  }()

  var body: some Scene {
      WindowGroup { UsersView(viewModel: viewModel) }
  }
}

Need help structuring your app? Talk to a team that ships layered Swift code. Hire dedicated iOS developers from CMARIX and start with an architecture review.

Testing Strategies for Clean Architecture iOS

Clean layers make testing simple. Test each layer alone, then test how they connect.

Unit tests cover use cases and ViewModels. Stub the repository and assert the state.

import Testing

@testable import YourApp

struct StubRepository: UserRepository {
  var result: Result<[User], Error>
  func fetchUsers() async throws -> [User] { try result.get() }
}

@MainActor
@Test func loadsUsers() async {
  let users = [User(id: 1, name: "Asha", email: "asha@example.com")]
  let useCase = DefaultFetchUsersUseCase(repository: StubRepository(result: .success(users)))
  let viewModel = UsersViewModel(fetchUsers: useCase)

  await viewModel.load()

  guard case .loaded(let loaded) = viewModel.state else {
      Issue.record("Expected loaded state")
      return
  }
  #expect(loaded == users)
}

  • Integration tests check that the data layer decodes real response samples. Use a mocked URLSession.
  • UI tests confirm key user flows on a simulator. Keep them few and focused.
  • End-to-end tests confirm that all components work together to deliver the intended user experience. Teams can automate them with XCTest UI testing or third-party tools such as Appium. They simulate real user workflows and help catch integration or system problems.
  • Security checks belong in the data layer. Review storage, network, and platform use against the OWASP Mobile Application Security Verification Standard. Run all suites in CI on every pull request.

SOLID Principles in Swift

Clean Architecture puts the SOLID principles into practice.

SOLID PRINCIPLEApplication in Clean Architecture
Single ResponsibilityOne use case per business action
Open/ClosedAdd repositories without editing use cases
Liskov SubstitutionMock and real repositories behave the same
Interface SegregationSmall repository protocols per feature
Dependency InversionDomain defines protocols, Data implements them

iOS Architecture Patterns Compared

No single pattern fits every app. Compare the common options before you commit.

Architecture PatternBest Use CaseImplementation ComplexityTestability
MVCSmall prototypesLowLow
MVPUIKit apps with testable presentersMediumHigh
MVVMSwiftUI screensMediumHigh
MVVM-CApps with complex navigationMedium to highHigh
VIPERLarge UIKit teamsHighHigh
MVIOne-way state flowMediumHigh
TCAStrict state managementHighVery high

If your product also targets Android, the same layering applies there. Compare the language trade-offs in our Kotlin vs Swift guide.

Build Your iOS App on a Clean Architecture Foundation

Choosing an architecture early saves months of rework later. CMARIX designs layered Swift apps with MVVM, dependency injection, and automated test suites from day one. We have also migrated and upgraded 100+ iOS apps.

Start with a clear budget. Our iOS app development cost guide breaks down the numbers. Then review the mobile app development process we follow from discovery to release.

New to the basics? Learn how to build an iOS app step by step. Building beyond Apple devices? Work with a mobile app development company that covers every platform.

Ready to build? Work with an iOS app development company that treats architecture as a first-class feature. You can also hire Swift developers who already ship Clean Architecture projects.

Build iOS App

Conclusion

iOS Clean Architecture provides you with clean layers, safe boundaries, and fast tests. MVVM gives each screen a simple structure. The trick lies in SwiftUI, @Observable, and Dependency Injection, which link everything with very few lines of code. Implement the pattern for one feature, then extend it throughout the app. For any experienced assistance, CMARIX is here to help.

FAQs on iOS Clean Architecture

What is clean architecture iOS and why should developers use it?

Clean Architecture iOS is a three-layer design: presentation, domain, and data. Programmers choose this architecture because it separates business logic from UIKit, SwiftUI, and networking. This separation enables refactoring and increases productivity on big teams. Clean Architecture brings the biggest benefit to long-living applications.

What is the main difference between Clean Architecture Swift and traditional MVVM?

MVVM organizes the presentation layer by linking a View to a ViewModel. Clean Architecture organizes the app into layers with dependencies pointing inward. Most teams combine them: MVVM handles the UI, while Clean Architecture handles business and data rules.

How do you structure a Clean Architecture SwiftUI project for scalability?

Create three main groups at the highest level, or as Swift Package Targets:ts: Presentation, Domain, and Data. Views and ViewModels should go into Presentation. Place entities, use cases, and repository protocols in Domain, while Data holds API clients, DTOs, and persistence.

How do the iOS architecture layers split responsibilities?

iOS Clean Architecture has three main layers. The presentation layer contains SwiftUI views and ViewModels. The domain layer contains entities, use cases, and repository protocols. The data layer implements repositories, API clients, and local storage like SwiftData or Core Data.

What are the pros and cons of using MVVM in modern SwiftUI for iOS 17+?

MVVM with SwiftUI on iOS 17+ works well with the @Observable macro. Benefits: clear separation of responsibilities, testable code, and reusable logic. Drawbacks: additional files for simple screens and a potential “massive” ViewModel class. Use MVVM for screens that contain business logic.

How does a clean Swift worker handle data fetching inside the VIP cycle?

In Clean Swift, a Worker is a small class that handles one task, such as fetching data from an API. The Interactor calls the Worker, receives the result, and passes it to the Presenter. The Presenter formats it for the ViewController.

Can you provide a clean, structured Swift example of network requests?

Define a repository protocol in the domain layer. Implement it in the data layer with URLSession and Decodable DTOs. Call it from a use case, then expose the result through a ViewModel state value. The example earlier in this guide shows each step in Swift.

How do you implement robust iOS dependency injection within clean code layers?

Dependency injection on iOS is when an object’s dependencies come from outside the class rather than being created inside it. Use constructor injection using protocols. This lets tests easily replace real services with mocks, keeping classes decoupled.

What design patterns work best when merging MVVM, Clean Architecture, and iOS frameworks?

Combine the following patterns with MVVM: the Repository pattern, the Use Case pattern, the Dependency Injection pattern, and the Coordinator pattern. The repository pattern hides data sources. The use case pattern contains business rules. Dependency Injection connects objects. The Coordinator manages navigation. You can use the Factory pattern or Adapter pattern where appropriate.

How do you set up automated unit testing for a Swift clean architecture workflow?

Test each layer alone. Stub repositories to test use cases and ViewModels. Use a mocked URLSession to test the data layer. Run suites with Swift Testing or XCTest in CI on every pull request. Keep UI tests few and focused on key flows.

Power your iOS app with Swift.
Read by 3549

Related Blogs

Mastering Swift Design Patterns for iOS App Development in 2026

Mastering Swift Design Patterns for iOS App Development in 2026

Swift design patterns are crucial elements of iOS app development that provide […]
Top 10 SwiftUI Libraries To Use In 2026

Top 10 SwiftUI Libraries To Use In 2026

Do you need a robust mobile application? Hire a mobile app developer […]
Kotlin vs Swift: Differences and Similarities Between Two Leading Languages for Mobile App Development

Kotlin vs Swift: Differences and Similarities Between Two Leading Languages for Mobile App Development

Both Kotlin and Swift have taken the app development world by storm […]
Hello.
Have an Interesting Project?
Let's talk about that!