Exploring Clean Architecture and MVVM + Coordinators in SwiftUI
Written on
SwiftUI, Apple's innovative UI framework, emphasizes declarative programming, enabling developers to craft interfaces effortlessly across all Apple devices. However, alongside this advancement, selecting an appropriate architecture is crucial for ensuring scalability, maintainability, and testability within projects. This article delves into the concepts of Clean Architecture and MVVM combined with Coordinators in the realm of SwiftUI, supplemented with practical code illustrations.
Clean Architecture in SwiftUI
Clean Architecture is centered around the principle of dividing concerns into distinct layers, which enhances the modularity, flexibility, and independence of your code from specific UI or frameworks.
Implementation in SwiftUI
To effectively implement Clean Architecture in SwiftUI, the following layers should be clearly defined:
- Entities: This layer encapsulates the fundamental business logic and rules. In SwiftUI, entities correspond to your data models, which remain unaffected by changes in UI or data sources.
- Use Cases: This layer defines application-specific business rules, facilitating the data flow between entities and presenting it to the UI through view models.
- Interface Adapters: This layer includes the view models in the MVVM pattern, adjusting data from the use cases for the UI. SwiftUI views bind to these view models, monitoring state changes to refresh the UI accordingly.
- Frameworks and Drivers: The outermost layer consists of frameworks such as CoreData or various external libraries. SwiftUI itself can also be classified within this layer, alongside any other UI frameworks in use.
Example: Defining Entities and Use Cases
Entities (Core Business Logic)
struct Item {
var id: UUID
var title: String
var details: String
}
Use Cases (Application-Specific Business Rules)
protocol ItemsRepository {
func fetchItems() async throws -> [Item]}
struct FetchItemsUseCase {
var itemsRepository: ItemsRepository
func execute() async throws -> [Item] {
try await itemsRepository.fetchItems()}
}
Example: Interface Adapters with SwiftUI
ItemViewModel
class ItemViewModel: ObservableObject {
@Published var items: [Item] = []
var fetchItemsUseCase: FetchItemsUseCase
init(fetchItemsUseCase: FetchItemsUseCase) {
self.fetchItemsUseCase = fetchItemsUseCase}
func loadItems() {
Task {
do {
self.items = try await fetchItemsUseCase.execute()} catch {
print("Failed to load items")}
}
}
}
Example: SwiftUI View
struct ItemsView: View {
@ObservedObject var viewModel: ItemViewModel
var body: some View {
List(viewModel.items, id: .id) { item in
Text(item.title)}
.onAppear {
viewModel.loadItems()}
}
}
Advantages of Clean Architecture with SwiftUI:
- Decouples essential business logic from UI and frameworks, facilitating easier testing and maintenance.
- Encourages a unidirectional dependency flow from outer layers to inner layers.
- Allows for interchangeable UIs or data sources without impacting the core business logic.
Challenges of Clean Architecture with SwiftUI:
- Requires meticulous definition of boundaries and dependencies between layers, which might be excessive for simpler applications.
MVVM + Coordinators with SwiftUI
MVVM is a widely recognized pattern in iOS development that distinguishes the presentation of data (ViewModel) from the presentation layer (View). However, MVVM does not inherently address navigation concerns, which is where Coordinators are beneficial.
Implementing MVVM + Coordinators in SwiftUI
- MVVM: Each SwiftUI view is paired with a corresponding ViewModel, typically observed using the @ObservedObject or @StateObject property wrappers. The ViewModel interacts with models to fetch or update data and informs the view of any changes.
- Coordinators: These serve as the navigation logic layer. Each coordinator is tasked with determining when to transition between views/screens. In SwiftUI, coordinators can be implemented as distinct objects that manage navigation by initiating observable property changes that SwiftUI views monitor.
By integrating MVVM for data binding with Coordinators for navigation management, developers can achieve a clearer structure, separating the UI from navigation logic.
Example: Coordinator in SwiftUI
AppCoordinator
class AppCoordinator: ObservableObject {
@Published var currentPage: Page = .home
enum Page {
case home
case details(Item)
}
func goToDetails(with item: Item) {
currentPage = .details(item)}
}
NavigationView
struct AppNavigationView: View {
@StateObject var coordinator = AppCoordinator()
var body: some View {
switch coordinator.currentPage {
case .home:
ItemsView(viewModel: ItemViewModel(fetchItemsUseCase: FetchItemsUseCase(itemsRepository: ...)), coordinator: coordinator)case .details(let item):
ItemDetailView(item: item)}
}
}
Wiring MVVM with Coordinator in SwiftUI
ItemsView with Coordinator
struct ItemsView: View {
@ObservedObject var viewModel: ItemViewModel
var coordinator: AppCoordinator
var body: some View {
List(viewModel.items, id: .id) { item in
Button(item.title) {
coordinator.goToDetails(with: item)}
}
.onAppear {
viewModel.loadItems()}
}
}
Advantages of MVVM + Coordinators with SwiftUI
- Clear distinction between navigation logic (Coordinators) and presentation/business logic (MVVM).
- Improved testability and maintainability, allowing independent testing of each component.
- Offers flexibility for managing intricate navigation flows within SwiftUI applications.
Challenges of MVVM + Coordinators with SwiftUI:
- Adds complexity in managing coordinators, particularly in applications with extensive navigation hierarchies.
- Necessitates careful consideration of the ownership and lifecycle of coordinators.
Clean Architecture vs MVVM + Coordinators: When to Use Which?
While both architectural patterns promote separation of concerns and testability, the selection between them often hinges on the complexity of the application and the preferences of the development team.
- Choose Clean Architecture for large, intricate applications where core business logic may be utilized across various platforms, or when the application logic is complex enough to gain from rigid layer separations.
- Opt for MVVM + Coordinators for less complex applications that still require sophisticated navigation. This combination strikes a balance between structured architecture and the declarative style of SwiftUI, making it suitable for most iOS/macOS applications.
Conclusion
Adopting either Clean Architecture or the MVVM + Coordinators approach can significantly enhance SwiftUI app development through a well-considered architectural strategy. Clean Architecture is ideal for applications with complex business logic, whereas MVVM + Coordinators effectively addresses applications with detailed navigation requirements. Grasping these patterns and selecting the most appropriate one for your project can greatly influence the success and scalability of your SwiftUI application.
Stackademic ?
Thank you for reading this article. Before you leave: - Please consider clapping and following the author! ? - Follow us on X, LinkedIn, YouTube, and Discord. - Explore our other platforms: In Plain English, CoFeed, Venture, and Cubed. - Find more content at Stackademic.com.