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>
165 lines
4.5 KiB
Markdown
165 lines
4.5 KiB
Markdown
# 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
|
|
|
|
```swift
|
|
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.
|
|
|
|
```swift
|
|
func isValidPosition(for item: ItineraryItem, day: Int, sortOrder: Double) -> Bool
|
|
```
|
|
|
|
**Usage during drag:**
|
|
```swift
|
|
// 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).
|
|
|
|
```swift
|
|
func validDayRange(for item: ItineraryItem) -> ClosedRange<Int>?
|
|
```
|
|
|
|
**Usage at drag start:**
|
|
```swift
|
|
// 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).
|
|
|
|
```swift
|
|
func barrierGames(for item: ItineraryItem) -> [ItineraryItem]
|
|
```
|
|
|
|
**Usage for visual feedback:**
|
|
```swift
|
|
// 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)
|
|
|
|
```swift
|
|
// 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)*
|