# Custom Itinerary Items Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Allow users to add, edit, reorder, and delete custom items (restaurants, hotels, activities, notes) within saved trip itineraries, with CloudKit sync. **Architecture:** Domain model with anchor-based positioning, CloudKit wrapper following CKPollVote pattern, CustomItemService actor for CRUD, SwiftData for local cache, UI integration in TripDetailView with drag/drop. **Tech Stack:** Swift, SwiftUI, SwiftData, CloudKit, draggable/dropDestination APIs --- ## Task 1: Domain Model **Files:** - Create: `SportsTime/Core/Models/Domain/CustomItineraryItem.swift` - Test: `SportsTimeTests/Domain/CustomItineraryItemTests.swift` **Step 1: Create the domain model file** ```swift // // CustomItineraryItem.swift // SportsTime // import Foundation struct CustomItineraryItem: Identifiable, Codable, Hashable { let id: UUID let tripId: UUID var category: ItemCategory var title: String var anchorType: AnchorType var anchorId: String? var anchorDay: Int let createdAt: Date var modifiedAt: Date init( id: UUID = UUID(), tripId: UUID, category: ItemCategory, title: String, anchorType: AnchorType = .startOfDay, anchorId: String? = nil, anchorDay: Int, createdAt: Date = Date(), modifiedAt: Date = Date() ) { self.id = id self.tripId = tripId self.category = category self.title = title self.anchorType = anchorType self.anchorId = anchorId self.anchorDay = anchorDay self.createdAt = createdAt self.modifiedAt = modifiedAt } enum ItemCategory: String, Codable, CaseIterable { case restaurant case hotel case activity case note var icon: String { switch self { case .restaurant: return "🍽️" case .hotel: return "🏨" case .activity: return "🎯" case .note: return "📝" } } var label: String { switch self { case .restaurant: return "Restaurant" case .hotel: return "Hotel" case .activity: return "Activity" case .note: return "Note" } } var systemImage: String { switch self { case .restaurant: return "fork.knife" case .hotel: return "bed.double.fill" case .activity: return "figure.run" case .note: return "note.text" } } } enum AnchorType: String, Codable { case startOfDay case afterGame case afterTravel } } ``` **Step 2: Create basic unit tests** ```swift // // CustomItineraryItemTests.swift // SportsTimeTests // import Testing @testable import SportsTime import Foundation struct CustomItineraryItemTests { @Test("Item initializes with default values") func item_InitializesWithDefaults() { let tripId = UUID() let item = CustomItineraryItem( tripId: tripId, category: .restaurant, title: "Joe's BBQ", anchorDay: 1 ) #expect(item.tripId == tripId) #expect(item.category == .restaurant) #expect(item.title == "Joe's BBQ") #expect(item.anchorType == .startOfDay) #expect(item.anchorId == nil) #expect(item.anchorDay == 1) } @Test("Item category has correct icons") func category_HasCorrectIcons() { #expect(CustomItineraryItem.ItemCategory.restaurant.icon == "🍽️") #expect(CustomItineraryItem.ItemCategory.hotel.icon == "🏨") #expect(CustomItineraryItem.ItemCategory.activity.icon == "🎯") #expect(CustomItineraryItem.ItemCategory.note.icon == "📝") } @Test("Item is Codable") func item_IsCodable() throws { let item = CustomItineraryItem( tripId: UUID(), category: .hotel, title: "Hilton Downtown", anchorType: .afterGame, anchorId: "game_123", anchorDay: 2 ) let encoded = try JSONEncoder().encode(item) let decoded = try JSONDecoder().decode(CustomItineraryItem.self, from: encoded) #expect(decoded.id == item.id) #expect(decoded.title == item.title) #expect(decoded.anchorType == .afterGame) #expect(decoded.anchorId == "game_123") } } ``` **Step 3: Run tests to verify** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/CustomItineraryItemTests test` Expected: PASS **Step 4: Commit** ```bash git add SportsTime/Core/Models/Domain/CustomItineraryItem.swift SportsTimeTests/Domain/CustomItineraryItemTests.swift git commit -m "feat(models): add CustomItineraryItem domain model" ``` --- ## Task 2: CloudKit Wrapper **Files:** - Modify: `SportsTime/Core/Models/CloudKit/CKModels.swift` (add at end) **Step 1: Add CKRecordType constant** Find the `CKRecordType` extension and add: ```swift static let customItineraryItem: CKRecordType = "CustomItineraryItem" ``` **Step 2: Add CKCustomItineraryItem struct** Add at the end of CKModels.swift: ```swift // MARK: - CKCustomItineraryItem struct CKCustomItineraryItem { static let itemIdKey = "itemId" 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 createdAtKey = "createdAt" static let modifiedAtKey = "modifiedAt" let record: CKRecord init(record: CKRecord) { self.record = record } init(item: CustomItineraryItem) { let record = CKRecord( recordType: CKRecordType.customItineraryItem, recordID: CKRecord.ID(recordName: item.id.uuidString) ) record[CKCustomItineraryItem.itemIdKey] = item.id.uuidString 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.createdAtKey] = item.createdAt record[CKCustomItineraryItem.modifiedAtKey] = item.modifiedAt self.record = record } func toItem() -> CustomItineraryItem? { guard let itemIdString = record[CKCustomItineraryItem.itemIdKey] as? String, let itemId = UUID(uuidString: itemIdString), let tripIdString = record[CKCustomItineraryItem.tripIdKey] as? String, let tripId = UUID(uuidString: tripIdString), 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 return CustomItineraryItem( id: itemId, tripId: tripId, category: category, title: title, anchorType: anchorType, anchorId: anchorId, anchorDay: anchorDay, createdAt: createdAt, modifiedAt: modifiedAt ) } } ``` **Step 3: Build to verify** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` Expected: BUILD SUCCEEDED **Step 4: Commit** ```bash git add SportsTime/Core/Models/CloudKit/CKModels.swift git commit -m "feat(cloudkit): add CKCustomItineraryItem wrapper" ``` --- ## Task 3: CustomItemService **Files:** - Create: `SportsTime/Core/Services/CustomItemService.swift` **Step 1: Create the service** ```swift // // CustomItemService.swift // SportsTime // // CloudKit service for custom itinerary items // import Foundation import CloudKit actor CustomItemService { static let shared = CustomItemService() private let container: CKContainer private let publicDatabase: CKDatabase private init() { self.container = CKContainer(identifier: "iCloud.com.sportstime.app") self.publicDatabase = container.publicCloudDatabase } // MARK: - CRUD Operations func createItem(_ item: CustomItineraryItem) async throws -> CustomItineraryItem { let ckItem = CKCustomItineraryItem(item: item) do { try await publicDatabase.save(ckItem.record) return item } catch let error as CKError { throw mapCloudKitError(error) } catch { throw CustomItemError.unknown(error) } } func updateItem(_ item: CustomItineraryItem) async throws -> CustomItineraryItem { // Fetch existing record to get changeTag let recordID = CKRecord.ID(recordName: item.id.uuidString) let existingRecord: CKRecord do { existingRecord = try await publicDatabase.record(for: recordID) } catch let error as CKError { throw mapCloudKitError(error) } catch { throw CustomItemError.unknown(error) } // Update fields 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.modifiedAtKey] = now do { try await publicDatabase.save(existingRecord) var updatedItem = item updatedItem.modifiedAt = now return updatedItem } catch let error as CKError { throw mapCloudKitError(error) } catch { throw CustomItemError.unknown(error) } } func deleteItem(_ itemId: UUID) async throws { let recordID = CKRecord.ID(recordName: itemId.uuidString) do { try await publicDatabase.deleteRecord(withID: recordID) } catch let error as CKError { if error.code != .unknownItem { throw mapCloudKitError(error) } // Item already deleted - ignore } catch { throw CustomItemError.unknown(error) } } func fetchItems(forTripId tripId: UUID) async throws -> [CustomItineraryItem] { let predicate = NSPredicate( format: "%K == %@", CKCustomItineraryItem.tripIdKey, tripId.uuidString ) let query = CKQuery(recordType: CKRecordType.customItineraryItem, predicate: predicate) do { let (results, _) = try await publicDatabase.records(matching: query) return results.compactMap { result in guard case .success(let record) = result.1 else { return nil } return CKCustomItineraryItem(record: record).toItem() }.sorted { $0.anchorDay < $1.anchorDay } } catch let error as CKError { throw mapCloudKitError(error) } catch { throw CustomItemError.unknown(error) } } // MARK: - Error Mapping private func mapCloudKitError(_ error: CKError) -> CustomItemError { switch error.code { case .notAuthenticated: return .notSignedIn case .networkUnavailable, .networkFailure: return .networkUnavailable case .unknownItem: return .itemNotFound default: return .unknown(error) } } } // MARK: - Errors enum CustomItemError: Error, LocalizedError { case notSignedIn case itemNotFound case networkUnavailable case unknown(Error) var errorDescription: String? { switch self { case .notSignedIn: return "Please sign in to iCloud to use custom items." case .itemNotFound: return "Item not found. It may have been deleted." case .networkUnavailable: return "Unable to connect. Please check your internet connection." case .unknown(let error): return "An error occurred: \(error.localizedDescription)" } } } ``` **Step 2: Build to verify** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` Expected: BUILD SUCCEEDED **Step 3: Commit** ```bash git add SportsTime/Core/Services/CustomItemService.swift git commit -m "feat(services): add CustomItemService for CloudKit CRUD" ``` --- ## Task 4: SwiftData Local Model **Files:** - Modify: `SportsTime/Core/Models/Local/SavedTrip.swift` **Step 1: Add LocalCustomItem model** Add after the `TripVote` model: ```swift // MARK: - Local Custom Item (Cache) @Model final class LocalCustomItem { @Attribute(.unique) var id: UUID var tripId: UUID var category: String var title: String var anchorType: String var anchorId: String? var anchorDay: Int var createdAt: Date var modifiedAt: Date var pendingSync: Bool // True if needs to sync to CloudKit init( id: UUID = UUID(), tripId: UUID, category: CustomItineraryItem.ItemCategory, title: String, anchorType: CustomItineraryItem.AnchorType = .startOfDay, anchorId: String? = nil, anchorDay: Int, createdAt: Date = Date(), modifiedAt: Date = Date(), pendingSync: Bool = false ) { self.id = id self.tripId = tripId self.category = category.rawValue self.title = title self.anchorType = anchorType.rawValue self.anchorId = anchorId self.anchorDay = anchorDay 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) else { return nil } return CustomItineraryItem( id: id, tripId: tripId, category: category, title: title, anchorType: anchorType, anchorId: anchorId, anchorDay: anchorDay, createdAt: createdAt, modifiedAt: modifiedAt ) } static func from(_ item: CustomItineraryItem, pendingSync: Bool = false) -> LocalCustomItem { LocalCustomItem( id: item.id, tripId: item.tripId, category: item.category, title: item.title, anchorType: item.anchorType, anchorId: item.anchorId, anchorDay: item.anchorDay, createdAt: item.createdAt, modifiedAt: item.modifiedAt, pendingSync: pendingSync ) } } ``` **Step 2: Build to verify** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` Expected: BUILD SUCCEEDED **Step 3: Commit** ```bash git add SportsTime/Core/Models/Local/SavedTrip.swift git commit -m "feat(models): add LocalCustomItem SwiftData model" ``` --- ## Task 5: AddItemSheet UI **Files:** - Create: `SportsTime/Features/Trip/Views/AddItemSheet.swift` **Step 1: Create the sheet** ```swift // // AddItemSheet.swift // SportsTime // // Sheet for adding/editing custom itinerary items // import SwiftUI struct AddItemSheet: View { @Environment(\.dismiss) private var dismiss @Environment(\.colorScheme) private var colorScheme let tripId: UUID let anchorDay: Int let anchorType: CustomItineraryItem.AnchorType let anchorId: String? let existingItem: CustomItineraryItem? var onSave: (CustomItineraryItem) -> Void @State private var selectedCategory: CustomItineraryItem.ItemCategory = .restaurant @State private var title: String = "" @State private var isSaving = false private var isEditing: Bool { existingItem != nil } var body: some View { NavigationStack { VStack(spacing: Theme.Spacing.lg) { // Category picker categoryPicker // Title input TextField("What's the plan?", text: $title) .textFieldStyle(.roundedBorder) .font(.body) Spacer() } .padding() .background(Theme.backgroundGradient(colorScheme)) .navigationTitle(isEditing ? "Edit Item" : "Add Item") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button(isEditing ? "Save" : "Add") { saveItem() } .disabled(title.trimmingCharacters(in: .whitespaces).isEmpty || isSaving) } } .onAppear { if let existing = existingItem { selectedCategory = existing.category title = existing.title } } } } @ViewBuilder private var categoryPicker: some View { HStack(spacing: Theme.Spacing.md) { ForEach(CustomItineraryItem.ItemCategory.allCases, id: \.self) { category in CategoryButton( category: category, isSelected: selectedCategory == category ) { selectedCategory = category } } } } private func saveItem() { let trimmedTitle = title.trimmingCharacters(in: .whitespaces) guard !trimmedTitle.isEmpty else { return } isSaving = true let item: CustomItineraryItem if let existing = existingItem { item = CustomItineraryItem( id: existing.id, tripId: existing.tripId, category: selectedCategory, title: trimmedTitle, anchorType: existing.anchorType, anchorId: existing.anchorId, anchorDay: existing.anchorDay, createdAt: existing.createdAt, modifiedAt: Date() ) } else { item = CustomItineraryItem( tripId: tripId, category: selectedCategory, title: trimmedTitle, anchorType: anchorType, anchorId: anchorId, anchorDay: anchorDay ) } onSave(item) dismiss() } } // MARK: - Category Button private struct CategoryButton: View { let category: CustomItineraryItem.ItemCategory let isSelected: Bool let action: () -> Void var body: some View { Button(action: action) { VStack(spacing: 4) { Text(category.icon) .font(.title2) Text(category.label) .font(.caption2) } .frame(maxWidth: .infinity) .padding(.vertical, 12) .background(isSelected ? Theme.warmOrange.opacity(0.2) : Color.clear) .cornerRadius(8) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(isSelected ? Theme.warmOrange : Color.secondary.opacity(0.3), lineWidth: isSelected ? 2 : 1) ) } .buttonStyle(.plain) } } #Preview { AddItemSheet( tripId: UUID(), anchorDay: 1, anchorType: .startOfDay, anchorId: nil, existingItem: nil ) { _ in } } ``` **Step 2: Build to verify** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` Expected: BUILD SUCCEEDED **Step 3: Commit** ```bash git add SportsTime/Features/Trip/Views/AddItemSheet.swift git commit -m "feat(ui): add AddItemSheet for custom itinerary items" ``` --- ## Task 6: CustomItemRow UI **Files:** - Create: `SportsTime/Features/Trip/Views/CustomItemRow.swift` **Step 1: Create the row component** ```swift // // CustomItemRow.swift // SportsTime // // Row component for custom itinerary items with drag handle // import SwiftUI struct CustomItemRow: View { @Environment(\.colorScheme) private var colorScheme let item: CustomItineraryItem var onTap: () -> Void var onDelete: () -> Void /* REORDERING OPTIONS (for future reference): 1. Edit mode toggle - User taps "Edit" to show drag handles, then "Done" (like Reminders) 2. Long-press to drag - No edit mode, just long-press and drag 3. Always show handles - Custom items always have visible drag handle (CURRENT CHOICE) */ var body: some View { HStack(spacing: 12) { // Category icon Text(item.category.icon) .font(.title3) // Title Text(item.title) .font(.subheadline) .lineLimit(2) Spacer() // Drag handle - always visible Image(systemName: "line.3.horizontal") .foregroundStyle(.secondary) .font(.subheadline) } .padding(.horizontal, Theme.Spacing.md) .padding(.vertical, Theme.Spacing.sm) .background(Theme.warmOrange.opacity(0.08)) .cornerRadius(8) .contentShape(Rectangle()) .onTapGesture { onTap() } .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button(role: .destructive) { onDelete() } label: { Label("Delete", systemImage: "trash") } } } } #Preview { VStack { CustomItemRow( item: CustomItineraryItem( tripId: UUID(), category: .restaurant, title: "Joe's BBQ - Best brisket in Texas!", anchorDay: 1 ), onTap: {}, onDelete: {} ) CustomItemRow( item: CustomItineraryItem( tripId: UUID(), category: .hotel, title: "Hilton Downtown", anchorDay: 1 ), onTap: {}, onDelete: {} ) } .padding() } ``` **Step 2: Build to verify** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` Expected: BUILD SUCCEEDED **Step 3: Commit** ```bash git add SportsTime/Features/Trip/Views/CustomItemRow.swift git commit -m "feat(ui): add CustomItemRow with drag handle and swipe-to-delete" ``` --- ## Task 7: TripDetailView Integration - Part 1 (State & Data Loading) **Files:** - Modify: `SportsTime/Features/Trip/Views/TripDetailView.swift` **Step 1: Add state properties** Add these state properties near the existing `@State` declarations: ```swift @State private var customItems: [CustomItineraryItem] = [] @State private var showAddItemSheet = false @State private var editingItem: CustomItineraryItem? @State private var addItemAnchor: (day: Int, type: CustomItineraryItem.AnchorType, id: String?)? = nil ``` **Step 2: Add loadCustomItems function** Add this function in the view: ```swift private func loadCustomItems() async { guard let tripId = trip.id as UUID? else { return } do { customItems = try await CustomItemService.shared.fetchItems(forTripId: tripId) } catch { print("Failed to load custom items: \(error)") } } ``` **Step 3: Add task modifier to load items** In the body, add a `.task` modifier after the existing `.onAppear`: ```swift .task { await loadCustomItems() } ``` **Step 4: Build to verify** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` Expected: BUILD SUCCEEDED **Step 5: Commit** ```bash git add SportsTime/Features/Trip/Views/TripDetailView.swift git commit -m "feat(ui): add custom items state and loading to TripDetailView" ``` --- ## Task 8: TripDetailView Integration - Part 2 (Inline Add Buttons) **Files:** - Modify: `SportsTime/Features/Trip/Views/TripDetailView.swift` **Step 1: Create InlineAddButton component** Add this private struct inside or near TripDetailView: ```swift private struct InlineAddButton: View { let action: () -> Void var body: some View { Button(action: action) { HStack { Image(systemName: "plus.circle.fill") .foregroundStyle(Theme.warmOrange.opacity(0.6)) Text("Add") .font(.caption) .foregroundStyle(.secondary) } .padding(.vertical, 4) .padding(.horizontal, 8) } .buttonStyle(.plain) } } ``` **Step 2: Update ItinerarySection enum** Find the `ItinerarySection` enum and add a new case: ```swift enum ItinerarySection { case day(dayNumber: Int, date: Date, games: [RichGame]) case travel(TravelSegment) case customItem(CustomItineraryItem) case addButton(day: Int, anchorType: CustomItineraryItem.AnchorType, anchorId: String?) } ``` **Step 3: Build to verify** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` Expected: BUILD SUCCEEDED (may have warnings about unhandled cases - fix in next task) **Step 4: Commit** ```bash git add SportsTime/Features/Trip/Views/TripDetailView.swift git commit -m "feat(ui): add InlineAddButton and update ItinerarySection enum" ``` --- ## Task 9: TripDetailView Integration - Part 3 (Rendering Custom Items) **Files:** - Modify: `SportsTime/Features/Trip/Views/TripDetailView.swift` **Step 1: Update itinerarySections to include custom items and add buttons** Replace the `itinerarySections` computed property to interleave custom items and add buttons. This is complex - the key is to: 1. For each day section, insert custom items anchored to that day 2. After each game/travel, insert any custom items anchored to it 3. Insert add buttons in the gaps ```swift private var itinerarySections: [ItinerarySection] { var sections: [ItinerarySection] = [] var dayCitySections: [(dayNumber: Int, date: Date, city: String, games: [RichGame])] = [] let days = tripDays for (index, dayDate) in days.enumerated() { let dayNum = index + 1 let gamesOnDay = gamesOn(date: dayDate) guard !gamesOnDay.isEmpty else { continue } var gamesByCity: [(city: String, games: [RichGame])] = [] for game in gamesOnDay { let city = game.stadium.city if let lastIndex = gamesByCity.indices.last, gamesByCity[lastIndex].city == city { gamesByCity[lastIndex].games.append(game) } else { gamesByCity.append((city, [game])) } } for cityGroup in gamesByCity { dayCitySections.append((dayNum, dayDate, cityGroup.city, cityGroup.games)) } } for (index, section) in dayCitySections.enumerated() { // Travel before this section if index > 0 { let prevSection = dayCitySections[index - 1] if prevSection.city != section.city { if let travelSegment = findTravelSegment(from: prevSection.city, to: section.city) { sections.append(.travel(travelSegment)) // Add button after travel sections.append(.addButton(day: section.dayNumber, anchorType: .afterTravel, anchorId: travelSegment.id.uuidString)) // Custom items after this travel let itemsAfterTravel = customItems.filter { $0.anchorDay == section.dayNumber && $0.anchorType == .afterTravel && $0.anchorId == travelSegment.id.uuidString } for item in itemsAfterTravel { sections.append(.customItem(item)) } } } } // Add button at start of day (before games) sections.append(.addButton(day: section.dayNumber, anchorType: .startOfDay, anchorId: nil)) // Custom items at start of day let itemsAtStart = customItems.filter { $0.anchorDay == section.dayNumber && $0.anchorType == .startOfDay } for item in itemsAtStart { sections.append(.customItem(item)) } // Day section sections.append(.day(dayNumber: section.dayNumber, date: section.date, games: section.games)) // Add button after day's games if let lastGame = section.games.last { sections.append(.addButton(day: section.dayNumber, anchorType: .afterGame, anchorId: lastGame.game.id)) // Custom items after this game let itemsAfterGame = customItems.filter { $0.anchorDay == section.dayNumber && $0.anchorType == .afterGame && $0.anchorId == lastGame.game.id } for item in itemsAfterGame { sections.append(.customItem(item)) } } } return sections } ``` **Step 2: Update the ForEach in itinerarySection to handle new cases** Find where `itinerarySections` is rendered and update the switch: ```swift ForEach(Array(itinerarySections.enumerated()), id: \.offset) { index, section in switch section { case .day(let dayNumber, let date, let games): DaySection( dayNumber: dayNumber, date: date, games: games, trip: trip, colorScheme: colorScheme ) .staggeredAnimation(index: index) case .travel(let segment): TravelSection(segment: segment) .staggeredAnimation(index: index) case .customItem(let item): CustomItemRow( item: item, onTap: { editingItem = item }, onDelete: { Task { await deleteItem(item) } } ) .staggeredAnimation(index: index) case .addButton(let day, let anchorType, let anchorId): InlineAddButton { addItemAnchor = (day, anchorType, anchorId) showAddItemSheet = true } } } ``` **Step 3: Build to verify** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` Expected: BUILD SUCCEEDED (may need to add missing functions in next task) **Step 4: Commit** ```bash git add SportsTime/Features/Trip/Views/TripDetailView.swift git commit -m "feat(ui): render custom items and add buttons in itinerary" ``` --- ## Task 10: TripDetailView Integration - Part 4 (CRUD Operations & Sheets) **Files:** - Modify: `SportsTime/Features/Trip/Views/TripDetailView.swift` **Step 1: Add CRUD helper functions** ```swift private func saveItem(_ item: CustomItineraryItem) async { do { if customItems.contains(where: { $0.id == item.id }) { // Update existing let updated = try await CustomItemService.shared.updateItem(item) if let index = customItems.firstIndex(where: { $0.id == updated.id }) { customItems[index] = updated } } else { // Create new let created = try await CustomItemService.shared.createItem(item) customItems.append(created) } } catch { print("Failed to save item: \(error)") } } private func deleteItem(_ item: CustomItineraryItem) async { do { try await CustomItemService.shared.deleteItem(item.id) customItems.removeAll { $0.id == item.id } } catch { print("Failed to delete item: \(error)") } } ``` **Step 2: Add sheet modifiers** Add these modifiers to the view body: ```swift .sheet(isPresented: $showAddItemSheet) { if let anchor = addItemAnchor { AddItemSheet( tripId: trip.id, anchorDay: anchor.day, anchorType: anchor.type, anchorId: anchor.id, existingItem: nil ) { item in Task { await saveItem(item) } } } } .sheet(item: $editingItem) { item in AddItemSheet( tripId: trip.id, anchorDay: item.anchorDay, anchorType: item.anchorType, anchorId: item.anchorId, existingItem: item ) { updatedItem in Task { await saveItem(updatedItem) } } } ``` **Step 3: Build to verify** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` Expected: BUILD SUCCEEDED **Step 4: Commit** ```bash git add SportsTime/Features/Trip/Views/TripDetailView.swift git commit -m "feat(ui): add CRUD operations and sheets for custom items" ``` --- ## Task 11: Drag and Drop (Optional Enhancement) **Files:** - Modify: `SportsTime/Features/Trip/Views/TripDetailView.swift` - Modify: `SportsTime/Features/Trip/Views/CustomItemRow.swift` **Note:** This task adds drag-and-drop reordering. It can be deferred if basic add/edit/delete is sufficient for now. **Step 1: Make CustomItemRow draggable** Update CustomItemRow to add `.draggable`: ```swift var body: some View { HStack(spacing: 12) { // ... existing content } // ... existing modifiers .draggable(item.id.uuidString) { // Drag preview HStack { Text(item.category.icon) Text(item.title) } .padding(8) .background(Theme.cardBackground(.dark)) .cornerRadius(8) } } ``` **Step 2: Add drop destinations to add buttons** Update InlineAddButton: ```swift private struct InlineAddButton: View { let day: Int let anchorType: CustomItineraryItem.AnchorType let anchorId: String? let onAdd: () -> Void let onDrop: (CustomItineraryItem.AnchorType, String?, Int) -> Void var body: some View { Button(action: onAdd) { // ... existing content } .dropDestination(for: String.self) { items, _ in guard let itemIdString = items.first else { return false } onDrop(anchorType, anchorId, day) return true } } } ``` **Step 3: Implement move logic** Add function to handle drops: ```swift private func moveItem(itemId: String, toAnchorType: CustomItineraryItem.AnchorType, toAnchorId: String?, toDay: Int) async { guard let uuid = UUID(uuidString: itemId), var item = customItems.first(where: { $0.id == uuid }) else { return } item.anchorType = toAnchorType item.anchorId = toAnchorId item.anchorDay = toDay item.modifiedAt = Date() await saveItem(item) } ``` **Step 4: Build and test** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` Expected: BUILD SUCCEEDED **Step 5: Commit** ```bash git add SportsTime/Features/Trip/Views/TripDetailView.swift SportsTime/Features/Trip/Views/CustomItemRow.swift git commit -m "feat(ui): add drag-and-drop reordering for custom items" ``` --- ## Task 12: Final Integration Test **Step 1: Run full test suite** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test` Expected: All tests PASS **Step 2: Manual testing checklist** - [ ] Open a saved trip - [ ] Tap "+ Add" button between items - [ ] Select category and enter title - [ ] Verify item appears in correct position - [ ] Tap item to edit - [ ] Swipe left to delete - [ ] (If drag implemented) Drag item to new position - [ ] Close and reopen trip - items persist - [ ] Test on second device - items sync via CloudKit **Step 3: Final commit** ```bash git add -A git commit -m "feat: complete custom itinerary items feature" ``` --- ## Summary | Task | Description | Files | |------|-------------|-------| | 1 | Domain model | `CustomItineraryItem.swift`, tests | | 2 | CloudKit wrapper | `CKModels.swift` | | 3 | Service layer | `CustomItemService.swift` | | 4 | SwiftData model | `SavedTrip.swift` | | 5 | Add/Edit sheet | `AddItemSheet.swift` | | 6 | Item row component | `CustomItemRow.swift` | | 7-10 | TripDetailView integration | `TripDetailView.swift` | | 11 | Drag and drop (optional) | Multiple | | 12 | Integration testing | - |