Files
SportstimeAPI/.planning/phases/02-constraint-validation/CONSTRAINT-API.md
Trey t 73ed3150ed docs(02-02): document ItineraryConstraints API for Phase 4
Document public API for drag-drop integration:
- isValidPosition() for position validation
- validDayRange() for precomputing valid days
- barrierGames() for visual highlighting
- Integration patterns for ItineraryTableViewController

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 14:54:28 -06:00

4.5 KiB

ItineraryConstraints API

Location: SportsTime/Core/Models/Domain/ItineraryConstraints.swift Verified by: 22 tests in SportsTimeTests/Domain/ItineraryConstraintsTests.swift

Overview

ItineraryConstraints validates item positions during drag-drop operations. It enforces:

  • Games cannot move (CONS-01)
  • Travel segments have day range limits (CONS-02)
  • Travel segments must respect game sortOrder on same day (CONS-03)
  • Custom items have no constraints (CONS-04)

Construction

let constraints = ItineraryConstraints(
    tripDayCount: days.count,
    items: allItineraryItems  // All items including games
)

Parameters:

  • tripDayCount: Total days in trip (1-indexed, so a 5-day trip has days 1-5)
  • items: All itinerary items (games, travel, custom). Games are used to calculate constraints for travel items.

Public API

isValidPosition(for:day:sortOrder:) -> Bool

Check if a specific position is valid for an item.

func isValidPosition(for item: ItineraryItem, day: Int, sortOrder: Double) -> Bool

Usage during drag:

// On each drag position update
let dropPosition = calculateDropPosition(at: touchLocation)
let isValid = constraints.isValidPosition(
    for: draggedItem,
    day: dropPosition.day,
    sortOrder: dropPosition.sortOrder
)

if isValid {
    showValidDropIndicator()
} else {
    showInvalidDropIndicator()
}

Returns:

  • true: Position is valid, allow drop
  • false: Position is invalid, reject drop (snap back)

Rules by item type:

Item Type Day Constraint SortOrder Constraint
.game Always false Always false
.travel Within valid day range After departure games, before arrival games
.custom Any day 1...tripDayCount Any sortOrder

validDayRange(for:) -> ClosedRange<Int>?

Get the valid day range for a travel item (for visual feedback).

func validDayRange(for item: ItineraryItem) -> ClosedRange<Int>?

Usage at drag start:

// When drag begins, precompute valid range
guard case .travel = draggedItem.kind,
      let validRange = constraints.validDayRange(for: draggedItem) else {
    // Not a travel item or impossible constraints
    return
}

// Use range to dim invalid days
for day in 1...tripDayCount {
    if !validRange.contains(day) {
        dimDay(day)
    }
}

Returns:

  • ClosedRange<Int>: Valid day range (e.g., 2...4)
  • nil: Constraints are impossible (e.g., departure game after arrival game)

barrierGames(for:) -> [ItineraryItem]

Get games that constrain a travel item (for visual highlighting).

func barrierGames(for item: ItineraryItem) -> [ItineraryItem]

Usage for visual feedback:

// Highlight barrier games during drag
let barriers = constraints.barrierGames(for: travelItem)
for barrier in barriers {
    highlightAsBarrier(barrier)  // e.g., gold border
}

Returns:

  • Array of game items: Last departure city game + first arrival city game
  • Empty array: Not a travel item or no constraining games

Integration Points

ItineraryTableViewController (existing)

// In reloadData()
self.constraints = ItineraryConstraints(tripDayCount: tripDayCount, items: itineraryItems)

// In drag handling
if constraints.isValidPosition(for: draggedItem, day: targetDay, sortOrder: targetSortOrder) {
    // Allow drop
} else {
    // Reject drop, snap back
}

Phase 4 Implementation Notes

  1. Drag Start:

    • Check item.isReorderable (games return false)
    • Call validDayRange(for:) to precompute valid days
    • Call barrierGames(for:) to identify visual barriers
  2. Drag Move:

    • Calculate target (day, sortOrder) from touch position
    • Call isValidPosition(for:day:sortOrder:) for real-time feedback
    • Update insertion line (valid) or red indicator (invalid)
  3. Drag End:

    • Final isValidPosition(for:day:sortOrder:) check
    • Valid: Update item's day/sortOrder, animate settle
    • Invalid: Animate snap back, haptic feedback

Test Coverage

Requirement Tests Verified
CONS-01 (games cannot move) 2 Yes
CONS-02 (travel day range) 3 Yes
CONS-03 (travel sortOrder) 5 Yes
CONS-04 (custom flexibility) 2 Yes
Edge cases 8 Yes
Success criteria 3 Yes
Barrier games 1 Yes
Total 22 100%

API documented: Phase 02 Ready for: Phase 04 (Drag Interaction)