DEV Community

How to Structure a SwiftUI Project in 2026

How to Structure a SwiftUI Project in 2026

Starting a new SwiftUI project? The default Xcode structure works for tutorials, but real apps need something better. Here's what I use after building multiple production apps.

The Problem

Xcode gives you ContentView.swift and that's it. As your app grows, you end up with:

  • 50 files in one folder
  • ViewModels mixed with Views
  • No clear separation of concerns
  • Pain every time you need to find something

The Solution: Feature-Based Architecture

MyApp/
├── App/
│   ├── MyAppApp.swift
│   ├── AppDelegate.swift (if needed)
│   └── AppConfiguration.swift
├── Features/
│   ├── Auth/
│   │   ├── Views/
│   │   │   ├── LoginView.swift
│   │   │   └── RegisterView.swift
│   │   ├── ViewModels/
│   │   │   └── AuthViewModel.swift
│   │   └── Models/
│   │       └── User.swift
│   ├── Home/
│   │   ├── Views/
│   │   ├── ViewModels/
│   │   └── Models/
│   └── Profile/
│       ├── Views/
│       ├── ViewModels/
│       └── Models/
├── Core/
│   ├── Network/
│   │   ├── NetworkManager.swift
│   │   ├── APIEndpoint.swift
│   │   └── APIError.swift
│   ├── Storage/
│   │   ├── StorageManager.swift
│   │   └── KeychainManager.swift
│   └── Extensions/
│       ├── View+Extensions.swift
│       └── String+Extensions.swift
├── UI/
│   ├── Components/
│   │   ├── PrimaryButton.swift
│   │   ├── LoadingView.swift
│   │   └── ErrorView.swift
│   └── Theme/
│       ├── Colors.swift
│       ├── Fonts.swift
│       └── Spacing.swift
└── Resources/
    ├── Assets.xcassets
    └── Localizable.strings
Enter fullscreen mode Exit fullscreen mode

Why This Works

1. Features are Self-Contained

Each feature has everything it needs:

  • Views (the UI)
  • ViewModels (the logic)
  • Models (the data)

Adding a new feature? Create a new folder. Removing one? Delete the folder.

2. Core is Shared Logic

Things used across features:

  • Networking
  • Storage
  • Extensions

One place to maintain. Used everywhere.

3. UI is Reusable

Components and theming that every feature uses:

// UI/Theme/Colors.swift
import SwiftUI

enum AppColors {
    static let primary = Color("Primary")
    static let secondary = Color("Secondary")
    static let background = Color("Background")
    static let text = Color("Text")
}
Enter fullscreen mode Exit fullscreen mode
// UI/Components/PrimaryButton.swift
struct PrimaryButton: View {
    let title: String
    let action: () -> Void

    var body: some View {
        Button(action: action) {
            Text(title)
                .font(.headline)
                .foregroundColor(.white)
                .frame(maxWidth: .infinity)
                .padding()
                .background(AppColors.primary)
                .cornerRadius(12)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

ViewModel Pattern

Every feature's ViewModel follows the same pattern:

@MainActor
final class HomeViewModel: ObservableObject {
    // MARK: - Published State
    @Published private(set) var items: [Item] = []
    @Published private(set) var isLoading = false
    @Published private(set) var error: Error?

    // MARK: - Dependencies
    private let networkManager: NetworkManager

    init(networkManager: NetworkManager = .shared) {
        self.networkManager = networkManager
    }

    // MARK: - Public Methods
    func loadItems() async {
        isLoading = true
        error = nil

        do {
            items = try await networkManager.fetch(ItemsEndpoint())
        } catch {
            self.error = error
        }

        isLoading = false
    }
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • @MainActor ensures UI updates on main thread
  • private(set) prevents external mutation
  • Dependency injection for testing

Navigation

For simple apps, use SwiftUI's native navigation:

NavigationStack {
    HomeView()
        .navigationDestination(for: Item.self) { item in
            DetailView(item: item)
        }
}
Enter fullscreen mode Exit fullscreen mode

For complex apps, consider a Coordinator:

@MainActor
final class AppCoordinator: ObservableObject {
    @Published var path = NavigationPath()

    enum Destination: Hashable {
        case detail(Item)
        case settings
        case profile
    }

    func navigate(to destination: Destination) {
        path.append(destination)
    }

    func pop() {
        path.removeLast()
    }

    func popToRoot() {
        path.removeLast(path.count)
    }
}
Enter fullscreen mode Exit fullscreen mode

Quick Setup Script

Create this structure automatically:

#!/bin/bash
mkdir -p App
mkdir -p Features/{Auth,Home,Profile}/{Views,ViewModels,Models}
mkdir -p Core/{Network,Storage,Extensions}
mkdir -p UI/{Components,Theme}
mkdir -p Resources
Enter fullscreen mode Exit fullscreen mode

My Full Template

I've packaged all of this (plus actual code) into a starter template:

  • 5 ready screens
  • 20+ UI components
  • NetworkManager with async/await
  • Dark mode support
  • MVVM architecture

Check it out at boosty.to/swiftuidev if you want to skip the setup.


TL;DR

  1. Feature-based structure — group by feature, not file type
  2. Core — shared utilities
  3. UI — reusable components and theming
  4. MVVM — ViewModels with @MainActor
  5. Be consistent — same pattern everywhere

What's your project structure? Let me know in the comments.


Follow me for more SwiftUI content:

Top comments (0)