Modular clean architecture implementation. If you’re looking to start a new project and familiar with Redux I’d recommend The Composable Architecture (TCA). But if your team prefers/understands Clean Architecture this is a great way to enforce the boundaries between layers.
Each feature is broken out into it’s own modules, one for Infrastructure (if needed), one for Presentation, one for UI.
Domain is where feature modules overlap (i.e. Login Feature will know about User Feature), so it makes sense having this as a shared module which stores all of the abstract types (services, routes, etc), entities, and interactors.
App is also a common area where all of the wiring is done between modules, this is where the Routers are implemented and Dependencies are injected.
Legend:
💭 - Abstract (Protocol)
🏛️ - Concrete Implementation
🧰 - Module
🛠️ - Test
🎭 - Test Mock/Spy
Access is enforced using Swift Package Manager to ensure proper boundaries between Core and UI.
@Dependency Injection
Dependencies are injected in a similar fashion to @EnvironmentObject using @Dependency, however, @Dependency supports protocols, so it’s easier to mock.
@dynamicMemberLookup and Phantom Types
Models enforce type restrictions by leveraging @dynamicMemberLookup on top of Phantom Types. More information available here.
IdealCleanArchitecture
Modular clean architecture implementation. If you’re looking to start a new project and familiar with Redux I’d recommend The Composable Architecture (TCA). But if your team prefers/understands Clean Architecture this is a great way to enforce the boundaries between layers.
Each feature is broken out into it’s own modules, one for Infrastructure (if needed), one for Presentation, one for UI.
Domain is where feature modules overlap (i.e. Login Feature will know about User Feature), so it makes sense having this as a shared module which stores all of the abstract types (services, routes, etc), entities, and interactors.
App is also a common area where all of the wiring is done between modules, this is where the Routers are implemented and Dependencies are injected.
Legend:
Module structure:
Feature Example:
VIPER architecture style
Passing data back typically happens between Views in VIPER, however, setting the Presenter to our delegate allows us to test more effectively.
Concepts Employed
I have employed several concepts to make this code more readable/usable.
If you’d like to read more about the approach there is more information available here.
Swift Package Manager
Access is enforced using Swift Package Manager to ensure proper boundaries between Core and UI.
@Dependency
InjectionDependencies are injected in a similar fashion to
@EnvironmentObject
using@Dependency
, however,@Dependency
supports protocols, so it’s easier to mock.@dynamicMemberLookup
andPhantom
TypesModels enforce type restrictions by leveraging
@dynamicMemberLookup
on top ofPhantom Types
. More information available here.@propertyWrapper
StylingStyles are created using
@propertyWrapper
‘s. More information available here.@resultBuilder
@AutoLayoutBuilder
Constraints are created in a more Swifty way with support for nesting. More information available here.
Assert
TestingTests are written with a little DSL I wrote over
XCTAssert
. More information available here.Routers
I am using a slightly modified approach detailed by Cassius Pacheco which allows for composable routing. He wrote a great article here.