Skip to content

Project layout

Every folder in a generated project, what lives there, and why.

This page describes the full layout produced by swiftspawn new, file by file.

MyApp/
├── MyApp.xcodeproj # real Xcode project (Xcode 26 format)
├── Package.swift # SPM manifest, with auto-managed dependency markers
├── Package.resolved # SPM lockfile
├── .swiftspawn.yml # CLI's project config
├── .swiftlint.yml # SwiftLint config
├── .swiftformat # SwiftFormat config
├── .gitignore
├── README.md
├── Configs/ # build configurations
│ ├── Debug.xcconfig
│ ├── Staging.xcconfig
│ └── Release.xcconfig
├── MyApp/ # source tree
└── Tests/ # test tree (created on first --with-tests)

MyApp.xcodeproj is a real Xcode 26 project, not a generator output that needs further processing. It uses PBXFileSystemSynchronizedRootGroup, which means new files added on disk show up in Xcode automatically without editing pbxproj.

.swiftspawn.yml is the CLI’s source of truth for the project name, bundle id, deployment target, and similar config. Subsequent swiftspawn runs use it; never delete it.

MyApp/
├── App/
│ ├── MyAppApp.swift # @main entry, hosts RootView
│ ├── RootView.swift # NavigationStack + route switch (markers)
│ ├── HelloView.swift # placeholder so the app launches
│ └── DIContainer.swift # Factory container (markers)
├── Infrastructure/
│ ├── Networking/
│ │ ├── APIClient.swift # URLSession-based client, interceptor chain
│ │ ├── APIRequest.swift # typed request value
│ │ ├── APIConfig.swift # base URL, default headers, auth strategy
│ │ ├── AppError.swift # the app's error type
│ │ ├── AuthInterceptor.swift # adds auth header to outgoing requests
│ │ └── LoggingInterceptor.swift # DEBUG-only request/response logs
│ ├── Routing/
│ │ ├── Route.swift # the Route enum (markers)
│ │ └── Router.swift # @Observable wrapping NavigationPath
│ ├── Auth/
│ │ └── TokenStore.swift # actor holding the auth token
│ └── Logging/
│ └── Log.swift # namespace over os.Logger
├── Resources/
│ ├── Assets.xcassets/
│ └── Info.plist
├── Features/ # one folder per feature
│ └── Components/ # views shared across features
└── Services/ # one trio per resource (flat)

The application entry point and root navigation.

  • MyAppApp.swift: @main struct. Creates the shared Router and DIContainer, wraps RootView in a WindowGroup.
  • RootView.swift: a NavigationStack bound to Router.path, with a switch over Route between markers. New screens get a case here automatically.
  • HelloView.swift: a placeholder so a freshly scaffolded project launches without errors. Delete it when you have real screens.
  • DIContainer.swift: the Factory Container extension. Service registrations live here between markers.

The pieces every app shares. Small enough to read in an afternoon.

  • Networking is a URLSession-backed APIClient with an interceptor chain. Interceptors get a chance to mutate outgoing requests and incoming responses; AuthInterceptor injects the token, LoggingInterceptor logs both.
  • Routing is an enum + an @Observable wrapper over NavigationPath. Each new screen adds one case to the enum and one arm to the root switch.
  • Auth is just a TokenStore actor. Replace its body with Keychain access in production.
  • Logging is a namespace with one logger per category over os.Logger. Filter Console.app by your bundle id and category to read the trace.

Standard SwiftUI resources: Assets.xcassets for images and colors, Info.plist for app metadata.

One folder per feature. Empty after new; populated by generate feature and generate screen.

A typical feature looks like:

Features/Movies/
├── Models/
│ ├── Movie.swift
│ └── Genre.swift
├── MovieList/
│ ├── MovieListView.swift
│ └── MovieListViewModel.swift
├── MovieDetail/
│ ├── MovieDetailView.swift
│ └── MovieDetailViewModel.swift
├── MovieEdit/
│ ├── MovieEditView.swift
│ └── MovieEditViewModel.swift
└── MoviePosterView.swift # standalone view (no subfolder)

Screens get subfolders, views don’t. See Vocabulary.

The default home for generate view <Name> when no --in is given. Use it for views shared across features (avatars, badges, error banners).

Flat. One trio per resource: protocol, implementation, mock.

Services/
├── MovieService.swift # protocol
├── MovieServiceImpl.swift # real, calls APIClient
├── MockMovieService.swift # canned responses for tests/previews
├── UserService.swift
├── UserServiceImpl.swift
└── MockUserService.swift

Why flat? Services are referred to from many features; nesting them inside one feature folder hides them from the others. The CRUD recipe and generate service always write here.

Tests/
└── MyAppTests/
├── Features/
│ └── Movies/
│ └── MovieList/
│ └── MovieListViewModelTests.swift
└── Services/
└── MovieServiceTests.swift

Tests mirror the source layout. --with-tests on any generator emits the matching test stub.

Three xcconfigs corresponding to three build configurations:

  • Debug.xcconfig: local development. Logging on, base URL points to dev.
  • Staging.xcconfig: pre-production. Logging on, base URL points to staging.
  • Release.xcconfig: production. Logging off, base URL points to prod.

APIConfig.swift reads these via Bundle.main.infoDictionary to pick the right base URL and auth at runtime.