Creates a new feature module with minimal viable structure. Use when bootstrapping a new feature from scratch, scaffolding the Tuist module, Container, Feature entry point, DeepLinkHandler, and initial screen with placeholder Text view. Includes all unit tests, mocks, stubs, and app integration. For adding domain/data layers afterward, use /datasource, /repository, /usecase. For enhancing views, use /view, /viewmodel, /navigator.
Create a new feature module with the minimum viable structure: Tuist module, Feature entry point, Container, DeepLinkHandler, and one screen with placeholder Text. Integrates into the app.
Gather from user before starting:
| Parameter | Format | Example |
|---|---|---|
| Feature | PascalCase | Episode |
| Screen | PascalCase | EpisodeList |
| Deep link host | lowercase | episode |
| Deep link path segment | lowercase, no slash | list |
Derived: module name = Challenge{Feature}, event prefix = snake_case of Screen.
Features/{Feature}/
├── Sources/
│ ├── {Feature}Feature.swift
│ ├── {Feature}Container.swift
│ └── Presentation/
│ ├── Navigation/
│ │ ├── {Feature}IncomingNavigation.swift
│ │ └── {Feature}DeepLinkHandler.swift
│ └── {Screen}/
│ ├── Navigator/
│ │ ├── {Screen}NavigatorContract.swift
│ │ └── {Screen}Navigator.swift
│ ├── Tracker/
│ │ ├── {Screen}TrackerContract.swift
│ │ ├── {Screen}Tracker.swift
│ │ └── {Screen}Event.swift
│ ├── ViewModels/
│ │ ├── {Screen}ViewModelContract.swift
│ │ └── {Screen}ViewModel.swift
│ └── Views/
│ └── {Screen}View.swift
└── Tests/
├── Unit/
│ ├── Feature/
│ │ └── {Feature}FeatureTests.swift
│ └── Presentation/
│ ├── Navigation/
│ │ └── {Feature}DeepLinkHandlerTests.swift
│ └── {Screen}/
│ ├── Navigator/
│ │ └── {Screen}NavigatorTests.swift
│ ├── Tracker/
│ │ ├── {Screen}TrackerTests.swift
│ │ └── {Screen}EventTests.swift
│ └── ViewModels/
│ └── {Screen}ViewModelTests.swift
└── Shared/
├── Mocks/
│ ├── {Screen}NavigatorMock.swift
│ └── {Screen}TrackerMock.swift
└── Stubs/
└── {Screen}ViewModelStub.swift
@Observable on minimal ViewModels (no observable state). Only add when ViewModel has private(set) var.some View (not some ViewModelContract). The Container creates both the ViewModel and wraps it in the View. Features call container.make{Screen}View(navigator:) and wrap in AnyView.any keyword on internal protocol types. Only on public protocols from other modules (e.g., any TrackerContract in Container)./list under episode host is independent from /list under character host.challenge://character/detail/42), never as query items (?id=42). Use url.pathComponents for parsing.\(appName) string interpolation for target names.HTTPClientContract as their network dependency — never specific clients like GraphQLClientContract. The Container is responsible for creating specific clients (e.g., GraphQLClient) internally from the HTTPClientContract. This keeps features decoupled from transport details.tracker: any TrackerContract.httpClient: any HTTPClientContract, tracker: any TrackerContract.Create Tuist/ProjectDescriptionHelpers/Modules/{Feature}Module.swift:
import ProjectDescription
public let {feature}Module = Module.create(directory: "Features/{Feature}")
Register the module in Modules.swift — Add {feature}Module to the Modules.all array. This single registration automatically includes the module's package in the root project and its test target in the Dev scheme (Framework: via testableTargets, SPM: via Challenge.xctestplan).
Create all source files from sources.md.
Order: IncomingNavigation → DeepLinkHandler → NavigatorContract → Navigator → TrackerContract → Tracker → Event → ViewModelContract → ViewModel → View → Container → Feature.
Create all test files from tests.md.
Order: Mocks → Stubs → Unit tests.
Wire the feature into the app — 4 files to modify:
Tuist/ProjectDescriptionHelpers/Modules/AppKitModule.swift — Add dependency:
{feature}Module.targetDependency,
AppKit/Sources/AppContainer.swift — Three changes:
import Challenge{Feature}private let {feature}Feature: {Feature}Featureinit:
{feature}Feature = {Feature}Feature(tracker: self.tracker){feature}Feature = {Feature}Feature(httpClient: self.httpClient, tracker: self.tracker)features arrayAppKit/Tests/Unit/AppContainerTests.swift — No changes needed (features is private, tested indirectly).
xcodebuild test \
-workspace Challenge.xcworkspace \
-scheme "Challenge (Dev)" \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.1'
| Need | Skill |
|---|---|
| REST API data source | /datasource |
| Repository + DTO mapping | /repository |
| Business logic | /usecase |
| Enhance ViewModel with state | /viewmodel |
| Enhance View with design system | /view |
| Add more navigation | /navigator |
| Snapshot tests | /snapshot |
| UI tests | /ui-tests |