Clean Architecture SwiftUI Demo

repository·master·Indexed 27 days ago

https://github.com/nalexn/clean-architecture-swiftui

A demo project showcasing a SwiftUI application built with Clean Architecture principles and Combine. It implements a decoupled architecture consisting of a Presentation Layer (SwiftUI views), a Business Logic Layer (Interactors), and a Data Access Layer (Repositories using SwiftData). The project demonstrates state management via a centralized AppState and dependency injection using the @Environment property wrapper.

Tokens
1.1K
Snippets
2
Records
5
Agent score
42%

What's inside clean-architecture-swiftui

  1. Architecture Overview of Clean Architecture for SwiftUI

    master

    This project implements a decoupled architecture consisting of three distinct layers to ensure separation of concerns:

    1. Presentation Layer: Composed of SwiftUI views that contain no business logic. Views are a function of the state and trigger side effects via user actions or lifecycle events (like onAppear), which are then forwarded to Interactors.
    2. Business Logic Layer: Represented by Interactors. Interactors receive requests to perform work (e.g., fetching data or computations). They do not return data directly; instead, they update the AppState (the single source of truth) or provide data via a Binding for local view use.
    3. Data Access Layer: Represented by Repositories. Repositories provide asynchronous APIs (using Combine Publisher) for CRUD operations on backends or local databases (like SwiftData). They contain no business logic and do not mutate the AppState. They are only accessible by Interactors.
  2. Choose between AppState and Binding for Data Flow

    master

    When deciding how to handle data returned by an Interactor, follow these patterns:

    • Use AppState: When the data is small or needs to be globally accessible (e.g., storing a user's last used login email in UserDefaults). The data is loaded into AppState at launch and updated by the Interactor.
    • Use Binding: When dealing with massive amounts of data stored in a local database (like SwiftData). This allows the Interactor to serve data on an on-demand basis to a specific view without bloating the global AppState.
  3. Manage State and Dependency Injection in SwiftUI

    master
    The architecture uses a Redux-like centralized AppState as the single source of truth. To integrate the Business Logic Layer into your SwiftUI view hierarchy, use native SwiftUI dependency injection via the @Environment property wrapper. This allows views to access the AppState and Interactors without manual prop drilling.
  4. Configure the application root view via AppEnvironment

    master

    The AppEnvironment.rootView property defines the application's view hierarchy and dependency injection setup. It applies several modifiers to the CountriesList view to ensure the Clean Architecture layers are correctly wired:

    • .modifier(RootViewAppearance()): Applies standard UI appearance.
    • .modelContainer(modelContainer): Sets up the persistence layer.
    • .attachEnvironmentOverrides(onChange: onChangeHandler): Manages environment changes.
    • .inject(diContainer): Injects the Dependency Injection container into the view hierarchy.

    If isRunningTests is true, it displays a testing indicator instead of the main UI.

    extension AppEnvironment {
        var rootView: some View {
            VStack {
                if isRunningTests {
                    Text("Running unit tests")
                } else {
                    CountriesList()
                        .modifier(RootViewAppearance())
                        .modelContainer(modelContainer)
                        .attachEnvironmentOverrides(onChange: onChangeHandler)
                        .inject(diContainer)
                    if modelContainer.isStub {
                        Text("⚠️ There is an issue with local database")
                            .font(.caption2)
                    }
                }
            }
        }
    }
  5. Initialize the application entry point with MainApp

    master

    The MainApp struct serves as the @main entry point for the application. It uses @UIApplicationDelegateAdaptor(AppDelegate.self) to manage the application lifecycle and provides the rootView via the AppEnvironment extension to populate the WindowGroup.

    @main
    struct MainApp: App {
        @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
        var body: some Scene {
            WindowGroup {
                appDelegate.rootView
            }
        }
    }