docs: complete project research for itinerary editor
Files: - STACK.md - UITableView drag-drop APIs, SwiftUI bridging patterns - FEATURES.md - Table stakes UX (lift, insertion line, haptics), polish features - ARCHITECTURE.md - 5-layer semantic positioning architecture - PITFALLS.md - Critical pitfalls (row vs semantic, travel as structural) - SUMMARY.md - Executive synthesis with roadmap implications Key findings: - Stack: UITableView + UIHostingConfiguration (existing pattern validated) - Architecture: Semantic (day, sortOrder) model, not row indices - Critical pitfall: Row indices are ephemeral; semantic positions are truth Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
291
.planning/research/STACK.md
Normal file
291
.planning/research/STACK.md
Normal file
@@ -0,0 +1,291 @@
|
||||
# Stack Research: UITableView Drag-Drop for Itinerary Editor
|
||||
|
||||
**Project:** SportsTime Itinerary Editor
|
||||
**Researched:** 2026-01-18
|
||||
**Overall Confidence:** HIGH (existing implementation in codebase + stable APIs)
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The SportsTime codebase already contains a production-quality UITableView drag-drop implementation in `ItineraryTableViewController.swift` and `ItineraryTableViewWrapper.swift`. This research validates that approach and documents the recommended stack for extending it to support external drops.
|
||||
|
||||
**Key Finding:** The existing implementation uses the traditional `canMoveRowAt`/`moveRowAt` approach with `tableView.isEditing = true`. For external drops (from outside the table), the codebase will need to add `UITableViewDropDelegate` protocol conformance.
|
||||
|
||||
---
|
||||
|
||||
## Recommended APIs
|
||||
|
||||
### Core APIs (Already in Use)
|
||||
|
||||
| API | Purpose | Confidence |
|
||||
|-----|---------|------------|
|
||||
| `UITableViewController` | Native table with built-in drag handling | HIGH |
|
||||
| `tableView.isEditing = true` | Enables drag handles on rows | HIGH |
|
||||
| `canMoveRowAt:` | Controls which rows show drag handles | HIGH |
|
||||
| `moveRowAt:to:` | Called when reorder completes | HIGH |
|
||||
| `targetIndexPathForMoveFromRowAt:toProposedIndexPath:` | Real-time validation during drag | HIGH |
|
||||
| `UIHostingConfiguration` | Embeds SwiftUI views in cells | HIGH |
|
||||
|
||||
**Rationale:** These APIs provide the smooth, native iOS reordering experience with real-time insertion line feedback. The existing implementation demonstrates this working well.
|
||||
|
||||
### APIs Needed for External Drops
|
||||
|
||||
| API | Purpose | When to Use | Confidence |
|
||||
|-----|---------|-------------|------------|
|
||||
| `UITableViewDropDelegate` | Accept drops from outside the table | Required for external drops | HIGH |
|
||||
| `UITableViewDragDelegate` | Provide drag items (not strictly needed if only receiving) | Optional | HIGH |
|
||||
| `dropSessionDidUpdate(_:withDestinationIndexPath:)` | Validate drop during hover | Shows insertion feedback for external drags | HIGH |
|
||||
| `performDropWith(_:)` | Handle external drop completion | Called only for external drops (not internal moves) | HIGH |
|
||||
| `canHandle(_:)` | Validate drop session types | Filter what can be dropped | HIGH |
|
||||
| `NSItemProvider` | Data transfer wrapper | Encodes dragged item data | HIGH |
|
||||
| `UIDragItem.localObject` | In-app optimization | Avoids encoding when drag is same-app | HIGH |
|
||||
|
||||
**Rationale:** For external drops, `UITableViewDropDelegate` is required. The key insight from research: when both `moveRowAt:` and `performDropWith:` are implemented, UIKit automatically routes internal reorders through `moveRowAt:` and external drops through `performDropWith:`. This is documented behavior.
|
||||
|
||||
---
|
||||
|
||||
## SwiftUI Integration Pattern
|
||||
|
||||
### Current Pattern (Validated)
|
||||
|
||||
The codebase uses `UIViewControllerRepresentable` with a Coordinator pattern:
|
||||
|
||||
```swift
|
||||
struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresentable {
|
||||
// Callbacks for data mutations (lifted state)
|
||||
var onTravelMoved: ((String, Int, Double) -> Void)?
|
||||
var onCustomItemMoved: ((UUID, Int, Double) -> Void)?
|
||||
|
||||
class Coordinator {
|
||||
var headerHostingController: UIHostingController<HeaderContent>?
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator()
|
||||
}
|
||||
|
||||
func makeUIViewController(context: Context) -> ItineraryTableViewController {
|
||||
let controller = ItineraryTableViewController(style: .plain)
|
||||
// Configure callbacks
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_ controller: ItineraryTableViewController, context: Context) {
|
||||
// Push new data to controller
|
||||
controller.reloadData(days: days, ...)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Confidence:** HIGH (implemented and working)
|
||||
|
||||
### For External Drops: Callback Extension
|
||||
|
||||
Add a new callback for external drops:
|
||||
|
||||
```swift
|
||||
var onExternalItemDropped: ((ExternalDropItem, Int, Double) -> Void)?
|
||||
// Parameters: dropped item, target day, target sortOrder
|
||||
```
|
||||
|
||||
The ItineraryTableViewController would need to:
|
||||
1. Conform to `UITableViewDropDelegate`
|
||||
2. Set `tableView.dropDelegate = self`
|
||||
3. Implement required delegate methods
|
||||
4. Call the callback when external drop completes
|
||||
|
||||
**Confidence:** HIGH (standard pattern extension)
|
||||
|
||||
---
|
||||
|
||||
## What to Avoid
|
||||
|
||||
### Anti-Pattern 1: SwiftUI-Only Drag-Drop for Complex Reordering
|
||||
|
||||
**What:** Using `.draggable()` / `.dropDestination()` / `.onMove()` directly in SwiftUI List
|
||||
|
||||
**Why Avoid:**
|
||||
- No real-time insertion line feedback during drag (item only moves on drop)
|
||||
- `ForEach.onMove` only works within a single section
|
||||
- Limited control over valid drop positions during drag
|
||||
- iPhone has additional limitations for SwiftUI List drag-drop
|
||||
|
||||
**Evidence:** The codebase documentation explicitly states: "SwiftUI's drag-and-drop APIs have significant limitations for complex reordering"
|
||||
|
||||
**Confidence:** HIGH
|
||||
|
||||
### Anti-Pattern 2: Third-Party Reordering Libraries
|
||||
|
||||
**What:** Using libraries like SwiftReorder, LPRTableView, TableViewDragger
|
||||
|
||||
**Why Avoid:**
|
||||
- Compatibility issues with recent iOS versions reported
|
||||
- Built-in UITableView drag-drop (iOS 11+) is more reliable
|
||||
- Additional dependency for functionality that's native
|
||||
|
||||
**Evidence:** Multiple search results recommend "use the built-in UITableView drag and drop API" over third-party libraries
|
||||
|
||||
**Confidence:** MEDIUM (anecdotal reports)
|
||||
|
||||
### Anti-Pattern 3: Mixing Diffable Data Source with Manual Array Updates
|
||||
|
||||
**What:** Using `UITableViewDiffableDataSource` but manually manipulating the array in `moveRowAt:`
|
||||
|
||||
**Why Avoid:**
|
||||
- Risk of data source inconsistency
|
||||
- Diffable data sources have their own update patterns
|
||||
- The current implementation uses manual `flatItems` array management which works correctly
|
||||
|
||||
**If Using Diffable Data Source:** Must reconcile changes through snapshot mechanism, not direct array manipulation
|
||||
|
||||
**Confidence:** MEDIUM
|
||||
|
||||
### Anti-Pattern 4: Ignoring `localObject` for Same-App Drops
|
||||
|
||||
**What:** Always encoding/decoding NSItemProvider data even for internal drops
|
||||
|
||||
**Why Avoid:**
|
||||
- Unnecessary overhead for same-app transfers
|
||||
- `UIDragItem.localObject` provides direct object access without serialization
|
||||
- More complex code for no benefit
|
||||
|
||||
**Best Practice:** Check `localObject` first, fall back to NSItemProvider decoding only for cross-app drops
|
||||
|
||||
**Confidence:** HIGH
|
||||
|
||||
---
|
||||
|
||||
## iOS 26 Considerations
|
||||
|
||||
### New SwiftUI Drag-Drop Modifiers (iOS 26)
|
||||
|
||||
iOS 26 introduces improved SwiftUI drag-drop modifiers:
|
||||
- `.draggable(containerItemID:)` - Marks items as draggable
|
||||
- `.dragContainer(for:selection:)` - Defines container and selection
|
||||
- `.dragConfiguration()` - Controls behavior (allowMove, allowDelete)
|
||||
- `.onDragSessionUpdated()` - Handles drag phases
|
||||
- `.dragPreviewsFormation(.stack)` - Customizes preview
|
||||
|
||||
**Assessment:** These are promising for simpler use cases, particularly macOS file management UIs. However, for the existing UITableView-based itinerary editor:
|
||||
|
||||
**Recommendation:** Keep the UITableView approach. The new SwiftUI modifiers don't provide the same level of control needed for:
|
||||
- Constraint-aware drop validation (travel can only go on certain days)
|
||||
- Real-time insertion line between specific rows
|
||||
- Semantic positioning (day + sortOrder) vs row indices
|
||||
|
||||
**Confidence:** MEDIUM (iOS 26 APIs are new, full capabilities not fully documented)
|
||||
|
||||
### Swift 6 Concurrency Considerations
|
||||
|
||||
The existing `ItineraryTableViewController` is a `final class` (not actor). Key considerations:
|
||||
|
||||
1. **Coordinator should be `@MainActor`** - Delegate callbacks occur on main thread
|
||||
2. **Callbacks are closures** - Already work correctly with Swift 6
|
||||
3. **No async operations during drag** - Validation is synchronous, which is correct
|
||||
|
||||
**No changes required** for Swift 6 compliance in the existing implementation.
|
||||
|
||||
**Confidence:** HIGH
|
||||
|
||||
---
|
||||
|
||||
## Architecture Decision: Two Approaches for External Drops
|
||||
|
||||
### Option A: Extend Existing UITableViewController (Recommended)
|
||||
|
||||
Add `UITableViewDropDelegate` to `ItineraryTableViewController`:
|
||||
|
||||
```swift
|
||||
extension ItineraryTableViewController: UITableViewDropDelegate {
|
||||
func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool {
|
||||
// Accept your custom item types
|
||||
return session.canLoadObjects(ofClass: ItineraryItemTransferable.self)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView,
|
||||
dropSessionDidUpdate session: UIDropSession,
|
||||
withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
|
||||
// Return .insertAtDestinationIndexPath for insertion line feedback
|
||||
return UITableViewDropProposal(operation: .copy, intent: .insertAtDestinationIndexPath)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
|
||||
// Extract item and calculate semantic position
|
||||
// Call onExternalItemDropped callback
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Builds on existing, working implementation
|
||||
- Minimal code changes
|
||||
- Maintains semantic positioning logic
|
||||
|
||||
**Cons:**
|
||||
- None significant
|
||||
|
||||
### Option B: SwiftUI Overlay for Drag Source
|
||||
|
||||
If the external drag SOURCE is a SwiftUI view (e.g., a "suggestions" panel):
|
||||
|
||||
```swift
|
||||
// In SwiftUI
|
||||
SuggestionCard(item: item)
|
||||
.draggable(item) {
|
||||
SuggestionPreview(item: item)
|
||||
}
|
||||
```
|
||||
|
||||
The UITableView receives this via `UITableViewDropDelegate` as above.
|
||||
|
||||
**Note:** This hybrid approach works well - SwiftUI provides the drag source, UIKit receives the drop.
|
||||
|
||||
**Confidence:** HIGH
|
||||
|
||||
---
|
||||
|
||||
## Summary: Recommended Stack
|
||||
|
||||
| Component | Recommendation | Rationale |
|
||||
|-----------|---------------|-----------|
|
||||
| **Table View** | `UITableViewController` | Native drag handles, real-time feedback |
|
||||
| **Internal Reorder** | `canMoveRowAt` / `moveRowAt` | Already working, proven |
|
||||
| **External Drops** | Add `UITableViewDropDelegate` | Required for external drops |
|
||||
| **SwiftUI Bridge** | `UIViewControllerRepresentable` | Already working |
|
||||
| **Cell Content** | `UIHostingConfiguration` | SwiftUI views in UIKit cells |
|
||||
| **State Management** | Lifted callbacks to parent | Unidirectional data flow |
|
||||
| **Drag Source (external)** | SwiftUI `.draggable()` | Simple for source views |
|
||||
| **Position Model** | (day, sortOrder) semantics | Already working, robust |
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
### Official Documentation
|
||||
- [UITableViewDragDelegate](https://developer.apple.com/documentation/uikit/uitableviewdragdelegate)
|
||||
- [UITableViewDropDelegate](https://developer.apple.com/documentation/uikit/uitableviewdropdelegate)
|
||||
- [Supporting drag and drop in table views](https://developer.apple.com/documentation/uikit/views_and_controls/table_views/supporting_drag_and_drop_in_table_views)
|
||||
- [Adopting drag and drop using SwiftUI](https://developer.apple.com/documentation/SwiftUI/Adopting-drag-and-drop-using-SwiftUI)
|
||||
|
||||
### Technical Articles
|
||||
- [Using Drag and Drop on UITableView for reorder](https://rderik.com/blog/using-drag-and-drop-on-uitableview-for-reorder/)
|
||||
- [Drag to Reorder in UITableView with Diffable Datasource](https://swiftjectivec.com/Tableview-Diffable-Datasource-Drag-to-Reorder/)
|
||||
- [Coding for iOS 11: How to drag & drop into collections & tables](https://hackernoon.com/drag-it-drop-it-in-collection-table-ios-11-6bd28795b313)
|
||||
- [SwiftUI in iOS 26 - What's new from WWDC 2025](https://differ.blog/p/swift-ui-in-ios-26-what-s-new-from-wwdc-2025-819b42)
|
||||
- [Drag and drop transferable data in SwiftUI](https://swiftwithmajid.com/2023/04/05/drag-and-drop-transferable-data-in-swiftui/)
|
||||
|
||||
### SwiftUI Limitations References
|
||||
- [Dragging list rows between sections - Apple Forums](https://developer.apple.com/forums/thread/674393)
|
||||
- [How to let users move rows in a list - Hacking with Swift](https://www.hackingwithswift.com/quick-start/swiftui/how-to-let-users-move-rows-in-a-list)
|
||||
|
||||
---
|
||||
|
||||
## Confidence Assessment
|
||||
|
||||
| Area | Confidence | Reason |
|
||||
|------|------------|--------|
|
||||
| Core UITableView Drag APIs | HIGH | Stable since iOS 11, extensive documentation |
|
||||
| External Drop via UITableViewDropDelegate | HIGH | Standard documented pattern |
|
||||
| SwiftUI Bridge Pattern | HIGH | Already implemented and working in codebase |
|
||||
| iOS 26 SwiftUI Improvements | MEDIUM | New APIs, limited production experience |
|
||||
| Swift 6 Compatibility | HIGH | Existing code is already compliant |
|
||||
| Third-party library avoidance | MEDIUM | Based on community reports, not direct testing |
|
||||
Reference in New Issue
Block a user