SwiftUI Clean Architecture: Swift iOS Architectural Design
A crucial part of this approach is the core layer, which contains reusable components accessible across various layers of the project. This layer typically includes essential configurations, constants, and dependencies which support the structure and maintainability of the overall application.
Here, I would like to share the approach I prepared for the commercial project that I work on. I wanted to carry on with the navigation flow of the app in UIKit, keep the high testability for the business logic with Clean Swift clean architecture, and yet try implementing the views with SwiftUI.
Understanding Clean Architecture
Clean Architecture is a software design pattern that aims to create a clear separation of concerns within an application. By dividing the application into multiple layers, each with a specific role and responsibility, Clean Architecture enables the development of maintainable, scalable, and testable software systems.
The core idea behind Clean Architecture is to distinguish between the application’s business logic and its infrastructure, allowing for greater flexibility and easier maintenance. This separation ensures that changes in one part of the application do not ripple through the entire system, making it easier to manage and evolve over time.
The Layers of Clean Architecture
Clean Architecture consists of multiple layers, each organized in a hierarchical structure. These layers represent different aspects of the application, with the innermost layers focusing on the core logic and the outer layers dealing with infrastructure and presentation. This organization helps in maintaining a clean separation of concerns, making the application more modular and easier to test.
Domain Layer
The Domain Layer is the heart of the Clean Architecture pattern. It encapsulates the application’s core business logic and is responsible for defining the domain entities, use cases, and interfaces. This layer is designed to be independent of the infrastructure and presentation layers, allowing for changes to be made without affecting the rest of the application.
In the Domain Layer, you’ll typically find:
-
Entities: These represent the domain objects and their relationships.
-
Use Cases: These define the actions that can be performed on the entities.
-
Interfaces: These define the contracts for the use cases and entities, ensuring that the business logic remains decoupled from the infrastructure.
By keeping the Domain Layer isolated, we ensure that the core business logic remains unaffected by changes in the external layers, promoting a clean and maintainable architecture.
Data Layer
The Data Layer is responsible for managing the application’s data storage and retrieval. It provides a flexible and scalable way to interact with various data sources, such as databases, file systems, and web services. The Data Layer is designed to be independent of the Domain Layer, allowing for changes to be made without affecting the business logic.
The Data Layer typically consists of the following components:
-
Repository Interface: This defines the contract for data access and manipulation, ensuring a consistent way to interact with data sources.
-
Repository Implementation: This provides concrete implementations of the repository interface, handling the actual data operations.
-
Data Sources: These represent the actual mechanisms for data storage and retrieval, such as databases or web services.
By abstracting the data access logic into repository interfaces and implementations, the Data Layer ensures that the business logic in the Domain Layer remains decoupled from the specifics of data storage, promoting a clean and flexible architecture.
Presentation Layer
The Presentation Layer is responsible for handling the application’s user interface and user experience. It receives input from the user, processes it, and displays the results. The Presentation Layer is designed to be independent of the Domain Layer and Data Layer, allowing for changes to be made without affecting the business logic or data storage.
The Presentation Layer typically consists of the following components:
-
Views: These represent the user interface components, such as SwiftUI views.
-
View Models: These provide a data-binding layer between the views and the Domain Layer, ensuring that the UI remains reactive and up-to-date.
-
Controllers: These handle user input and interact with the View Models, coordinating the flow of data and actions within the application.
By separating the application into these distinct layers, Clean Architecture provides a maintainable, scalable, and testable software system that is easy to understand and modify. This separation ensures that each layer can evolve independently, promoting a clean and modular design.
Why not switch to pure SwiftUI?
I saw many ports of Redux, MVVM, and other well-known architectural design patterns for UI architecture in Swift used with SwiftUI. Yet none of them convinced me that it can be used in a project that is quite old, relies heavily on the old components in Objective-C (singletons and managers), and what is the most important - there is still need to push or show the old ViewControllers written in UIKit from the new scenes that we add.
Besides that, I try to keep the project testable with the business logic separated, and the other big reason not to switch to pure SwiftUI is that Clean Swift was already used as the major pattern for all the modern modules. Dependency injection helps to reduce coupling between layers, allowing for easier maintenance and testing.
If you wonder what Clean Swift architectural design pattern is, check my previous article before reading this one.
What are the changes?
The major changes required to use SwiftUI with Clean Swift are:
-
SceneViewController will keep a reference of SceneViewModel that is a source of data, states, and delegates of our View.
protocol WelcomeSceneViewControllerInput: AnyObject { var router: WelcomeSceneRoutingLogic? { get set } var viewModel: WelcomeSceneViewModel? { get set } }
- SceneViewModel is a new class where we can use Combine to make our SwiftUI's View reactive.
protocol WelcomeSceneViewDelegate: AnyObject { func didSelectButton(_ sender: WelcomeSceneViewModel?) } protocol WelcomeSceneViewModel { var delegate: WelcomeSceneViewDelegate? { get set } var text: String { get } var buttonText: String { get } } final class DefaultWelcomeSceneViewModel: WelcomeSceneViewModel { var delegate: WelcomeSceneViewDelegate? @Published private(set) var text: String @Published private(set) var buttonText: String init( text: String, buttonText: String ) { self.text = text self.buttonText = buttonText } }
- SceneViewController inherits from UIHostingController<SceneView>, not from UIViewController.
final class WelcomeSceneViewController: UIHostingController { var interactor: WelcomeSceneViewControllerOutput? var router: WelcomeSceneRoutingLogic? var viewModel: WelcomeSceneViewModel? }
- SceneConfigurator takes the ViewModel and assigns it to the ViewController.
The method 'configured(...)' contains a lot of boilerplate code but it should not be a problem as long as you use my code templates from this repo with the test cases provided for the Configurator module.
protocol WelcomeSceneConfigurator { func configured( with viewModel: WelcomeSceneViewModel ) -> WelcomeSceneViewController } final class DefaultWelcomeSceneConfigurator: WelcomeSceneConfigurator { func configured( with viewModel: WelcomeSceneViewModel ) -> WelcomeSceneViewController {
var viewModel = viewModel let viewController = WelcomeSceneViewController( rootView: WelcomeSceneView(viewModel: viewModel) ) let interactor = WelcomeSceneInteractor() let presenter = WelcomeScenePresenter() let router = WelcomeSceneRouter() router.source = viewController presenter.viewController = viewController interactor.presenter = presenter viewController.interactor = interactor viewController.router = router viewController.viewModel = viewModel viewModel.delegate = viewController return viewController } } - The View of our ViewController does not inherit from UIView anymore but from SwiftUI's View.
struct WelcomeSceneView: View { let viewModel: WelcomeSceneViewModel var body: some View { VStack { Text(viewModel.text) Divider() Button(viewModel.buttonText) { viewModel.delegate?.didSelectButton(viewModel) } } } }
If the snippets above are not enough, please check the example project in my GitHub account.
Clean Architecture with Clean Swift + SwiftUI
What does it bring to the project?
- Enforces modularity with very high testability, reusable components, and extracts business logic.
- Can be applied to existing projects of any size and age with target deployment above iOS 14.
Disadvantages of Clean Swift with SwiftUI
- Boilerplate code and need to use templates.
If you feel lost anywhere in the article, it may be because of the fact I assume you already know Clean Swift architectural design pattern for iOS development. If you don't, or just would like to review the basics, please check my previous blogpost Clean Swift (VIP) iOS Architecture Pattern.
My idea is just one of many and it's hard to tell how the approach to mixing UIKit and SwiftUI will change over the years. Take it and modify it the way you need it for your project.