refactor(itinerary): replace anchor-based positioning with day/sortOrder
Replace complex anchor system (anchorType, anchorId, anchorDay) with simple (day: Int, sortOrder: Double) positioning for custom items. Changes: - CustomItineraryItem: Remove anchor fields, add day and sortOrder - CKModels: Add migration fallback from old CloudKit fields - ItineraryTableViewController: Add calculateSortOrder() for midpoint insertion - TripDetailView: Simplify callbacks, itinerarySections, and routeWaypoints - AddItemSheet: Take simple day parameter instead of anchor - SavedTrip: Update LocalCustomItem SwiftData model Benefits: - Items freely movable via drag-and-drop - Route waypoints follow exact visual order - Simpler mental model: position = (day, sortOrder) - Midpoint insertion allows unlimited reordering Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -632,16 +632,19 @@ struct CKCustomItineraryItem {
|
||||
static let tripIdKey = "tripId"
|
||||
static let categoryKey = "category"
|
||||
static let titleKey = "title"
|
||||
static let anchorTypeKey = "anchorType"
|
||||
static let anchorIdKey = "anchorId"
|
||||
static let anchorDayKey = "anchorDay"
|
||||
static let sortOrderKey = "sortOrder"
|
||||
static let dayKey = "day" // NEW: replaces anchorDay
|
||||
static let sortOrderDoubleKey = "sortOrderDouble" // NEW: Double instead of Int
|
||||
static let createdAtKey = "createdAt"
|
||||
static let modifiedAtKey = "modifiedAt"
|
||||
// Location fields for mappable items
|
||||
static let latitudeKey = "latitude"
|
||||
static let longitudeKey = "longitude"
|
||||
static let addressKey = "address"
|
||||
// DEPRECATED - kept for migration reads only
|
||||
static let anchorTypeKey = "anchorType"
|
||||
static let anchorIdKey = "anchorId"
|
||||
static let anchorDayKey = "anchorDay"
|
||||
static let sortOrderKey = "sortOrder"
|
||||
|
||||
let record: CKRecord
|
||||
|
||||
@@ -658,10 +661,8 @@ struct CKCustomItineraryItem {
|
||||
record[CKCustomItineraryItem.tripIdKey] = item.tripId.uuidString
|
||||
record[CKCustomItineraryItem.categoryKey] = item.category.rawValue
|
||||
record[CKCustomItineraryItem.titleKey] = item.title
|
||||
record[CKCustomItineraryItem.anchorTypeKey] = item.anchorType.rawValue
|
||||
record[CKCustomItineraryItem.anchorIdKey] = item.anchorId
|
||||
record[CKCustomItineraryItem.anchorDayKey] = item.anchorDay
|
||||
record[CKCustomItineraryItem.sortOrderKey] = item.sortOrder
|
||||
record[CKCustomItineraryItem.dayKey] = item.day
|
||||
record[CKCustomItineraryItem.sortOrderDoubleKey] = item.sortOrder
|
||||
record[CKCustomItineraryItem.createdAtKey] = item.createdAt
|
||||
record[CKCustomItineraryItem.modifiedAtKey] = item.modifiedAt
|
||||
// Location fields (nil values are not stored in CloudKit)
|
||||
@@ -679,15 +680,30 @@ struct CKCustomItineraryItem {
|
||||
let categoryString = record[CKCustomItineraryItem.categoryKey] as? String,
|
||||
let category = CustomItineraryItem.ItemCategory(rawValue: categoryString),
|
||||
let title = record[CKCustomItineraryItem.titleKey] as? String,
|
||||
let anchorTypeString = record[CKCustomItineraryItem.anchorTypeKey] as? String,
|
||||
let anchorType = CustomItineraryItem.AnchorType(rawValue: anchorTypeString),
|
||||
let anchorDay = record[CKCustomItineraryItem.anchorDayKey] as? Int,
|
||||
let createdAt = record[CKCustomItineraryItem.createdAtKey] as? Date,
|
||||
let modifiedAt = record[CKCustomItineraryItem.modifiedAtKey] as? Date
|
||||
else { return nil }
|
||||
|
||||
let anchorId = record[CKCustomItineraryItem.anchorIdKey] as? String
|
||||
let sortOrder = record[CKCustomItineraryItem.sortOrderKey] as? Int ?? 0
|
||||
// Read new fields, with migration fallback from old fields
|
||||
let day: Int
|
||||
if let newDay = record[CKCustomItineraryItem.dayKey] as? Int {
|
||||
day = newDay
|
||||
} else if let oldDay = record[CKCustomItineraryItem.anchorDayKey] as? Int {
|
||||
// Migration: use old anchorDay
|
||||
day = oldDay
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let sortOrder: Double
|
||||
if let newSortOrder = record[CKCustomItineraryItem.sortOrderDoubleKey] as? Double {
|
||||
sortOrder = newSortOrder
|
||||
} else if let oldSortOrder = record[CKCustomItineraryItem.sortOrderKey] as? Int {
|
||||
// Migration: convert old Int sortOrder to Double
|
||||
sortOrder = Double(oldSortOrder)
|
||||
} else {
|
||||
sortOrder = 0.0
|
||||
}
|
||||
|
||||
// Location fields (optional - nil if not stored)
|
||||
let latitude = record[CKCustomItineraryItem.latitudeKey] as? Double
|
||||
@@ -699,9 +715,7 @@ struct CKCustomItineraryItem {
|
||||
tripId: tripId,
|
||||
category: category,
|
||||
title: title,
|
||||
anchorType: anchorType,
|
||||
anchorId: anchorId,
|
||||
anchorDay: anchorDay,
|
||||
day: day,
|
||||
sortOrder: sortOrder,
|
||||
createdAt: createdAt,
|
||||
modifiedAt: modifiedAt,
|
||||
|
||||
@@ -11,10 +11,8 @@ struct CustomItineraryItem: Identifiable, Codable, Hashable {
|
||||
let tripId: UUID
|
||||
var category: ItemCategory
|
||||
var title: String
|
||||
var anchorType: AnchorType
|
||||
var anchorId: String?
|
||||
var anchorDay: Int
|
||||
var sortOrder: Int // For ordering within same anchor position
|
||||
var day: Int // Day number (1-indexed)
|
||||
var sortOrder: Double // Position within day (allows insertion between items)
|
||||
let createdAt: Date
|
||||
var modifiedAt: Date
|
||||
|
||||
@@ -39,10 +37,8 @@ struct CustomItineraryItem: Identifiable, Codable, Hashable {
|
||||
tripId: UUID,
|
||||
category: ItemCategory,
|
||||
title: String,
|
||||
anchorType: AnchorType = .startOfDay,
|
||||
anchorId: String? = nil,
|
||||
anchorDay: Int,
|
||||
sortOrder: Int = 0,
|
||||
day: Int,
|
||||
sortOrder: Double = 0.0,
|
||||
createdAt: Date = Date(),
|
||||
modifiedAt: Date = Date(),
|
||||
latitude: Double? = nil,
|
||||
@@ -53,9 +49,7 @@ struct CustomItineraryItem: Identifiable, Codable, Hashable {
|
||||
self.tripId = tripId
|
||||
self.category = category
|
||||
self.title = title
|
||||
self.anchorType = anchorType
|
||||
self.anchorId = anchorId
|
||||
self.anchorDay = anchorDay
|
||||
self.day = day
|
||||
self.sortOrder = sortOrder
|
||||
self.createdAt = createdAt
|
||||
self.modifiedAt = modifiedAt
|
||||
@@ -97,10 +91,4 @@ struct CustomItineraryItem: Identifiable, Codable, Hashable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AnchorType: String, Codable {
|
||||
case startOfDay
|
||||
case afterGame
|
||||
case afterTravel
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,9 +109,8 @@ final class LocalCustomItem {
|
||||
var tripId: UUID
|
||||
var category: String
|
||||
var title: String
|
||||
var anchorType: String
|
||||
var anchorId: String?
|
||||
var anchorDay: Int
|
||||
var day: Int
|
||||
var sortOrder: Double
|
||||
var createdAt: Date
|
||||
var modifiedAt: Date
|
||||
var pendingSync: Bool // True if needs to sync to CloudKit
|
||||
@@ -121,9 +120,8 @@ final class LocalCustomItem {
|
||||
tripId: UUID,
|
||||
category: CustomItineraryItem.ItemCategory,
|
||||
title: String,
|
||||
anchorType: CustomItineraryItem.AnchorType = .startOfDay,
|
||||
anchorId: String? = nil,
|
||||
anchorDay: Int,
|
||||
day: Int,
|
||||
sortOrder: Double = 0.0,
|
||||
createdAt: Date = Date(),
|
||||
modifiedAt: Date = Date(),
|
||||
pendingSync: Bool = false
|
||||
@@ -132,17 +130,15 @@ final class LocalCustomItem {
|
||||
self.tripId = tripId
|
||||
self.category = category.rawValue
|
||||
self.title = title
|
||||
self.anchorType = anchorType.rawValue
|
||||
self.anchorId = anchorId
|
||||
self.anchorDay = anchorDay
|
||||
self.day = day
|
||||
self.sortOrder = sortOrder
|
||||
self.createdAt = createdAt
|
||||
self.modifiedAt = modifiedAt
|
||||
self.pendingSync = pendingSync
|
||||
}
|
||||
|
||||
var toItem: CustomItineraryItem? {
|
||||
guard let category = CustomItineraryItem.ItemCategory(rawValue: category),
|
||||
let anchorType = CustomItineraryItem.AnchorType(rawValue: anchorType)
|
||||
guard let category = CustomItineraryItem.ItemCategory(rawValue: category)
|
||||
else { return nil }
|
||||
|
||||
return CustomItineraryItem(
|
||||
@@ -150,9 +146,8 @@ final class LocalCustomItem {
|
||||
tripId: tripId,
|
||||
category: category,
|
||||
title: title,
|
||||
anchorType: anchorType,
|
||||
anchorId: anchorId,
|
||||
anchorDay: anchorDay,
|
||||
day: day,
|
||||
sortOrder: sortOrder,
|
||||
createdAt: createdAt,
|
||||
modifiedAt: modifiedAt
|
||||
)
|
||||
@@ -164,9 +159,8 @@ final class LocalCustomItem {
|
||||
tripId: item.tripId,
|
||||
category: item.category,
|
||||
title: item.title,
|
||||
anchorType: item.anchorType,
|
||||
anchorId: item.anchorId,
|
||||
anchorDay: item.anchorDay,
|
||||
day: item.day,
|
||||
sortOrder: item.sortOrder,
|
||||
createdAt: item.createdAt,
|
||||
modifiedAt: item.modifiedAt,
|
||||
pendingSync: pendingSync
|
||||
|
||||
@@ -55,10 +55,8 @@ actor CustomItemService {
|
||||
let now = Date()
|
||||
existingRecord[CKCustomItineraryItem.categoryKey] = item.category.rawValue
|
||||
existingRecord[CKCustomItineraryItem.titleKey] = item.title
|
||||
existingRecord[CKCustomItineraryItem.anchorTypeKey] = item.anchorType.rawValue
|
||||
existingRecord[CKCustomItineraryItem.anchorIdKey] = item.anchorId
|
||||
existingRecord[CKCustomItineraryItem.anchorDayKey] = item.anchorDay
|
||||
existingRecord[CKCustomItineraryItem.sortOrderKey] = item.sortOrder
|
||||
existingRecord[CKCustomItineraryItem.dayKey] = item.day
|
||||
existingRecord[CKCustomItineraryItem.sortOrderDoubleKey] = item.sortOrder
|
||||
existingRecord[CKCustomItineraryItem.modifiedAtKey] = now
|
||||
// Location fields (nil values clear the field in CloudKit)
|
||||
existingRecord[CKCustomItineraryItem.latitudeKey] = item.latitude
|
||||
@@ -77,16 +75,16 @@ actor CustomItemService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Batch update sortOrder for multiple items (for reordering)
|
||||
/// Batch update day+sortOrder for multiple items (for reordering)
|
||||
func updateSortOrders(_ items: [CustomItineraryItem]) async throws {
|
||||
guard !items.isEmpty else { return }
|
||||
print("☁️ [CloudKit] Batch updating sortOrder for \(items.count) items")
|
||||
print("☁️ [CloudKit] Batch updating day+sortOrder for \(items.count) items")
|
||||
|
||||
// Fetch all records
|
||||
let recordIDs = items.map { CKRecord.ID(recordName: $0.id.uuidString) }
|
||||
let fetchResults = try await publicDatabase.records(for: recordIDs)
|
||||
|
||||
// Update each record's sortOrder
|
||||
// Update each record's day and sortOrder
|
||||
var recordsToSave: [CKRecord] = []
|
||||
let now = Date()
|
||||
|
||||
@@ -94,7 +92,8 @@ actor CustomItemService {
|
||||
let recordID = CKRecord.ID(recordName: item.id.uuidString)
|
||||
guard case .success(let record) = fetchResults[recordID] else { continue }
|
||||
|
||||
record[CKCustomItineraryItem.sortOrderKey] = item.sortOrder
|
||||
record[CKCustomItineraryItem.dayKey] = item.day
|
||||
record[CKCustomItineraryItem.sortOrderDoubleKey] = item.sortOrder
|
||||
record[CKCustomItineraryItem.modifiedAtKey] = now
|
||||
recordsToSave.append(record)
|
||||
}
|
||||
@@ -107,7 +106,7 @@ actor CustomItemService {
|
||||
modifyOp.modifyRecordsResultBlock = { result in
|
||||
switch result {
|
||||
case .success:
|
||||
print("☁️ [CloudKit] Batch sortOrder update complete")
|
||||
print("☁️ [CloudKit] Batch day+sortOrder update complete")
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
print("☁️ [CloudKit] Batch update failed: \(error)")
|
||||
@@ -156,7 +155,7 @@ actor CustomItemService {
|
||||
print("☁️ [CloudKit] Failed to parse record: \(record.recordID.recordName)")
|
||||
}
|
||||
return item
|
||||
}.sorted { ($0.anchorDay, $0.sortOrder) < ($1.anchorDay, $1.sortOrder) }
|
||||
}.sorted { ($0.day, $0.sortOrder) < ($1.day, $1.sortOrder) }
|
||||
|
||||
print("☁️ [CloudKit] Parsed \(items.count) valid items")
|
||||
return items
|
||||
|
||||
Reference in New Issue
Block a user