Swift and SwiftUI tutorials for Swift Developers

Best Project Structure for SwiftUI

Any iOS Developer who has transitioned from classic UIKit to the declarative paradigm knows the rules of the game have changed. At first, the promise was simple: less code, faster interfaces, and native reactivity. However, as applications grow, we face the same old monster: spaghetti code. If you are immersed in Swift programming, you’ve likely realized that poor organization can turn declarative magic into a maintenance nightmare.

In this article, we will break down step-by-step what the best project structure for SwiftUI is. You will learn how to organize your code in Xcode to build scalable, maintainable apps, and most importantly, ready to share logic and views across iOS, macOS, and watchOS using Swift.


The Paradigm Shift: Why SwiftUI Demands a New Organization

For years, Swift programming in Apple environments was dominated by the MVC (Model-View-Controller) pattern, often joked about as Massive View Controller. With the arrival of SwiftUI, the controller disappears in favor of a reactive, state-based approach.

Without controllers, developers often make the mistake of putting all business logic, network calls, and routing directly inside the views (View). This results in “Massive Views”. To avoid this, a modern iOS Developer must adopt architectures like MVVM (Model-View-ViewModel), Redux, or TCA (The Composable Architecture). Regardless of the exact pattern you choose, the best project structure for SwiftUI will always rely on the separation of concerns and modularity.


Feature-Driven Organization

In the past, it was common to structure projects in Xcode by grouping files by their technical type (Type-Driven):

  • Models/
  • Views/
  • ViewModels/
  • Services/

While this works for tiny projects, it’s a disaster for enterprise apps. If you need to modify the “Profile” screen, you have to jump between four different folders, searching for scattered files.

The best project structure for SwiftUI uses a Feature-Driven approach. Here, you group files according to the feature or user flow.

The Ideal Directory Tree

For a cross-platform project in Xcode that supports iOS, macOS, and watchOS, the folder-level structure should look like this:

MyApp/
│
├── App/
│   ├── MyApp.swift (Entry Point)
│   ├── AppEnvironment.swift
│   └── Configuration/
│
├── Core/
│   ├── Networking/
│   ├── Storage/
│   ├── Extensions/
│   └── DesignSystem/
│       ├── Colors.swift
│       ├── Typography.swift
│       └── Components/
│
├── Features/
│   ├── Authentication/
│   │   ├── Models/
│   │   ├── Views/
│   │   ├── ViewModels/
│   │   └── Services/
│   ├── Dashboard/
│   └── Profile/
│
├── Shared/
│   ├── Models/
│   └── Utilities/
│
├── Platforms/
│   ├── iOS/
│   │   ├── Info.plist
│   │   └── iOSSpecificView.swift
│   ├── macOS/
│   │   ├── Info.plist
│   │   └── macOSSpecificView.swift
│   └── watchOS/
│       ├── Info.plist
│       └── watchOSSpecificView.swift
│
└── Tests/
    ├── UnitTests/
    └── UITests/

Let’s break down each of these layers in depth and how they benefit your Swift programming workflow.


1. The App Layer: The Entry Point

The App folder contains the main configuration of your application. Every good iOS Developer knows that the entry point (@main) should be as clean as possible. In SwiftUI, this means your struct conforming to the App protocol.

There shouldn’t be any complex business logic here, just initial dependency injection and environment configuration.

import SwiftUI

@main
struct MyApp: App {
    // Global state or core services injection
    @StateObject private var appEnvironment = AppEnvironment()

    var body: some Scene {
        WindowGroup {
            AppCoordinatorView()
                .environmentObject(appEnvironment)
        }
    }
}

By isolating the AppEnvironment in this folder, you centralize the setup for analytics services, session managers, and global theme configurations.


2. The Core Layer: The Application Engine

The Core folder is where user interface-agnostic Swift programming lives. This is the technical heart of the project. Everything in Core should be usable by any feature of the app without creating circular dependencies.

Key Components of Core:

  • Networking: Your HTTP client. Here you define network protocols, interceptors, token management, and generic parsing with Codable.
  • Storage: Local database management (Core Data, SwiftData, or Realm) and UserDefaults.
  • DesignSystem: Crucial in SwiftUI. Instead of using scattered Color.blue or Font.system throughout your code, create your own reusable modifiers and components. This is fundamental to the best project structure for SwiftUI.

An example of a Design System component:

import SwiftUI

struct PrimaryButton: View {
    let title: String
    let action: () -> Void
    
    var body: some View {
        Button(action: action) {
            Text(title)
                .font(.headline)
                .foregroundColor(.white)
                .padding()
                .frame(maxWidth: .infinity)
                .background(Color.accentColor)
                .cornerRadius(10)
        }
    }
}

Keeping these visual elements in Core/DesignSystem guarantees visual consistency across iOS, macOS, and watchOS.


3. The Features Layer: The Functional Heart

This is where you will spend most of your time in Xcode. Each folder within Features is a micro-module. Think of Authentication or Dashboard as independent mini-apps.

If we adopt MVVM, a feature like authentication would be broken down like this:

  • Models: Pure Swift data structures (e.g., User, AuthToken).
  • Services: Protocols and classes that interact with Core/Networking to make feature-specific requests.
  • ViewModels: Observable classes that handle state and business logic. (Using @Observable in Swift 5.9+).
  • Views: Pure SwiftUI files that observe the ViewModel.

Why is this vital for an iOS Developer?

If tomorrow you decide the Authentication module needs to be rewritten, or if you’re going to extract it to an independent framework to share with another project, you simply grab the Authentication folder and move it. You don’t have dependencies tangled up in a global Views or ViewModels folder.


4. Cross-Platform Development: Shared and Platforms

