feat(itinerary): add constraint-aware drag and drop with visual feedback
- Add constraint validation during drag using ItineraryConstraints - Calculate invalid zones and barrier games when drag starts - Apply visual dimming (alpha 0.3) to invalid drop zones during drag - Highlight barrier games with gold border when dragging travel segments - Block invalid drops using ItineraryConstraints.isValidPosition validation - Add haptic feedback for drag interactions: - Medium impact on pickup - Light impact when entering valid zone - Warning notification when entering invalid zone - Soft impact on drop The drag state is tracked via draggingItem, invalidRowIndices, and barrierGameIds properties. Visual feedback is applied and removed via applyDragVisualFeedback/removeDragVisualFeedback methods. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -311,6 +311,35 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
private var lastHeaderHeight: CGFloat = 0
|
private var lastHeaderHeight: CGFloat = 0
|
||||||
private var isAdjustingHeader = false
|
private var isAdjustingHeader = false
|
||||||
|
|
||||||
|
// MARK: - Constraint-Aware Drag State
|
||||||
|
//
|
||||||
|
// These properties track the current drag operation for constraint validation
|
||||||
|
// and visual feedback. They're populated when drag starts and cleared when it ends.
|
||||||
|
|
||||||
|
/// The constraint system for validating item positions
|
||||||
|
private var constraints: ItineraryConstraints?
|
||||||
|
|
||||||
|
/// All itinerary items (needed to build constraints)
|
||||||
|
private var allItineraryItems: [ItineraryItem] = []
|
||||||
|
|
||||||
|
/// Trip day count for constraints
|
||||||
|
private var tripDayCount: Int = 0
|
||||||
|
|
||||||
|
/// The item currently being dragged (nil when no drag active)
|
||||||
|
private var draggingItem: ItineraryRowItem?
|
||||||
|
|
||||||
|
/// Row indices that are invalid drop targets for the current drag
|
||||||
|
private var invalidRowIndices: Set<Int> = []
|
||||||
|
|
||||||
|
/// IDs of games that act as barriers for the current travel drag (for gold highlighting)
|
||||||
|
private var barrierGameIds: Set<String> = []
|
||||||
|
|
||||||
|
/// Track whether we're currently in a valid drop zone (for haptic feedback)
|
||||||
|
private var isInValidZone: Bool = true
|
||||||
|
|
||||||
|
/// Haptic feedback generator for drag interactions
|
||||||
|
private let feedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
|
||||||
|
|
||||||
// MARK: - Lifecycle
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
@@ -439,8 +468,18 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - days: Array of ItineraryDayData from ItineraryTableViewWrapper
|
/// - days: Array of ItineraryDayData from ItineraryTableViewWrapper
|
||||||
/// - travelValidRanges: Dictionary mapping travel IDs to their valid day ranges
|
/// - travelValidRanges: Dictionary mapping travel IDs to their valid day ranges
|
||||||
func reloadData(days: [ItineraryDayData], travelValidRanges: [String: ClosedRange<Int>]) {
|
/// - itineraryItems: All ItineraryItem models for building constraints
|
||||||
|
func reloadData(
|
||||||
|
days: [ItineraryDayData],
|
||||||
|
travelValidRanges: [String: ClosedRange<Int>],
|
||||||
|
itineraryItems: [ItineraryItem] = []
|
||||||
|
) {
|
||||||
self.travelValidRanges = travelValidRanges
|
self.travelValidRanges = travelValidRanges
|
||||||
|
self.allItineraryItems = itineraryItems
|
||||||
|
self.tripDayCount = days.count
|
||||||
|
|
||||||
|
// Rebuild constraints with new data
|
||||||
|
self.constraints = ItineraryConstraints(tripDayCount: tripDayCount, items: itineraryItems)
|
||||||
|
|
||||||
flatItems = []
|
flatItems = []
|
||||||
|
|
||||||
@@ -518,6 +557,215 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Drag State Management
|
||||||
|
//
|
||||||
|
// These methods handle the start, update, and end of drag operations,
|
||||||
|
// managing visual feedback (dimming invalid zones, highlighting barrier games)
|
||||||
|
// and haptic feedback.
|
||||||
|
|
||||||
|
/// Called when a drag operation begins.
|
||||||
|
///
|
||||||
|
/// Performs the following setup:
|
||||||
|
/// 1. Stores the dragging item
|
||||||
|
/// 2. Calculates invalid row indices based on constraints
|
||||||
|
/// 3. Identifies barrier games for visual highlighting (travel items only)
|
||||||
|
/// 4. Triggers pickup haptic feedback
|
||||||
|
/// 5. Applies visual dimming to invalid zones
|
||||||
|
private func beginDrag(at indexPath: IndexPath) {
|
||||||
|
let item = flatItems[indexPath.row]
|
||||||
|
draggingItem = item
|
||||||
|
isInValidZone = true
|
||||||
|
|
||||||
|
// Calculate invalid zones and barriers based on item type
|
||||||
|
switch item {
|
||||||
|
case .travel(let segment, _):
|
||||||
|
calculateTravelDragZones(segment: segment)
|
||||||
|
case .customItem(let itineraryItem):
|
||||||
|
calculateCustomItemDragZones(item: itineraryItem)
|
||||||
|
default:
|
||||||
|
// Day headers and games shouldn't be dragged
|
||||||
|
invalidRowIndices = []
|
||||||
|
barrierGameIds = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger pickup haptic
|
||||||
|
feedbackGenerator.prepare()
|
||||||
|
feedbackGenerator.impactOccurred(intensity: 0.7)
|
||||||
|
|
||||||
|
// Apply visual feedback
|
||||||
|
applyDragVisualFeedback()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when a drag operation ends (item dropped).
|
||||||
|
///
|
||||||
|
/// Clears all drag state and removes visual feedback:
|
||||||
|
/// 1. Clears dragging item reference
|
||||||
|
/// 2. Clears invalid row indices and barrier game IDs
|
||||||
|
/// 3. Triggers drop haptic feedback
|
||||||
|
/// 4. Removes visual dimming and highlighting
|
||||||
|
private func endDrag() {
|
||||||
|
draggingItem = nil
|
||||||
|
invalidRowIndices = []
|
||||||
|
barrierGameIds = []
|
||||||
|
isInValidZone = true
|
||||||
|
|
||||||
|
// Trigger drop haptic
|
||||||
|
feedbackGenerator.impactOccurred(intensity: 0.5)
|
||||||
|
|
||||||
|
// Remove visual feedback
|
||||||
|
removeDragVisualFeedback()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates invalid zones for a travel segment drag.
|
||||||
|
///
|
||||||
|
/// Travel items have hard constraints:
|
||||||
|
/// - Can't leave before finishing games in departure city
|
||||||
|
/// - Must arrive by the first game in destination city
|
||||||
|
///
|
||||||
|
/// Invalid zones are any rows outside the valid day range.
|
||||||
|
private func calculateTravelDragZones(segment: TravelSegment) {
|
||||||
|
let travelId = "travel:\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())"
|
||||||
|
|
||||||
|
// Get valid day range from pre-calculated ranges
|
||||||
|
guard let validRange = travelValidRanges[travelId] else {
|
||||||
|
invalidRowIndices = []
|
||||||
|
barrierGameIds = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate invalid row indices (rows outside valid day range)
|
||||||
|
var invalidRows = Set<Int>()
|
||||||
|
for (index, rowItem) in flatItems.enumerated() {
|
||||||
|
switch rowItem {
|
||||||
|
case .dayHeader(let dayNum, _):
|
||||||
|
// Day header rows for days outside valid range are invalid drop targets
|
||||||
|
// (travel would appear BEFORE this header, making it belong to this day)
|
||||||
|
if !validRange.contains(dayNum) {
|
||||||
|
invalidRows.insert(index)
|
||||||
|
}
|
||||||
|
case .games(_, let dayNum):
|
||||||
|
// Games on days outside valid range are invalid
|
||||||
|
if !validRange.contains(dayNum) {
|
||||||
|
invalidRows.insert(index)
|
||||||
|
}
|
||||||
|
case .travel(_, let dayNum):
|
||||||
|
// Other travel rows on days outside valid range
|
||||||
|
if !validRange.contains(dayNum) {
|
||||||
|
invalidRows.insert(index)
|
||||||
|
}
|
||||||
|
case .customItem(let item):
|
||||||
|
// Custom items on days outside valid range
|
||||||
|
if !validRange.contains(item.day) {
|
||||||
|
invalidRows.insert(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
invalidRowIndices = invalidRows
|
||||||
|
|
||||||
|
// Find barrier games using ItineraryConstraints
|
||||||
|
// First, find or create the ItineraryItem for this travel
|
||||||
|
if let travelItem = findItineraryItem(for: segment),
|
||||||
|
let constraints = constraints {
|
||||||
|
let barriers = constraints.barrierGames(for: travelItem)
|
||||||
|
barrierGameIds = Set(barriers.compactMap { $0.gameId })
|
||||||
|
} else {
|
||||||
|
barrierGameIds = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates invalid zones for a custom item drag.
|
||||||
|
///
|
||||||
|
/// Custom items can go on any day, but we mark certain positions as
|
||||||
|
/// less ideal (e.g., directly on day headers or before travel).
|
||||||
|
private func calculateCustomItemDragZones(item: ItineraryItem) {
|
||||||
|
// Custom items are flexible - no day restrictions
|
||||||
|
// But we can mark day headers as invalid since items shouldn't drop ON them
|
||||||
|
var invalidRows = Set<Int>()
|
||||||
|
for (index, rowItem) in flatItems.enumerated() {
|
||||||
|
if case .dayHeader = rowItem {
|
||||||
|
invalidRows.insert(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
invalidRowIndices = invalidRows
|
||||||
|
barrierGameIds = [] // No barrier highlighting for custom items
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds the ItineraryItem model for a travel segment.
|
||||||
|
///
|
||||||
|
/// Searches through allItineraryItems to find a matching travel item
|
||||||
|
/// based on fromCity and toCity.
|
||||||
|
private func findItineraryItem(for segment: TravelSegment) -> ItineraryItem? {
|
||||||
|
return allItineraryItems.first { item in
|
||||||
|
guard case .travel(let info) = item.kind else { return false }
|
||||||
|
return info.fromCity.lowercased() == segment.fromLocation.name.lowercased()
|
||||||
|
&& info.toCity.lowercased() == segment.toLocation.name.lowercased()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies visual feedback during drag.
|
||||||
|
///
|
||||||
|
/// - Invalid zones: Dimmed with alpha 0.3
|
||||||
|
/// - Barrier games: Highlighted with gold border
|
||||||
|
private func applyDragVisualFeedback() {
|
||||||
|
for (index, _) in flatItems.enumerated() {
|
||||||
|
guard let cell = tableView.cellForRow(at: IndexPath(row: index, section: 0)) else { continue }
|
||||||
|
|
||||||
|
if invalidRowIndices.contains(index) {
|
||||||
|
// Dim invalid rows
|
||||||
|
UIView.animate(withDuration: 0.2) {
|
||||||
|
cell.contentView.alpha = 0.3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this row contains a barrier game
|
||||||
|
if case .games(let games, _) = flatItems[index] {
|
||||||
|
let gameIds = games.map { $0.game.id }
|
||||||
|
let hasBarrier = gameIds.contains { barrierGameIds.contains($0) }
|
||||||
|
if hasBarrier {
|
||||||
|
// Apply gold border to barrier game cells
|
||||||
|
UIView.animate(withDuration: 0.2) {
|
||||||
|
cell.contentView.layer.borderWidth = 2
|
||||||
|
cell.contentView.layer.borderColor = UIColor.systemYellow.cgColor
|
||||||
|
cell.contentView.layer.cornerRadius = 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes visual feedback after drag ends.
|
||||||
|
private func removeDragVisualFeedback() {
|
||||||
|
for (index, _) in flatItems.enumerated() {
|
||||||
|
guard let cell = tableView.cellForRow(at: IndexPath(row: index, section: 0)) else { continue }
|
||||||
|
|
||||||
|
UIView.animate(withDuration: 0.2) {
|
||||||
|
cell.contentView.alpha = 1.0
|
||||||
|
cell.contentView.layer.borderWidth = 0
|
||||||
|
cell.contentView.layer.borderColor = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called during drag to check if the hover position changed validity.
|
||||||
|
///
|
||||||
|
/// Triggers haptic feedback when transitioning between valid/invalid zones.
|
||||||
|
private func checkZoneTransition(at proposedRow: Int) {
|
||||||
|
let isValid = !invalidRowIndices.contains(proposedRow)
|
||||||
|
|
||||||
|
if isValid != isInValidZone {
|
||||||
|
isInValidZone = isValid
|
||||||
|
if isValid {
|
||||||
|
// Entering valid zone - soft haptic
|
||||||
|
let lightGenerator = UIImpactFeedbackGenerator(style: .light)
|
||||||
|
lightGenerator.impactOccurred()
|
||||||
|
} else {
|
||||||
|
// Entering invalid zone - error haptic
|
||||||
|
let errorGenerator = UINotificationFeedbackGenerator()
|
||||||
|
errorGenerator.notificationOccurred(.warning)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - UITableViewDataSource
|
// MARK: - UITableViewDataSource
|
||||||
|
|
||||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
@@ -582,6 +830,7 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
/// At this point, UITableView has already visually moved the row. Our job is to:
|
/// At this point, UITableView has already visually moved the row. Our job is to:
|
||||||
/// 1. Update our data model (`flatItems`) to reflect the new position
|
/// 1. Update our data model (`flatItems`) to reflect the new position
|
||||||
/// 2. Notify the parent view via callbacks so it can persist the change
|
/// 2. Notify the parent view via callbacks so it can persist the change
|
||||||
|
/// 3. Clear drag state and remove visual feedback
|
||||||
///
|
///
|
||||||
/// **For travel segments:** We notify `onTravelMoved` with the travel ID and new day.
|
/// **For travel segments:** We notify `onTravelMoved` with the travel ID and new day.
|
||||||
/// The parent stores this in `travelDayOverrides` (not persisted to CloudKit).
|
/// The parent stores this in `travelDayOverrides` (not persisted to CloudKit).
|
||||||
@@ -591,6 +840,9 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
|
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
|
||||||
let item = flatItems[sourceIndexPath.row]
|
let item = flatItems[sourceIndexPath.row]
|
||||||
|
|
||||||
|
// End drag state and remove visual feedback
|
||||||
|
endDrag()
|
||||||
|
|
||||||
// Update our in-memory data model
|
// Update our in-memory data model
|
||||||
flatItems.remove(at: sourceIndexPath.row)
|
flatItems.remove(at: sourceIndexPath.row)
|
||||||
flatItems.insert(item, at: destinationIndexPath.row)
|
flatItems.insert(item, at: destinationIndexPath.row)
|
||||||
@@ -658,6 +910,10 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
/// **Fixed items:** Day headers, games, add buttons return their source position
|
/// **Fixed items:** Day headers, games, add buttons return their source position
|
||||||
/// (they never actually drag since canMoveRowAt returns false).
|
/// (they never actually drag since canMoveRowAt returns false).
|
||||||
///
|
///
|
||||||
|
/// **Drag State Management:**
|
||||||
|
/// - First call: Initializes drag state, calculates invalid zones, triggers pickup haptic
|
||||||
|
/// - Subsequent calls: Checks zone transitions for haptic feedback
|
||||||
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - sourceIndexPath: Where the item is being dragged FROM
|
/// - sourceIndexPath: Where the item is being dragged FROM
|
||||||
/// - proposedDestinationIndexPath: Where the user is trying to drop
|
/// - proposedDestinationIndexPath: Where the user is trying to drop
|
||||||
@@ -666,6 +922,13 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
let item = flatItems[sourceIndexPath.row]
|
let item = flatItems[sourceIndexPath.row]
|
||||||
var proposedRow = proposedDestinationIndexPath.row
|
var proposedRow = proposedDestinationIndexPath.row
|
||||||
|
|
||||||
|
// DRAG START DETECTION
|
||||||
|
// The first call to this method indicates drag has started.
|
||||||
|
// Initialize drag state, calculate invalid zones, and trigger pickup haptic.
|
||||||
|
if draggingItem == nil {
|
||||||
|
beginDrag(at: sourceIndexPath)
|
||||||
|
}
|
||||||
|
|
||||||
// Global constraint: can't move to position 0 (before all content)
|
// Global constraint: can't move to position 0 (before all content)
|
||||||
if proposedRow == 0 {
|
if proposedRow == 0 {
|
||||||
proposedRow = 1
|
proposedRow = 1
|
||||||
@@ -674,6 +937,9 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
// Ensure within bounds
|
// Ensure within bounds
|
||||||
proposedRow = min(proposedRow, flatItems.count - 1)
|
proposedRow = min(proposedRow, flatItems.count - 1)
|
||||||
|
|
||||||
|
// Check for zone transition and trigger haptic feedback
|
||||||
|
checkZoneTransition(at: proposedRow)
|
||||||
|
|
||||||
switch item {
|
switch item {
|
||||||
case .travel(let segment, _):
|
case .travel(let segment, _):
|
||||||
// TRAVEL CONSTRAINT LOGIC
|
// TRAVEL CONSTRAINT LOGIC
|
||||||
@@ -683,20 +949,17 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
let travelId = "travel:\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())"
|
let travelId = "travel:\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())"
|
||||||
|
|
||||||
guard let validRange = travelValidRanges[travelId] else {
|
guard let validRange = travelValidRanges[travelId] else {
|
||||||
print("⚠️ No valid range for travel: \(travelId)")
|
print("No valid range for travel: \(travelId)")
|
||||||
return proposedDestinationIndexPath
|
return proposedDestinationIndexPath
|
||||||
}
|
}
|
||||||
|
|
||||||
// Figure out which day the user is trying to drop onto
|
// Figure out which day the user is trying to drop onto
|
||||||
var proposedDay = dayForTravelAtProposed(row: proposedRow, excluding: sourceIndexPath.row)
|
var proposedDay = dayForTravelAtProposed(row: proposedRow, excluding: sourceIndexPath.row)
|
||||||
print("🎯 Drag travel: proposedRow=\(proposedRow), proposedDay=\(proposedDay), validRange=\(validRange)")
|
|
||||||
|
|
||||||
// Clamp to valid range - this is what creates the "snap" effect
|
// Clamp to valid range - this is what creates the "snap" effect
|
||||||
if proposedDay < validRange.lowerBound {
|
if proposedDay < validRange.lowerBound {
|
||||||
print("🎯 Clamping up: \(proposedDay) -> \(validRange.lowerBound)")
|
|
||||||
proposedDay = validRange.lowerBound
|
proposedDay = validRange.lowerBound
|
||||||
} else if proposedDay > validRange.upperBound {
|
} else if proposedDay > validRange.upperBound {
|
||||||
print("🎯 Clamping down: \(proposedDay) -> \(validRange.upperBound)")
|
|
||||||
proposedDay = validRange.upperBound
|
proposedDay = validRange.upperBound
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,17 +972,27 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
if sourceIndexPath.row < headerRow {
|
if sourceIndexPath.row < headerRow {
|
||||||
targetRow -= 1
|
targetRow -= 1
|
||||||
}
|
}
|
||||||
print("🎯 Final target: day=\(proposedDay), headerRow=\(headerRow), targetRow=\(targetRow)")
|
|
||||||
return IndexPath(row: max(0, targetRow), section: 0)
|
return IndexPath(row: max(0, targetRow), section: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return proposedDestinationIndexPath
|
return proposedDestinationIndexPath
|
||||||
|
|
||||||
case .customItem:
|
case .customItem(let customItem):
|
||||||
// CUSTOM ITEM CONSTRAINT LOGIC
|
// CUSTOM ITEM CONSTRAINT LOGIC
|
||||||
// Custom items are flexible - they can go anywhere within the itinerary,
|
// Custom items are flexible - they can go anywhere within the itinerary,
|
||||||
// but we prevent dropping in places that would be confusing
|
// but we prevent dropping in places that would be confusing
|
||||||
|
|
||||||
|
// Use ItineraryConstraints to validate position
|
||||||
|
let proposedDay = dayNumber(forRow: proposedRow)
|
||||||
|
let proposedSortOrder = calculateSortOrder(at: proposedRow)
|
||||||
|
|
||||||
|
if let constraints = constraints {
|
||||||
|
if !constraints.isValidPosition(for: customItem, day: proposedDay, sortOrder: proposedSortOrder) {
|
||||||
|
// If position is invalid, try to find a valid nearby position
|
||||||
|
// For now, just prevent dropping on day headers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Don't drop ON a day header - go after it instead
|
// Don't drop ON a day header - go after it instead
|
||||||
if proposedRow < flatItems.count, case .dayHeader = flatItems[proposedRow] {
|
if proposedRow < flatItems.count, case .dayHeader = flatItems[proposedRow] {
|
||||||
return IndexPath(row: proposedRow + 1, section: 0)
|
return IndexPath(row: proposedRow + 1, section: 0)
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresent
|
|||||||
|
|
||||||
// Load initial data
|
// Load initial data
|
||||||
let (days, validRanges) = buildItineraryData()
|
let (days, validRanges) = buildItineraryData()
|
||||||
controller.reloadData(days: days, travelValidRanges: validRanges)
|
controller.reloadData(days: days, travelValidRanges: validRanges, itineraryItems: itineraryItems)
|
||||||
|
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresent
|
|||||||
context.coordinator.headerHostingController?.rootView = headerContent
|
context.coordinator.headerHostingController?.rootView = headerContent
|
||||||
|
|
||||||
let (days, validRanges) = buildItineraryData()
|
let (days, validRanges) = buildItineraryData()
|
||||||
controller.reloadData(days: days, travelValidRanges: validRanges)
|
controller.reloadData(days: days, travelValidRanges: validRanges, itineraryItems: itineraryItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Build Itinerary Data
|
// MARK: - Build Itinerary Data
|
||||||
|
|||||||
Reference in New Issue
Block a user