Clean Architecture: Projektstruktur und Ebenen
In einem professionellen iOS-Projekt ist die Ordnerstruktur deine erste Verteidigungslinie gegen „Spaghetti-Code“. Während die Kreise der Clean Architecture konzeptionell sind, muss deine Struktur im Xcode-Projekt oder im Swift Package (SPM) physisch vorhanden sein und strikt durchgesetzt werden.
1. Das Mapping: Theorie vs. Ordner
| Clean-Ebene | Ordnername | Inhalt | Abhängigkeitsregel |
|---|---|---|---|
| Entities | Domain/Entities |
Reine Swift-Structs (z. B. Translation). |
Keine. Pures Swift. |
| Use Cases | Domain/UseCases |
Logik-Protokolle und Interaktoren. | Nur Entities. |
| Gateways | Domain/Interfaces |
Repository-Protokolle (z. B. TranslationRepository). |
Nur Entities. |
| Data Layer | Data/Repositories |
Konkrete Repository-Implementierungen. | Domain-Ebene. |
| Infrastructure | Data/DataSources |
API-Clients, Core Data Manager, DTOs. | Externe Frameworks (Alamofire, CoreData). |
| Presentation | Presentation/UI |
SwiftUI Views, ViewModels, Coordinators. | Domain-Ebene. |
2. Die empfohlene Projektstruktur
Für ein Projekt mit deiner Komplexität (Bibel-App, Core Data, mehrere Sprachen) ist ein modularisierter SPM-Ansatz ideal. Wenn du ein einzelnes Target nutzt, verwende diese Hierarchie:
/App (Der Composition Root)
├── DependencyInjection/ (Swinject oder custom Container)
└── AppLauncher.swift (App-Struktur)
/Domain (Das "Gehirn")
├── Entities/ (Translation.swift, Verse.swift)
├── UseCases/ (FetchVersesUseCase.swift, BookmarkVerseUseCase.swift)
└── Interfaces/ (TranslationRepositoryProtocol.swift)
/Data (The "Arbeiter")
├── Repositories/ (TranslationRepository.swift)
├── DataSources/
│ ├── Remote/ (APIClient.swift, TranslationDTO.swift)
│ └── Local/ (CoreDataStack.swift, CDTranslation+Mapping.swift)
└── Mappers/ (DTOToEntityMapper.swift)
/Presentation (Das "Gesicht")
├── Scenes/ (Die UI-Screens)
│ ├── TranslationList/
│ │ ├── TranslationListView.swift
│ │ └── TranslationListViewModel.swift
│ └── TranslationDetail/ ...
├── Components/ (Wiederverwendbare SwiftUI Views)
└── Coordinator/ (Navigationslogik)
3. Der Abhängigkeitsfluss (Die "Rule of Inward")
Die wichtigste Erkenntnis für einen Senior-Entwickler ist, dass Abhängigkeiten nur nach innen zeigen dürfen.
-
Domain ist König: Sie weiß nichts über SwiftUI, Core Data oder Firebase. Wenn du deine Domain-Unit-Tests nicht ohne
UIKitausführen kannst, ist deine Architektur „undicht“ (Leaky Architecture). -
Die Repository-Brücke: Die Data-Ebene implementiert die in der Domain definierten Protokolle.
- Domain: „Ich brauche einen Weg, um Übersetzungen zu erhalten.“ (Protokoll)
- Data: „Ich hole sie aus Core Data.“ (Implementierung)
- Die ViewModel-Brücke: Das ViewModel ruft den UseCase auf, der eine Entity zurückgibt. Das ViewModel konvertiert diese Entity dann in ein ViewItem (ein für die UI optimiertes Struct).
4. Warum Mappers nutzen? (Data vs. Domain)
In deiner App hat ein Core Data-Objekt oft Altlasten (Optionals, NSManagedObject-Logik). Mappers halten die Domain sauber.
-
CoreDataTranslation(Data Layer): An Core Data gebunden, optionale Strings. -
Translation(Domain Layer): Sauber, nicht-optional, ein immutables Struct.
Die Lösung: Erstelle in Data/Mappers eine einfache Extension:
extension CoreDataTranslation{
func toDomain() -> Translation {
// Mapping von DB-Modell zu sauberem Domain-Modell
return Translation(
id: self.id ?? UUID().uuidString,
name: self.name ?? "Unbekannt"
)
}
}
5. Senior-Strategie: Modularisierung (SPM)
Bei über 5 Jahren Erfahrung ist es ratsam, diese Ebenen in separate Swift Packages auszulagern:
-
DomainPackage: Null Abhängigkeiten. Extrem schnell zu testen. -
DataPackage: Hängt nur von der Domain ab. -
UIPackage: Hängt nur von der Domain ab.
Der Vorteil: Es verhindert physisch, dass jemand versehentlich ein Datenbank-Modell in eine View importiert. Der Compiler würde den Dienst verweigern.
Top comments (0)