Skill: Feature
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.
Parameters
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.
File Structure
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
Conventions
- No
@Observableon minimal ViewModels (no observable state). Only add when ViewModel hasprivate(set) var. - No
anykeyword on internal protocol types. Only on public protocols from other modules (e.g.,any TrackerContractin Container). - No imports in ViewModel when all types are internal to the module.
- Deep link paths are scoped per host —
/listunderepisodehost is independent from/listundercharacterhost. - Deep links use path-based URLs — parameters are embedded in the path (e.g.,
challenge://character/detail/42), never as query items (?id=42). Useurl.pathComponentsfor parsing. - Tuist module uses
\(appName)string interpolation for target names. - Features always receive
HTTPClientContractas their network dependency — never specific clients likeGraphQLClientContract. The Container is responsible for creating specific clients (e.g.,GraphQLClient) internally from theHTTPClientContract. This keeps features decoupled from transport details. - Features that don't need networking only receive
tracker: any TrackerContract. - Features that need networking receive
httpClient: any HTTPClientContract, tracker: any TrackerContract.
Workflow
Step 1: Tuist Module
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 Challenge.xctestplan.
Step 2: Source Files
Create all source files from sources.md.
Order: IncomingNavigation → DeepLinkHandler → NavigatorContract → Navigator → TrackerContract → Tracker → Event → ViewModelContract → ViewModel → View → Container → Feature.
Step 3: Test Files
Create all test files from tests.md.
Order: Mocks → Stubs → Unit tests.
Step 4: App Integration
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:
- Add import:
import Challenge{Feature} - Add property:
private let {feature}Feature: {Feature}Feature - Initialize in
init:- Without networking:
{feature}Feature = {Feature}Feature(tracker: self.tracker) - With networking:
{feature}Feature = {Feature}Feature(httpClient: self.httpClient, tracker: self.tracker)
- Without networking:
- Add to
featuresarray
AppKit/Tests/Unit/AppContainerTests.swift — No changes needed (features is private, tested indirectly).
Step 5: Verify
xcodebuild test \
-workspace Challenge.xcworkspace \
-scheme "Challenge (Dev)" \
-testPlan Challenge \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=latest'
Extending the Feature
| 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 |