How to Structure a SwiftUI Project in 2026

Published: (February 1, 2026 at 08:00 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

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

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")
}
// UI/Components/PrimaryButton.swift
import SwiftUI

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

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
    }
}

Key points

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

Simple apps – native SwiftUI navigation

NavigationStack {
    HomeView()
        .navigationDestination(for: Item.self) { item in
            DetailView(item: item)
        }
}

Complex apps – Coordinator pattern

@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)
    }
}

Quick Setup Script

Create the 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

My Full Template

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

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

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

TL;DR

  • Feature‑based structure – group by feature, not by file type.
  • Core – shared utilities.
  • UI – reusable components and theming.
  • MVVM – ViewModels with @MainActor.
  • Be consistent – apply the same pattern everywhere.
Back to Blog

Related posts

Read more »

[SUI] Barra de búsqueda

Barra de búsqueda en NavigationStack Un NavigationStack puede incluir una barra de búsqueda mediante el modificador searchable. Su firma es: swift searchable t...

SwiftUI Isn't Slow — Your Code Is

SwiftUI Rendering: What’s Fast and What’s Slow SwiftUI’s rendering engine is fast. What slows it down is the work you ask it to redo hundreds of times per seco...