smithery/entaku0818

voilog-tca-development

Guide TCA (The Composable Architecture) development for VoiLog iOS app including @ObservableState, @ViewAction, delegate patterns, and TestStore testing. Use when implementing new features, modifying TCA reducers, writing tests, or asking about VoiLog architecture patterns.

Installation

$ npx skills add smithery/entaku0818 --skill voilog-tca-development

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/entaku0818.

npx skills add smithery/entaku0818

Browse all from smithery/entaku0818

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Skill metadata

Parsed from SKILL.md frontmatter.

Version1.1.0
More metadata
author
VoiLog Team
version
1.1.0
category
development
tags
["tca","swiftui","ios","voilog","testing"]
updated
2026-02-01

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 9,335 B
  • docs SUMMARY.md 304 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

VoiLog TCA Development

RULE: テストは必ず書く
新機能・バグ修正・リファクタリングのいずれでも、実装と同時に TestStore テストを作成すること。テストなしの実装は完了とみなさない。

Quick Start

VoiLog uses modern TCA patterns with @ObservableState and @ViewAction.

Feature Template Location

Start with: /ios/VoiLog/Template/FeatureTemplate.swift

Modern Architecture Files

Primary locations (PREFERRED for modifications):

  • Recording: /ios/VoiLog/Recording/RecordingFeature.swift
  • Playback: /ios/VoiLog/Playback/PlaybackFeature.swift
  • App Coordinator: /ios/VoiLog/DebugMode/VoiceAppFeature.swift (VoiceAppFeature)

Legacy location (AVOID modifications):

  • /ios/VoiLog/Voice/ - Legacy production code

Instructions

Step 1: Choose the Right Architecture

For new features or modifications:

  1. Use Modern Architecture (/ios/VoiLog/Recording, /Playback, /DebugMode)
  2. Follow the tab-based pattern with VoiceAppFeature
  3. Implement delegate pattern for inter-feature communication

Never modify:

  • Files in /ios/VoiLog/Voice/ directory (Legacy)

Step 2: Create New Feature

@Reducer
struct MyFeature {
  @ObservableState
  struct State: Equatable {
    var isLoading = false
    // Add your state properties
  }

  enum Action: ViewAction {
    case view(View)
    case delegate(Delegate)

    enum View {
      case onAppear
      case buttonTapped
    }

    enum Delegate {
      case completed(Result)
    }
  }

  var body: some ReducerOf<Self> {
    Reduce { state, action in
      switch action {
      case .view(.onAppear):
        // Implementation
        return .none

      case .view(.buttonTapped):
        // Send delegate action to parent
        return .send(.delegate(.completed(.success)))

      case .delegate:
        return .none
      }
    }
  }
}

Step 3: Integrate with VoiceAppFeature

For features that need tab switching:

// In VoiceAppFeature
case .recording(.delegate(.recordingCompleted)):
  state.selectedTab = 1 // Switch to playback tab
  return .send(.playback(.reloadData))

Step 4: Write Tests

See "Testing Patterns" section below for comprehensive test examples.

Testing Patterns

Basic TestStore Structure

import ComposableArchitecture
import Testing

@Test
func testBasicAction() async {
  let store = TestStore(initialState: MyFeature.State()) {
    MyFeature()
  }

  // Send action and verify state changes
  await store.send(.view(.buttonTapped)) {
    $0.isLoading = true
  }

  // Verify delegate action received
  await store.receive(.delegate(.completed))
}

Pattern 1: ViewAction → State Change

@Test
func testStateChange() async {
  let store = TestStore(initialState: RecordingFeature.State()) {
    RecordingFeature()
  }

  await store.send(.view(.recordButtonTapped)) {
    $0.recordingState = .recording
    $0.isRecordButtonEnabled = false
  }
}

Pattern 2: ViewAction → Delegate Notification

@Test
func testDelegateNotification() async {
  let store = TestStore(
    initialState: RecordingFeature.State(recordingState: .recording)
  ) {
    RecordingFeature()
  }

  await store.send(.view(.stopButtonTapped)) {
    $0.recordingState = .stopped
  }

  await store.receive(.delegate(.recordingCompleted))
}

Pattern 3: Testing with Dependencies

@Test
func testWithRepository() async {
  let mockMemo = VoiceMemo(
    id: UUID(),
    title: "Test Memo",
    duration: 60.0
  )

  let store = TestStore(initialState: PlaybackFeature.State()) {
    PlaybackFeature()
  } withDependencies: {
    $0.voiceMemoRepository = .mock(
      fetchAll: { [mockMemo] }
    )
  }

  await store.send(.view(.onAppear))
  await store.receive(.memosLoaded([mockMemo])) {
    $0.memos = [mockMemo]
  }
}

