Feature-marker ios-workflow
iOS-specific workflow activated automatically when platform-context.json reports primary_platform=ios. Handles toolchain verification, Swift Testing patterns, XcodeBuildMCP integration, and SwiftLint.
git clone https://github.com/Viniciuscarvalho/Feature-marker
T=$(mktemp -d) && git clone --depth=1 https://github.com/Viniciuscarvalho/Feature-marker "$T" && mkdir -p ~/.claude/skills && cp -r "$T/feature-marker-dist/feature-marker/resources/spec-workflow/skills/ios-workflow" ~/.claude/skills/viniciuscarvalho-feature-marker-ios-workflow && rm -rf "$T"
feature-marker-dist/feature-marker/resources/spec-workflow/skills/ios-workflow/SKILL.mdiOS Workflow
Activated automatically when the Platform Detection Engine identifies an iOS/Swift project. Never activate this manually in non-iOS projects.
Phase 1: Toolchain verification
When iOS is detected, check and report tool availability:
🍎 iOS project detected. Checking toolchain... ✅ swift — found (Swift 6.0.3) ✅ xcodebuild — found (Xcode 16.2) ✅ swiftlint — found (0.57.0) ✅ XcodeBuildMCP — found (~/.claude/skills/xcodebuildmcp/SKILL.md) ⚠️ xcbeautify — not found (optional: brew install xcbeautify) iOS toolchain ready.
Checks to perform:
→ report Swift versionswift --version
→ report Xcode versionxcodebuild -version
→ report version or "not found"command -v swiftlint && swiftlint version
→ XcodeBuildMCP presencels ~/.claude/skills/xcodebuildmcp/SKILL.md 2>/dev/null
→ optional, just reportcommand -v xcbeautify
None of these checks are blockers — missing tools generate warnings, not errors.
Phase 2: Test Framework Detection
Determine whether the project uses Swift Testing or XCTest:
# Count test files using each framework swift_testing_count=$(grep -r "import Testing" . --include="*.swift" -l 2>/dev/null | wc -l) xctest_count=$(grep -r "import XCTest" . --include="*.swift" -l 2>/dev/null | wc -l)
ANDswift_testing_count > 0
→ use Swift Testingxctest_count == 0
ANDxctest_count > 0
→ use XCTest (legacy)swift_testing_count == 0- Both present → use Swift Testing for new tests, keep existing XCTest tests
- None found → default to Swift Testing (modern standard)
Report to user:
🧪 Test framework: Swift Testing (@Test, @Suite, #expect)
or:
🧪 Test framework: XCTest (existing project uses XCTest — maintaining consistency)
Phase 3: iOS Test Pipeline
Execute in this exact order:
Step 1 — Unit tests (swift test or xcodebuild)
# Option A: Swift Package Manager project swift test --parallel # Option B: Xcode project/workspace (if .xcodeproj/.xcworkspace found) xcodebuild test \ -scheme {detected_scheme} \ -destination 'platform=iOS Simulator,name=iPhone 16' \ -resultBundlePath .claude/feature-state/{slug}/test-results.xcresult \ | xcbeautify 2>/dev/null || cat
Detect which to use: if
Package.swift exists without .xcodeproj → use swift test.
Step 2 — SwiftLint (files modified by this feature only)
Run SwiftLint only on modified files — not the whole project:
# Get files modified since the feature branch started modified_files=$(git diff --name-only HEAD~1 -- "*.swift" 2>/dev/null) # Run swiftlint on those files only (if swiftlint is available) if command -v swiftlint > /dev/null 2>&1 && [ -n "$modified_files" ]; then swiftlint lint $modified_files --reporter json > .claude/feature-state/{slug}/swiftlint.json swiftlint lint $modified_files # human-readable output fi
If swiftlint not found: skip with
⚠️ swiftlint not found — skipping lint.
Step 3 — XcodeBuildMCP build + simulator (optional, non-blocking)
Only if
capabilities.xcodebuildmcp_available == true in platform-context.json:
1. /xcodebuildmcp discover_projs → find .xcodeproj or .xcworkspace 2. /xcodebuildmcp session_set_defaults → configure scheme, destination, configuration 3. /xcodebuildmcp build_run_sim → build + launch on iOS Simulator → capture build output and simulator status
Result reporting:
✅ App running on iPhone 16 Simulator (iOS 18.3)
or:
⚠️ Simulator build failed: [error summary] — tests passed, continuing to commit
XcodeBuildMCP failure is non-blocking. Unit tests passing is sufficient for the workflow.
Swift Testing patterns for iOS
When generating new tests (Test Only mode or per-task test generation), always use Swift Testing — never XCTest — unless the project exclusively uses XCTest.
Required imports and structure
import Testing @testable import {ModuleName} @Suite("ViewModel or UseCase Name") struct PersonalTrainerViewModelTests { @Test("describe the behavior, not the method name") func loadsTrainerOnAppear() async throws { // Arrange let viewModel = PersonalTrainerViewModel( discoverTrainersUseCase: MockDiscoverTrainersUseCase(), featureFlagChecker: MockFeatureFlagChecker(enabled: true) ) // Act await viewModel.onAppear() // Assert #expect(viewModel.isFeatureEnabled == true) #expect(viewModel.currentTrainer != nil) } @Test("does not load trainer when feature flag is disabled") func doesNotLoadWhenDisabled() async { let viewModel = PersonalTrainerViewModel( featureFlagChecker: MockFeatureFlagChecker(enabled: false) ) await viewModel.onAppear() #expect(viewModel.currentTrainer == nil) } // Parameterized test @Test("requestConnection returns true for valid trainer IDs", arguments: ["trainer-alpha", "trainer-beta", "trainer-gamma"]) func requestConnectionSucceeds(trainerId: String) async { let viewModel = PersonalTrainerViewModel( connectTrainerUseCase: MockConnectTrainerUseCase(shouldSucceed: true) ) let result = await viewModel.requestConnection(trainerId: trainerId) #expect(result == true) } }
Rules for iOS tests
- Use
to group related tests (one suite per ViewModel/UseCase/Repository)@Suite - Use
— describe what it does, not the method@Test("behavior description") - Use
for assertions — never#expect(condition)XCTAssert - Use
to unwrap optionals that must exist#require(value) - Use
for tests that can throw,throws
for async testsasync throws - Use
for data-driven tests (replaces XCTestarguments:
)parametrize - Use
trait for categorization:.tags()@Test("...", .tags(.unit, .viewModel)) - Never use
,XCTest
, orXCTAssert*class FooTests: XCTestCase
File structure for iOS tests
Follow the existing project convention. If no convention exists, use:
{ProjectName}/ └── {ProjectName}Tests/ ├── Domain/ │ └── UseCases/ │ └── {UseCase}Tests.swift ← one file per use case ├── Data/ │ ├── Repositories/ │ │ └── {Repository}Tests.swift │ └── Services/ │ └── {Service}Tests.swift └── Presentation/ └── Features/ └── {Feature}/ └── {Feature}ViewModelTests.swift
Test file naming
- One test file per production file:
→PersonalTrainerViewModel.swiftPersonalTrainerViewModelTests.swift - Place in mirror directory under
{ProjectName}Tests/
Test Only mode — iOS
When
--mode test-only is invoked on an iOS project:
- Detect test framework (Swift Testing vs XCTest)
- Find Swift files without corresponding test files:
find . -name "*.swift" \ ! -name "*Tests.swift" \ ! -name "*Mock*" \ ! -path "*/Tests/*" \ -type f 2>/dev/null - For each file without tests, check if it's testable (ViewModels, UseCases, Repositories, Services)
- Present to user:
Found 3 files without test coverage: - Presentation/Features/PersonalTrainer/PersonalTrainerViewModel.swift - Domain/UseCases/ConnectTrainerUseCase.swift - Data/Repositories/TrainerRepository.swift Generate Swift Testing tests for these files? [yes/no/select] - Generate tests following Swift Testing patterns above
- Run
swift test --parallel - If XcodeBuildMCP available: optionally run build on simulator
- Report coverage delta
Configuration in platform-context.json
The iOS entry in
platform-context.json includes:
{ "type": "ios", "test_framework": "swift-testing", "capabilities": { "swiftlint_available": true, "xcodebuildmcp_available": true, "swift_testing_available": true } }
This configuration is ignored completely on non-iOS projects.