ios-swift
/

iOS Project Structure – Organizing Large-Scale Swift Apps

Last Sync: Today

On this page

9
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

ios-swift

iOS Project Structure – Organizing Large-Scale Swift Apps

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.

  1. High-Level Folder Layout

At the root level, separate your code into these main groups. This works for both SwiftUI and UIKit projects.

TEXTRead-only
1
MyApp/
├── App/
├── Core/
├── Features/
├── Shared/
├── Services/
├── Resources/
├── Networking/
├── Persistence/
├── Utilities/
├── Extensions/
└── Tests/

  1. Detailed Breakdown by Layer

Each top-level folder has a specific responsibility. Below is the complete tree with explanations.

TEXTRead-only
1
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)

  1. Clean Architecture in Practice

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).

SWIFTRead-only
1
// Domain Layer (protocol)
protocol AuthRepositoryProtocol {
    func login(email: String, password: String) async throws -> User
}

// Data Layer (implements protocol)
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()
    }
}

// Presentation Layer (uses protocol, not concrete type)
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)
            // Handle success
        } catch {
            // Handle error
        }
    }
}

  1. Module vs Feature vs Layer

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/

  1. Xcode Groups vs Folder References

| 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).

  1. Naming Conventions

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

  1. Project Configuration Files

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

  1. Common Anti-Patterns to Avoid

❌ 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.

Test Your Knowledge

Q1
of 4

Which layer in Clean Architecture should have NO dependencies on UIKit, SwiftUI, or networking frameworks?

A
Presentation
B
Domain
C
Data
D
Services
Q2
of 4

What color are Xcode groups (logical folders) in the project navigator?

A
Blue
B
Yellow
C
Gray
D
Purple
Q3
of 4

In a feature module following Clean Architecture, where should the repository protocol be defined?

A
Presentation layer
B
Data layer
C
Domain layer
D
Shared layer
Q4
of 4

Which of these is an anti-pattern in iOS project structure?

A
Using a Shared folder for reusable components
B
Creating a 5000-line Utils.swift file
C
Organizing by feature instead of by type
D
Using dependency injection via initializers

Frequently Asked Questions

Should I use groups or folder references for Swift files?

Always use groups (yellow folders) for Swift code. Xcode automatically maps them to the filesystem, but you can organize logically without mirroring disk structure. Use folder references (blue) only for resource bundles (fonts, HTML, JSON) that need to preserve their folder hierarchy at runtime.

How do I decide when to create a new feature module?

Create a new feature folder when:

  • The screen has its own distinct responsibility (e.g., Login vs Dashboard).
  • It has at least 3-5 files (View, ViewModel, UseCase, Repository).
  • It can be developed independently by a different team member.
  • It has minimal coupling to other features (except through Core).
What's the difference between Shared, Core, and Utilities?
  • Core: Base classes, protocols, DI containers – defines your architecture.
  • Shared: UI components, themes, cross-feature logic – used by multiple features.
  • Utilities: Pure functions with no side effects (crypto, math, formatters) – no dependencies on the app.
How do I handle dependency injection with this structure?

Use a container per feature. Create AppContainer.swift in Core/DI that registers all global services. Each feature can have its own assembler. In SwiftUI, pass dependencies via @EnvironmentObject or custom initializers. For UIKit, use constructor injection in ViewControllers. Avoid Service Locator anti-pattern.

Should I use Swift Packages for modularization?

Previous

ios introduction

Next

ios app lifecycle

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.