Pattern 4: Async Operations

@Test
func testAsyncOperation() async {
  let store = TestStore(initialState: MyFeature.State()) {
    MyFeature()
  }

  await store.send(.view(.loadData)) {
    $0.isLoading = true
  }

  await store.receive(.dataLoadComplete(result: .success(data))) {
    $0.isLoading = false
    $0.data = data
  }
}

Pattern 5: Timer and Debounce

@Test
func testWithTimer() async {
  let clock = TestClock()

  let store = TestStore(initialState: MyFeature.State()) {
    MyFeature()
  } withDependencies: {
    $0.continuousClock = clock
  }

  await store.send(.view(.startTimer)) {
    $0.isTimerRunning = true
  }

  await clock.advance(by: .seconds(1))
  await store.receive(.timerTicked) {
    $0.elapsedTime = 1
  }
}

Pattern 6: Error Handling

@Test
func testErrorHandling() async {
  let store = TestStore(initialState: MyFeature.State()) {
    MyFeature()
  } withDependencies: {
    $0.voiceMemoRepository = .mock(
      fetchAll: { throw TestError.networkError }
    )
  }

  await store.send(.view(.loadData))
  await store.receive(.loadFailed(TestError.networkError)) {
    $0.errorMessage = "Network error"
  }
}

Test Execution Commands

Run all tests

xcodebuild test \
  -project ios/VoiLog.xcodeproj \
  -scheme VoiLogTests \
  -destination 'platform=iOS Simulator,name=iPhone 15'

Run specific test class

xcodebuild test \
  -project ios/VoiLog.xcodeproj \
  -scheme VoiLogTests \
  -destination 'platform=iOS Simulator,name=iPhone 15' \
  -only-testing:VoiLogTests/PlaylistListFeatureTests

Common Test Mistakes

Mistake 1: Missing State Verification

// ❌ Bad: State changes but not verified
await store.send(.view(.buttonTapped))

// ✅ Good: All state changes verified
await store.send(.view(.buttonTapped)) {
  $0.isLoading = true
  $0.buttonTitle = "Loading..."
}

Mistake 2: Missing Action Reception

// ❌ Bad: Delegate action sent but not verified
await store.send(.view(.complete))

// ✅ Good: All received actions verified
await store.send(.view(.complete))
await store.receive(.delegate(.completed))

Mistake 3: Wrong Order

// ❌ Bad: Wrong reception order
await store.receive(.second)
await store.receive(.first)

// ✅ Good: Correct order
await store.receive(.first)
await store.receive(.second)

Common Patterns

Pattern 1: Repository Integration

@Dependency(\.voiceMemoRepository) var repository

// In reducer
case .view(.saveData):
  return .run { [data = state.data] send in
    try await repository.save(data)
    await send(.delegate(.saved))
  }

Pattern 2: Auto Tab Switching

See /ios/VoiLog/DebugMode/VoiceAppFeature.swift for reference:

  • Recording completion → Auto switch to playback tab
  • Use selectedTab state
  • Send reloadData action to target tab

Troubleshooting

Error: State not updating in View

Cause: Forgot @ObservableState macro Solution: Add @ObservableState to State struct

Error: Action not recognized

Cause: Using old WithViewStore pattern Solution: Use @ViewAction pattern instead

Test Error: "Expected state to change but it did not"

Cause: State mutation block provided but state didn't change Solution: Either remove the mutation block or ensure state actually changes

// ❌ Bad: Expects state change but none occurs
await store.send(.view(.noOp)) {
  $0.value = newValue  // But reducer doesn't change this
}

// ✅ Good: No mutation block if no state change
await store.send(.view(.noOp))

Test Error: "Received unexpected action"

Cause: Reducer sent an action that wasn't verified in test Solution: Add await store.receive() for all emitted actions

// ❌ Bad: Missing receive
await store.send(.view(.load))
// Test fails: Unexpected .loadComplete action

// ✅ Good: Verify all received actions
await store.send(.view(.load))
await store.receive(.loadComplete)

Test Error: "TestStore skipped receiving actions"

Cause: Actions received in wrong order or not at all Solution: Verify actions in the exact order they're emitted

// ❌ Bad: Wrong order
await store.receive(.second)
await store.receive(.first)

// ✅ Good: Correct order
await store.receive(.first)
await store.receive(.second)

Test hangs or times out

Cause: Waiting for an action that never comes Solution:

  1. Verify the reducer actually sends the expected action
  2. Check if async operations completed
  3. Use store.exhaustivity = .off for debugging (not recommended for final tests)

References

For detailed examples: