Phase 2c: Round List TDD Session

Date: 2025-11-23 Duration: ~6 hours PR: #291 (MERGED) Approach: Test-Driven Development (RED-GREEN-REFACTOR)


Quick Summary

Implemented iOS Round List feature with tab-based filtering using strict TDD methodology. Session yielded three critical technical discoveries that will impact all future iOS KMP development.

Key Achievements:

  • ✅ 13 comprehensive tests (100% passing)
  • ✅ RoundListViewModel with reactive Combine architecture
  • ✅ RoundListView with SwiftUI tabs
  • ✅ Fixed iOS test infrastructure issues
  • ✅ 3 reusable KMP patterns documented

Critical Discoveries

1. KMP Enum Comparison Pattern ⚠️ CRITICAL

Problem: Kotlin enums are reference types in Swift, not value types.

// ❌ FAILS - Compares object identity
let filtered = rounds.filter { $0.status == .inProgress }
 
// ✅ WORKS - Compares enum names
let filtered = rounds.filter { $0.status.name == RoundStatus.inProgress.name }

Why: Kotlin enum class maps to Swift as reference type classes. Each case is a singleton instance. The == operator compares references, not values. Use .name property for value comparison.

Impact: Affects ALL enum comparisons across iOS KMP integration.

Where to Use:

  • Filtering collections by enum
  • Conditionals checking enum values
  • Switch statements on KMP enums
  • Any equality check between KMP enums

2. Framework Search Paths for Test Targets ⚠️ CRITICAL

Problem: Test targets with CocoaPods + custom frameworks need explicit path inheritance.

<!-- ❌ BROKEN - Test can't find CocoaPods frameworks -->
"FRAMEWORK_SEARCH_PATHS[arch=*]" = (
    "$(SRCROOT)/../../shared/presentation/...",
);
 
<!-- ✅ FIXED - Inherits CocoaPods paths -->
"FRAMEWORK_SEARCH_PATHS[arch=*]" = (
    "$(inherited)",  // MUST BE FIRST
    "$(SRCROOT)/../../shared/presentation/...",
);

Why: Architecture-specific settings ([arch=*]) override base settings completely. Without $(inherited), test target ignores CocoaPods xcconfig framework paths.

Impact: Without this, test targets can’t use ANY CocoaPods dependencies.

File: iosApp/ArcheryApprentice/ArcheryApprentice.xcodeproj/project.pbxproj


3. User Script Sandboxing for Test Targets

Problem: Xcode 15+ sandboxes build scripts, breaking CocoaPods resource copying.

<!-- Test target only -->
ENABLE_USER_SCRIPT_SANDBOXING = NO;

Alternative: Remove pods with resource bundles from test target if not needed.

# Podfile
target 'ArcheryApprenticeTests' do
  inherit! :search_paths
  pod 'FirebaseAuth'      # No resources ✅
  pod 'FirebaseCore'      # No resources ✅
  # pod 'GoogleSignIn'    # HAS resources ❌
end

Implementation Details

RoundListViewModel (233 lines)

Architecture:

  • Repository protocol for testing
  • Reactive Combine-based filtering
  • Tab-based status filters
  • Loading and error states
  • Real-time count calculations

Key Pattern - Reactive Filtering:

private func setupObservers() {
    $rounds
        .combineLatest($selectedTabIndex)
        .sink { [weak self] rounds, tabIndex in
            self?.updateFilteredRounds()
        }
        .store(in: &cancellables)
}

Critical Code - Enum Comparison:

// File: RoundListViewModel.swift:131
return rounds.filter { $0.status.name == status.name }.count
 
// File: RoundListViewModel.swift:160
filteredRounds = rounds.filter { $0.status.name == status.name }

RoundListView (390 lines)

UI Components:

  • Tab bar with 5 filters (All, In Progress, Completed, Paused, Planned)
  • Round cards with details (name, date, distance, score, status)
  • Loading state (centered spinner)
  • Error state (message + retry button)
  • Empty states (context-aware messages)
  • Pull-to-refresh

State Management:

if viewModel.isLoading {
    LoadingView()
} else if let error = viewModel.errorMessage {
    ErrorView(message: error, onRetry: { Task { await viewModel.refresh() } })
} else if viewModel.filteredRounds.isEmpty {
    EmptyStateView(selectedTab: viewModel.selectedTabIndex)
} else {
    RoundListContent(rounds: viewModel.filteredRounds)
}

Test Suite (370 lines, 13 tests)

Test Categories:

  1. Initialization (3 tests) - Clean starting state
  2. Load Rounds (3 tests) - Repository integration
  3. Filter by Status (3 tests) - Tab filtering logic
  4. Tab Counts (1 test) - Real-time calculations
  5. Empty States (2 tests) - No data handling
  6. Error Handling (2 tests) - Error management
  7. Refresh (1 test) - Pull-to-refresh

Mock Repository Pattern:

class MockRoundRepository: RoundRepositoryProtocol {
    var roundsToReturn: [Round] = []
    var shouldFail = false
    var errorToThrow: Error?
    var simulateDelay = false
 
    func getAllRounds() async throws -> [Round] {
        if simulateDelay {
            try? await Task.sleep(nanoseconds: 500_000_000)
        }
        if shouldFail {
            throw errorToThrow ?? NSError(domain: "MockError", code: 500)
        }
        return roundsToReturn
    }
}

TDD Journey

Phase 1: RED - Write Failing Tests

Created 13 tests defining expected behavior:

  • Initialization tests
  • Repository integration tests
  • Filtering logic tests
  • Count calculation tests
  • Empty state tests
  • Error handling tests
  • Refresh tests

Phase 2: GREEN - Minimal Implementation

Implemented RoundListViewModel to pass tests.

Discovered: KMP enum comparison issue causing filter tests to fail.

Fixed: Changed == to .name comparison.

Phase 3: REFACTOR - Infrastructure Issues

Problem: Tests wouldn’t compile - linker couldn’t find Firebase frameworks.

Investigation:

  1. Checked CocoaPods xcconfig - paths present ✅
  2. Compared main vs test target settings
  3. Found missing $(inherited) in test target

Fixed: Added $(inherited) to FRAMEWORK_SEARCH_PATHS[arch=*]

Additional Issues:

  • User script sandboxing blocking CocoaPods
  • GoogleSignIn resource bundles conflicting

Phase 4: GREEN (Post-Fix) - All Tests Passing

13/13 tests passing (0.611 seconds total)

Phase 5: UI Implementation

Built SwiftUI views using tested ViewModel.

Phase 6: Copilot Review

Addressed 6 review comments:

  • Removed debug statements
  • Fixed SF Symbol names
  • Added code coverage config
  • Improved error handling
  • Enhanced build validation

Files Changed

Added:

  • RoundListViewModel.swift (233 lines)
  • RoundListView.swift (390 lines)
  • RoundListViewModelTests.swift (370 lines)
  • ArcheryApprentice.xctestplan (24 lines)

Modified:

  • project.pbxproj (+109/-56 lines)
  • Podfile (+34/-11 lines)

Total: 7 files, +1,184 lines, -67 lines


Lessons Learned

1. TDD Catches Issues Early

Test-first approach discovered:

  • KMP enum comparison issue (before UI)
  • Framework search paths issue (during test setup)
  • Mock repository pattern enabled fast iteration

Result: No UI rework needed - issues found and fixed during test phase.

2. KMP Type Bridging Needs Documentation

Kotlin → Swift type mappings are non-obvious:

  • Enums become reference types
  • Int becomes Int32
  • Long becomes Int64
  • List<T> becomes [KotlinT]

Action: Added patterns to CLAUDE.md for future reference.

3. Test Target Configuration Requires Care

Test targets don’t automatically inherit main target settings:

  • Framework search paths need explicit $(inherited)
  • User script sandboxing may need disabling
  • Pod dependencies may differ from main target

Best Practice: Configure test target immediately when adding pods/frameworks.


Next Steps

Immediate:

  1. Wire RoundListView into app navigation
  2. Implement Round Detail View (tap on round card)
  3. Connect to real repository (replace mock)

Short-Term:

  1. Phase 2d: Settings Screen
  2. Phase 2e: Active Scoring Screen
  3. Phase 2f: Round Details with Statistics

Long-Term:

  1. Phase 3: Equipment Management (95% shared code)
  2. Phase 5: Authentication (complete deferred work)

References

Documentation:

  • Full session details: docs/IOS_PHASE_2C_ROUND_LIST_SESSION.md (main repo)
  • KMP patterns: docs/CLAUDE.md - iOS KMP Development Patterns section (main repo)
  • Phase 2 roadmap: roadmap
  • Testing strategy: testing-strategy

Code:

  • ViewModel: iosApp/ArcheryApprentice/ArcheryApprentice/ViewModels/RoundListViewModel.swift
  • View: iosApp/ArcheryApprentice/ArcheryApprentice/Views/RoundListView.swift
  • Tests: iosApp/ArcheryApprentice/ArcheryApprenticeTests/RoundListViewModelTests.swift
  • Project config: iosApp/ArcheryApprentice/ArcheryApprentice.xcodeproj/project.pbxproj

Pull Request: #291 - feat(ios): Phase 2c - Round List with TDD


Key Takeaways

TDD works excellently for iOS KMP development

  • Catches type bridging issues early
  • Forces thinking about architecture
  • Provides safety net for refactoring

KMP enum comparison is critical

  • Must use .name property
  • Affects all enum filtering/comparison
  • Pattern now documented

Test infrastructure needs explicit configuration

  • $(inherited) required for framework paths
  • User script sandboxing may need disabling
  • Test targets are not automatic copies of main target

Copilot review adds production quality

  • Caught debug code
  • Improved error handling
  • Added code coverage tracking

Session completed: 2025-11-23 PR #291: MERGED Phase 2c: COMPLETE