Why Project Structure Matters
A poorly organized iOS project leads to merge conflicts, slow compilation, and developer frustration. A good structure is feature-based, layered, and testable. This guide presents a battle-tested structure for SwiftUI + UIKit apps using Clean Architecture principles, suitable for teams of 2–20 developers.
At the root level, separate your code into these main groups. This works for both SwiftUI and UIKit projects.
MyApp/
├── App/
├── Core/
├── Features/
├── Shared/
├── Services/
├── Resources/
├── Networking/
├── Persistence/
├── Utilities/
├── Extensions/
└── Tests/
Each top-level folder has a specific responsibility. Below is the complete tree with explanations.
MyApp/
│
├── App/ # App lifecycle & entry point
│ ├── MyAppApp.swift # @main for SwiftUI
│ ├── AppDelegate.swift # UIKit lifecycle (if needed)
│ ├── SceneDelegate.swift
│ └── Info.plist
│
├── Core/ # Core architecture & base classes
│ ├── Architecture/
│ │ ├── BaseView.swift
│ │ ├── BaseViewModel.swift
│ │ ├── BaseViewController.swift # UIKit
│ │ └── CoordinatorProtocol.swift
│ ├── Routing/
│ │ ├── AppRouter.swift
│ │ ├── Route.swift
│ │ └── DeepLinkHandler.swift
│ └── DI/ # Dependency Injection
│ ├── AppContainer.swift
│ ├── ServiceLocator.swift
│ └── Assemblies/
│
├── Features/ # Feature modules (grouped by domain)
│ ├── Auth/
│ │ ├── Presentation/
│ │ │ ├── Views/
│ │ │ │ ├── LoginView.swift
│ │ │ │ └── SignUpView.swift
│ │ │ ├── ViewModels/
│ │ │ │ └── AuthViewModel.swift
│ │ │ └── Components/
│ │ │ └── AuthTextField.swift
│ │ ├── Domain/
│ │ │ ├── Entities/
│ │ │ │ └── User.swift
│ │ │ ├── UseCases/
│ │ │ │ ├── LoginUseCase.swift
│ │ │ │ └── SignUpUseCase.swift
│ │ │ └── Repositories/
│ │ │ └── AuthRepositoryProtocol.swift
│ │ └── Data/
│ │ ├── Repositories/
│ │ │ └── AuthRepositoryImpl.swift
│ │ ├── DataSources/
│ │ │ ├── Remote/
│ │ │ │ └── AuthAPI.swift
│ │ │ └── Local/
│ │ │ └── AuthLocalStorage.swift
│ │ └── DTOs/
│ │ └── AuthResponseDTO.swift
│ │
│ ├── Dashboard/
│ │ ├── Presentation/
│ │ ├── Domain/
│ │ └── Data/
│ │
│ └── Profile/
│ ├── Presentation/
│ ├── Domain/
│ └── Data/
│
├── Shared/ # Cross-feature UI & logic
│ ├── UI/
│ │ ├── Components/
│ │ │ ├── PrimaryButton.swift
│ │ │ ├── LoadingView.swift
│ │ │ └── ErrorView.swift
│ │ ├── Modifiers/
│ │ │ └── CardStyle.swift
│ │ └── Themes/
│ │ ├── Color+Extensions.swift
│ │ ├── Font+Extensions.swift
│ │ └── AppConstants.swift
│ ├── Utils/
│ │ ├── Validators/
│ │ │ └── EmailValidator.swift
│ │ ├── Formatters/
│ │ │ └── DateFormatter+Custom.swift
│ │ └── Helpers/
│ │ └── KeychainWrapper.swift
│ └── Extensions/
│ ├── String+Extensions.swift
│ ├── View+Extensions.swift
│ └── Date+Extensions.swift
│
├── Services/ # Global services
│ ├── Analytics/
│ │ ├── AnalyticsService.swift
│ │ └── AnalyticsEvent.swift
│ ├── Notification/
│ │ └── PushNotificationService.swift
│ ├── Logging/
│ │ └── LoggerService.swift
│ └── FeatureFlags/
│ └── FeatureFlagService.swift
│
├── Networking/ # API layer
│ ├── APIClient.swift
│ ├── Endpoints.swift
│ ├── HTTPMethod.swift
│ ├── Middleware/
│ │ ├── AuthInterceptor.swift
│ │ └── LoggingInterceptor.swift
│ └── Mock/
│ └── MockAPIClient.swift
│
├── Persistence/ # Local data storage
│ ├── CoreData/
│ │ ├── PersistenceController.swift
│ │ └── Models/
│ ├── SwiftData/
│ │ └── ModelContainer.swift
│ ├── UserDefaults/
│ │ └── UserDefaults+Keys.swift
│ └── FileManager/
│ └── FileStorageService.swift
│
├── Resources/ # Assets & configuration
│ ├── Assets.xcassets
│ ├── Localization/
│ │ ├── Localizable.strings
│ │ ├── Localizable.stringsdict
│ │ └── en.lproj/
│ ├── Fonts/
│ │ └── CustomFont.ttf
│ ├── Config/
│ │ ├── Development.plist
│ │ ├── Staging.plist
│ │ └── Production.plist
│ └── GoogleService-Info.plist
│
├── Utilities/ # Pure logic helpers (no side effects)
│ ├── Crypto/
│ │ └── AESCrypto.swift
│ ├── Math/
│ │ └── PercentageCalculator.swift
│ └── Performance/
│ └── Debouncer.swift
│
├── Extensions/ # Global Swift extensions
│ ├── Swift/
│ │ ├── Array+Safe.swift
│ │ ├── Dictionary+Merge.swift
│ │ └── Optional+Unwrap.swift
│ ├── UIKit/
│ │ ├── UIViewController+Alert.swift
│ │ └── UIColor+Hex.swift
│ └── SwiftUI/
│ ├── Color+Hex.swift
│ └── View+Conditional.swift
│
└── Tests/ # Test targets
├── UnitTests/
│ ├── Domain/
│ ├── UseCases/
│ └── Utilities/
├── IntegrationTests/
│ ├── Networking/
│ └── Persistence/
├── UITests/
│ └── Features/
└── Mocks/
├── MockRepositories/
└── MockServices/
# Xcode Project Groups (same hierarchy but note:)
# - Yellow folders = Group (logical, no physical folder)
# - Blue folders = Folder Reference (physical folder, preserves structure)
Inside each Feature/ module, we follow Clean Architecture with three layers:
- Presentation (UI + ViewModels) – depends only on Domain.
- Domain (Business logic + Entities) – NO dependencies on UIKit, SwiftUI, or networking. Pure Swift.
- Data (Repositories + DataSources) – implements Domain protocols.
Direction of dependencies: Presentation → Domain ← Data (Dependency Inversion via protocols).
protocol AuthRepositoryProtocol {
func login(email: String, password: String) async throws -> User
}
class AuthRepositoryImpl: AuthRepositoryProtocol {
let api: AuthAPI
func login(email: String, password: String) async throws -> User {
let dto = try await api.login(request: .init(email: email, password: password))
return dto.toDomain()
}
}
class LoginViewModel: ObservableObject {
@Published var isLoading = false
private let authRepo: AuthRepositoryProtocol
init(authRepo: AuthRepositoryProtocol) {
self.authRepo = authRepo
}
func login(email: String, password: String) async {
isLoading = true
defer { isLoading = false }
do {
let user = try await authRepo.login(email: email, password: password)
} catch {
}
}
}
For large apps (10+ screens, 5+ developers), consider breaking into Swift packages or Xcode projects:
| Level | When to use | Example |
|-------|-------------|---------|
| Feature Module | Screens that work together | Auth, Checkout, Settings |
| Shared Module | Code reused across features | UI Components, Networking, Core |
| App Module | Glues everything together | Main app target |
Modular structure example:
MyApp.xcworkspace/
├── App/ (main app target)
├── Modules/
│ ├── CorePackage/ (Swift package)
│ ├── NetworkingPackage/
│ ├── UIPackage/
│ ├── FeatureAuth/
│ ├── FeatureDashboard/
│ └── FeatureProfile/
└── Tests/
| Feature | Group (Yellow) | Folder Reference (Blue) |
|---------|---------------|------------------------|
| Physical folder | Optional | Required |
| File organization | Logical only | Mirrors disk |
| Nested resources | ❌ Won't compile | ✅ Works for resources |
| When to use | Swift code files | Assets, JSON, HTML, Bundled files |
Best practice: Use Groups for .swift files. Use Folder References for resource folders (fonts, JSON, HTML, videos).
Consistent naming prevents confusion:
- View:
LoginView.swift, ProfileHeaderView.swift
- ViewModel:
LoginViewModel.swift (SwiftUI), LoginViewController.swift + LoginViewModel.swift (UIKit + MVVM)
- UseCase:
LoginUseCase.swift, FetchProductsUseCase.swift
- Repository Protocol:
AuthRepositoryProtocol.swift
- Repository Impl:
AuthRepositoryImpl.swift
- DTO:
LoginRequestDTO.swift, UserResponseDTO.swift
- Service:
AnalyticsService.swift, PushNotificationService.swift
- Coordinator:
AuthCoordinator.swift
- Factory:
ViewModelFactory.swift
Keep configuration at the root of your repo (not inside Xcode project):
MyApp/
├── .swiftlint.yml # Linting rules
├── .gitignore
├── fastlane/
│ └── Fastfile # CI/CD automation
├── project.yml # XcodeGen (optional but recommended)
├── Tuist/
│ └── ProjectDescriptionHelpers/
├── Makefile # Common tasks (test, lint, format)
├── README.md
└── config/
├── Debug.xcconfig
├── Release.xcconfig
└── Shared.xcconfig
❌ Massive View Controller – Move logic to ViewModels or UseCases.
❌ God Helpers – SharedUtils.swift with 5000 lines. Break into small, focused files.
❌ Circular Dependencies – Feature A imports Feature B, and B imports A. Use shared Core module.
❌ Deep Nesting – Features/Auth/Presentation/Views/Subviews/Components/Button.swift – Keep max 4 levels.
❌ Ignoring Tests – Mirror your main structure under Tests/ from day one.
❌ Mixing UI Frameworks – Avoid UIKit + SwiftUI in same feature without clear boundary.