One of the marvels of modern Swift programming and SwiftUI is “Learn once, apply anywhere”. However, an interface that works perfectly on the iPhone might look terrible on the Apple Watch screen or fail to utilize the space on a Mac.

The best project structure for SwiftUI tackles cross-platform development by separating device-specific UI from shared logic.

The Shared Folder

Here you will place Domain Models and Use Cases (or ViewModels if the logic is identical). The “Log In” logic (making the server call, saving the token) is exactly the same on iOS, macOS, and watchOS. 90% of the code in a good cross-platform Swift project should reside in Core, Features (logic), and Shared.

The Platforms Folder

Taking advantage of Xcode “Targets”, you can assign specific files to specific platforms. If you need a drastically different design, you can create separate views.

// In the Platforms/iOS/LoginView_iOS.swift folder
import SwiftUI

struct LoginView: View {
    @State private var viewModel = LoginViewModel()
    
    var body: some View {
        VStack {
            // Stacked form design optimized for touch screens
        }
    }
}

// In the Platforms/macOS/LoginView_macOS.swift folder
import SwiftUI

struct LoginView: View {
    @State private var viewModel = LoginViewModel()
    
    var body: some View {
        HStack {
            // Two-column design with image on the left and form on the right
        }
    }
}

By having the same view name (LoginView) in different Targets, the top navigation layer doesn’t need to know which platform it’s running on; it will compile the correct view based on the selected Target in Xcode.


The Next Level: Modularization with Swift Package Manager (SPM)

For large enterprise projects, structuring the project using folders (Groups) in Xcode is no longer enough. The state of the art for a Senior iOS Developer is dividing the application into multiple local Swift Packages using Swift Package Manager.

In the best project structure for SwiftUI, your Xcode App (the .xcodeproj) is just an empty shell.

Instead of folders, your project looks like this:

  • App Target: Only has the entry point and links the SPMs.
  • CorePackage: An SPM with the networking layer and shared extensions.
  • DesignSystemPackage: A purely visual SPM.
  • AuthFeaturePackage: An SPM with the login feature.

Benefits of Modularizing with SPM in Swift Programming:

  1. Drastically reduced build times: Xcode only recompiles the package you’re modifying, not the whole app.
  2. Prevention of hidden dependencies: In Swift packages, you must explicitly declare what imports what. It’s impossible for the Features layer to access something private in Core if it’s not publicly exposed (public).
  3. Native multi-platform support: SPM is designed to handle iOS, macOS, tvOS, and watchOS fluidly, allowing you to define in the Package.swift which code compiles for which platform.
  4. Ultra-fast SwiftUI Previews: Rendering a view in an isolated package takes fractions of a second compared to spinning up the environment of a giant monolith.

State Management and Data Flow

No SwiftUI tutorial is complete without talking about state. Folder structure is useless if the data flow is chaotic.

With the introduction of Swift 5.9, the @Observable macro revolutionized how we connect our ViewModels. In the best project structure for SwiftUI, dependency injection is done top-down using the Environment.

If you have services in your Core that need to be accessed by multiple Features, inject them at the root of your App:

@Observable
final class AppState {
    var isUserLoggedIn: Bool = false
    var userProfile: User? = nil
}

// In your App.swift
@main
struct MyApp: App {
    @State private var appState = AppState()

    var body: some Scene {
        WindowGroup {
            RootCoordinatorView()
                .environment(appState) // New Swift standard injection
        }
    }
}

Inside each Feature folder, views will consume this state safely. Keep reactivity contained. Avoid using @AppStorage or direct Singleton calls (e.g., NetworkManager.shared) deeply nested in views. Pass it via injection to maintain testability.


The Testing Strategy

Where do we put the tests in this structure? In Xcode, you will create testing Targets that mirror your main structure.

  • UnitTests: If you’ve followed this Feature-Driven architecture and extracted your logic into ViewModels and Services, testing Swift programming code is trivial. You can test the AuthViewModel logic without needing to instantiate SwiftUI.
  • UITests: These focus on complete flows. Here you test the visual integration of the views built in your folders.

If you use SPM for your modules, each package (e.g., AuthFeaturePackage) will have its own internal Tests folder, keeping the test code right next to the feature it is testing. This is a highly valued cohesion principle for any iOS Developer.


Summary of Best Practices

To ensure you maintain the best project structure for SwiftUI, keep these commandments in mind:

  1. Views are “dumb”: A SwiftUI view should only care about rendering UI and capturing user events. All branching logic (complex business ifs/elses) belongs in the ViewModel or Model.
  2. Embrace Swift Protocols: Instead of injecting concrete classes of your Services, inject protocols. This allows you to create instant Mocks for your Xcode Previews and unit tests.
  3. Isolate third-party dependencies: Never directly use third-party SDKs in your views (like Firebase or Alamofire). Create a wrapper in your Core layer. If one day you decide to change providers, you only modify one file in Core instead of dozens of views in Features.
  4. Prioritize clean code over UI “hacks”: If you notice you’re fighting the framework to achieve a specific cross-platform animation or design, take a step back. Often, separating approaches by platform in the Platforms folder is cleaner than having a file full of #if os(iOS) and #elseif os(macOS).

Conclusion

The Apple ecosystem evolves at a breakneck pace. Being a standout iOS Developer today isn’t just about knowing Swift programming syntax, but understanding how to design robust systems.

The best project structure for SwiftUI in Xcode is one that scales with you. By adopting a Feature-Driven approach, separating your logic layer (Core) from the visual layer (Features/Views), and embracing modularization with Swift Package Manager, you will be creating apps for iOS, macOS, and watchOS that are a joy to maintain, extend, and above all, test.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

What's New in SwiftUI for iOS 27

Related Posts