refactor(itinerary): replace CustomItineraryItem with ItineraryItem across codebase
- Update CKModels.swift to remove deleted type references - Migrate LocalCustomItem to LocalItineraryItem in SavedTrip.swift - Update AppDelegate to handle subscription removal - Refactor AddItemSheet to create ItineraryItem with CustomInfo - Update ItineraryTableViewController and Wrapper for new model - Refactor TripDetailView state, methods and callbacks - Fix TripMapView to display custom items with new model structure This completes the migration from the legacy CustomItineraryItem/TravelDayOverride model to the unified ItineraryItem model with ItemKind enum. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,12 +34,12 @@ struct TripDetailView: View {
|
||||
@State private var loadedGames: [String: RichGame] = [:]
|
||||
@State private var isLoadingGames = false
|
||||
|
||||
// Custom items state
|
||||
@State private var customItems: [CustomItineraryItem] = []
|
||||
// Itinerary items state
|
||||
@State private var itineraryItems: [ItineraryItem] = []
|
||||
@State private var addItemAnchor: AddItemAnchor?
|
||||
@State private var editingItem: CustomItineraryItem?
|
||||
@State private var editingItem: ItineraryItem?
|
||||
@State private var subscriptionCancellable: AnyCancellable?
|
||||
@State private var draggedItem: CustomItineraryItem?
|
||||
@State private var draggedItem: ItineraryItem?
|
||||
@State private var draggedTravelId: String? // Track which travel segment is being dragged
|
||||
@State private var dropTargetId: String? // Track which drop zone is being hovered
|
||||
@State private var travelDayOverrides: [String: Int] = [:] // Key: travel ID, Value: day number
|
||||
@@ -122,7 +122,7 @@ struct TripDetailView: View {
|
||||
day: anchor.day,
|
||||
existingItem: nil
|
||||
) { item in
|
||||
Task { await saveCustomItem(item) }
|
||||
Task { await saveItineraryItem(item) }
|
||||
}
|
||||
}
|
||||
.sheet(item: $editingItem) { item in
|
||||
@@ -131,7 +131,7 @@ struct TripDetailView: View {
|
||||
day: item.day,
|
||||
existingItem: item
|
||||
) { updatedItem in
|
||||
Task { await saveCustomItem(updatedItem) }
|
||||
Task { await saveItineraryItem(updatedItem) }
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
@@ -140,21 +140,23 @@ struct TripDetailView: View {
|
||||
.task {
|
||||
await loadGamesIfNeeded()
|
||||
if allowCustomItems {
|
||||
await loadCustomItems()
|
||||
await loadItineraryItems()
|
||||
await setupSubscription()
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
subscriptionCancellable?.cancel()
|
||||
}
|
||||
.onChange(of: customItems) { _, newItems in
|
||||
.onChange(of: itineraryItems) { _, newItems in
|
||||
// Clear drag state after items update (move completed)
|
||||
draggedItem = nil
|
||||
dropTargetId = nil
|
||||
// Recalculate routes when custom items change (mappable items affect route)
|
||||
print("🗺️ [MapUpdate] customItems changed, count: \(newItems.count)")
|
||||
for item in newItems where item.isMappable {
|
||||
print("🗺️ [MapUpdate] Mappable: \(item.title) on day \(item.day), sortOrder: \(item.sortOrder)")
|
||||
print("🗺️ [MapUpdate] itineraryItems changed, count: \(newItems.count)")
|
||||
for item in newItems {
|
||||
if item.isCustom, let info = item.customInfo, info.isMappable {
|
||||
print("🗺️ [MapUpdate] Mappable: \(info.title) on day \(item.day), sortOrder: \(item.sortOrder)")
|
||||
}
|
||||
}
|
||||
Task {
|
||||
updateMapRegion()
|
||||
@@ -182,7 +184,7 @@ struct TripDetailView: View {
|
||||
ItineraryTableViewWrapper(
|
||||
trip: trip,
|
||||
games: Array(games.values),
|
||||
customItems: customItems,
|
||||
itineraryItems: itineraryItems,
|
||||
travelDayOverrides: travelDayOverrides,
|
||||
headerContent: {
|
||||
VStack(spacing: 0) {
|
||||
@@ -222,7 +224,7 @@ struct TripDetailView: View {
|
||||
},
|
||||
onCustomItemMoved: { itemId, day, sortOrder in
|
||||
Task { @MainActor in
|
||||
guard let item = customItems.first(where: { $0.id == itemId }) else { return }
|
||||
guard let item = itineraryItems.first(where: { $0.id == itemId }) else { return }
|
||||
await moveItem(item, toDay: day, sortOrder: sortOrder)
|
||||
}
|
||||
},
|
||||
@@ -230,7 +232,7 @@ struct TripDetailView: View {
|
||||
editingItem = item
|
||||
},
|
||||
onCustomItemDeleted: { item in
|
||||
Task { await deleteCustomItem(item) }
|
||||
Task { await deleteItineraryItem(item) }
|
||||
},
|
||||
onAddButtonTapped: { day in
|
||||
addItemAnchor = AddItemAnchor(day: day)
|
||||
@@ -570,9 +572,15 @@ struct TripDetailView: View {
|
||||
let isDraggingThis = draggedItem?.id == item.id
|
||||
CustomItemRow(
|
||||
item: item,
|
||||
onTap: { editingItem = item },
|
||||
onDelete: { Task { await deleteCustomItem(item) } }
|
||||
onTap: { editingItem = item }
|
||||
)
|
||||
.contextMenu {
|
||||
Button(role: .destructive) {
|
||||
Task { await deleteItineraryItem(item) }
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
.opacity(isDraggingThis ? 0.4 : 1.0)
|
||||
.staggeredAnimation(index: index)
|
||||
.overlay(alignment: .top) {
|
||||
@@ -644,12 +652,12 @@ struct TripDetailView: View {
|
||||
provider.loadObject(ofClass: NSString.self) { item, _ in
|
||||
guard let droppedId = item as? String,
|
||||
let itemId = UUID(uuidString: droppedId),
|
||||
let droppedItem = self.customItems.first(where: { $0.id == itemId }) else { return }
|
||||
let droppedItem = self.itineraryItems.first(where: { $0.id == itemId }) else { return }
|
||||
|
||||
Task { @MainActor in
|
||||
let day = self.findDayForTravelSegment(segment)
|
||||
// Place at beginning of day (sortOrder before existing items)
|
||||
let minSortOrder = self.customItems
|
||||
let minSortOrder = self.itineraryItems
|
||||
.filter { $0.day == day && $0.id != droppedItem.id }
|
||||
.map { $0.sortOrder }
|
||||
.min() ?? 1.0
|
||||
@@ -659,7 +667,7 @@ struct TripDetailView: View {
|
||||
return true
|
||||
}
|
||||
|
||||
private func handleCustomItemDrop(providers: [NSItemProvider], targetItem: CustomItineraryItem) -> Bool {
|
||||
private func handleCustomItemDrop(providers: [NSItemProvider], targetItem: ItineraryItem) -> Bool {
|
||||
guard let provider = providers.first, provider.canLoadObject(ofClass: NSString.self) else {
|
||||
return false
|
||||
}
|
||||
@@ -672,12 +680,12 @@ struct TripDetailView: View {
|
||||
provider.loadObject(ofClass: NSString.self) { item, _ in
|
||||
guard let droppedId = item as? String,
|
||||
let itemId = UUID(uuidString: droppedId),
|
||||
let droppedItem = self.customItems.first(where: { $0.id == itemId }),
|
||||
let droppedItem = self.itineraryItems.first(where: { $0.id == itemId }),
|
||||
droppedItem.id != targetItem.id else { return }
|
||||
|
||||
Task { @MainActor in
|
||||
// Place before target item using midpoint insertion
|
||||
let itemsInDay = self.customItems.filter { $0.day == targetItem.day && $0.id != droppedItem.id }
|
||||
let itemsInDay = self.itineraryItems.filter { $0.day == targetItem.day && $0.id != droppedItem.id }
|
||||
.sorted { $0.sortOrder < $1.sortOrder }
|
||||
let targetIdx = itemsInDay.firstIndex(where: { $0.id == targetItem.id }) ?? 0
|
||||
let prevSortOrder = targetIdx > 0 ? itemsInDay[targetIdx - 1].sortOrder : 0.0
|
||||
@@ -701,11 +709,11 @@ struct TripDetailView: View {
|
||||
provider.loadObject(ofClass: NSString.self) { item, _ in
|
||||
guard let droppedId = item as? String,
|
||||
let itemId = UUID(uuidString: droppedId),
|
||||
let droppedItem = self.customItems.first(where: { $0.id == itemId }) else { return }
|
||||
let droppedItem = self.itineraryItems.first(where: { $0.id == itemId }) else { return }
|
||||
|
||||
Task { @MainActor in
|
||||
// Calculate sortOrder: append at end of day's items
|
||||
let maxSortOrder = self.customItems
|
||||
let maxSortOrder = self.itineraryItems
|
||||
.filter { $0.day == day && $0.id != droppedItem.id }
|
||||
.map { $0.sortOrder }
|
||||
.max() ?? 0.0
|
||||
@@ -759,26 +767,23 @@ struct TripDetailView: View {
|
||||
}
|
||||
|
||||
/// Move item to a new day and sortOrder position
|
||||
private func moveItem(_ item: CustomItineraryItem, toDay day: Int, sortOrder: Double) async {
|
||||
private func moveItem(_ item: ItineraryItem, toDay day: Int, sortOrder: Double) async {
|
||||
var updated = item
|
||||
updated.day = day
|
||||
updated.sortOrder = sortOrder
|
||||
updated.modifiedAt = Date()
|
||||
|
||||
print("📍 [Move] Moving \(item.title) to day \(day), sortOrder: \(sortOrder)")
|
||||
let title = item.customInfo?.title ?? "item"
|
||||
print("📍 [Move] Moving \(title) to day \(day), sortOrder: \(sortOrder)")
|
||||
|
||||
// Update local state
|
||||
if let idx = customItems.firstIndex(where: { $0.id == item.id }) {
|
||||
customItems[idx] = updated
|
||||
if let idx = itineraryItems.firstIndex(where: { $0.id == item.id }) {
|
||||
itineraryItems[idx] = updated
|
||||
}
|
||||
|
||||
// Sync to CloudKit
|
||||
do {
|
||||
_ = try await CustomItemService.shared.updateItem(updated)
|
||||
print("✅ [Move] Synced \(updated.title) with day: \(day), sortOrder: \(sortOrder)")
|
||||
} catch {
|
||||
print("❌ [Move] Failed to sync: \(error)")
|
||||
}
|
||||
// Sync to CloudKit (debounced)
|
||||
await ItineraryItemService.shared.updateItem(updated)
|
||||
print("✅ [Move] Synced \(title) with day: \(day), sortOrder: \(sortOrder)")
|
||||
}
|
||||
|
||||
/// Build itinerary sections: shows ALL days with travel, custom items, and add buttons
|
||||
@@ -840,7 +845,8 @@ struct TripDetailView: View {
|
||||
// Add button first - always right after day header
|
||||
sections.append(.addButton(day: dayNum))
|
||||
|
||||
let dayItems = customItems.filter { $0.day == dayNum }
|
||||
let dayItems = itineraryItems
|
||||
.filter { $0.day == dayNum && $0.isCustom }
|
||||
.sorted { $0.sortOrder < $1.sortOrder }
|
||||
for item in dayItems {
|
||||
sections.append(.customItem(item))
|
||||
@@ -1021,8 +1027,8 @@ struct TripDetailView: View {
|
||||
}
|
||||
|
||||
/// Mappable custom items for display on the map
|
||||
private var mappableCustomItems: [CustomItineraryItem] {
|
||||
customItems.filter { $0.isMappable }
|
||||
private var mappableCustomItems: [ItineraryItem] {
|
||||
itineraryItems.filter { $0.isCustom && $0.customInfo?.isMappable == true }
|
||||
}
|
||||
|
||||
/// Convert stored route coordinates to MKPolyline for rendering
|
||||
@@ -1041,7 +1047,8 @@ struct TripDetailView: View {
|
||||
print("🗺️ [Waypoints] Building waypoints. Mappable items by day:")
|
||||
for (day, items) in itemsByDay.sorted(by: { $0.key < $1.key }) {
|
||||
for item in items.sorted(by: { $0.sortOrder < $1.sortOrder }) {
|
||||
print("🗺️ [Waypoints] Day \(day): \(item.title), sortOrder: \(item.sortOrder)")
|
||||
let title = item.customInfo?.title ?? "item"
|
||||
print("🗺️ [Waypoints] Day \(day): \(title), sortOrder: \(item.sortOrder)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1101,9 +1108,9 @@ struct TripDetailView: View {
|
||||
if let items = itemsByDay[dayNumber] {
|
||||
let sortedItems = items.sorted { $0.sortOrder < $1.sortOrder }
|
||||
for item in sortedItems {
|
||||
if let coord = item.coordinate {
|
||||
print("🗺️ [Waypoints] Adding \(item.title) (sortOrder: \(item.sortOrder))")
|
||||
waypoints.append((item.title, coord, true))
|
||||
if let info = item.customInfo, let coord = info.coordinate {
|
||||
print("🗺️ [Waypoints] Adding \(info.title) (sortOrder: \(item.sortOrder))")
|
||||
waypoints.append((info.title, coord, true))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1253,86 +1260,81 @@ struct TripDetailView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Custom Items (CloudKit persistence)
|
||||
// MARK: - Itinerary Items (CloudKit persistence)
|
||||
|
||||
private func setupSubscription() async {
|
||||
// Subscribe to real-time updates for this trip
|
||||
do {
|
||||
try await CustomItemSubscriptionService.shared.subscribeToTrip(trip.id)
|
||||
// TODO: Re-implement CloudKit subscription for ItineraryItem changes
|
||||
// The subscription service was removed during the ItineraryItem refactor.
|
||||
// For now, items are only loaded on view appear.
|
||||
print("📡 [Subscription] CloudKit subscriptions not yet implemented for ItineraryItem")
|
||||
}
|
||||
|
||||
// Listen for changes and reload
|
||||
subscriptionCancellable = await CustomItemSubscriptionService.shared.changePublisher
|
||||
.filter { $0 == self.trip.id }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [self] _ in
|
||||
print("📡 [Subscription] Received update, reloading custom items...")
|
||||
Task {
|
||||
await loadCustomItems()
|
||||
}
|
||||
private func loadItineraryItems() async {
|
||||
print("🔍 [ItineraryItems] Loading items for trip: \(trip.id)")
|
||||
do {
|
||||
let items = try await ItineraryItemService.shared.fetchItems(forTripId: trip.id)
|
||||
print("✅ [ItineraryItems] Loaded \(items.count) items from CloudKit")
|
||||
itineraryItems = items
|
||||
|
||||
// Extract travel day overrides from travel-type items
|
||||
var overrides: [String: Int] = [:]
|
||||
for item in items where item.isTravel {
|
||||
if let travelInfo = item.travelInfo {
|
||||
let travelId = "travel:\(travelInfo.fromCity.lowercased())->\(travelInfo.toCity.lowercased())"
|
||||
overrides[travelId] = item.day
|
||||
}
|
||||
} catch {
|
||||
print("📡 [Subscription] Failed to subscribe: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func loadCustomItems() async {
|
||||
print("🔍 [CustomItems] Loading items for trip: \(trip.id)")
|
||||
do {
|
||||
let items = try await CustomItemService.shared.fetchItems(forTripId: trip.id)
|
||||
print("✅ [CustomItems] Loaded \(items.count) items from CloudKit")
|
||||
customItems = items
|
||||
|
||||
// Also load travel day overrides
|
||||
let overrides = try await TravelOverrideService.shared.fetchOverridesAsDictionary(forTripId: trip.id)
|
||||
print("✅ [TravelOverrides] Loaded \(overrides.count) travel day overrides")
|
||||
}
|
||||
travelDayOverrides = overrides
|
||||
print("✅ [TravelOverrides] Extracted \(overrides.count) travel day overrides")
|
||||
} catch {
|
||||
print("❌ [CustomItems] Failed to load: \(error)")
|
||||
print("❌ [ItineraryItems] Failed to load: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func saveCustomItem(_ item: CustomItineraryItem) async {
|
||||
private func saveItineraryItem(_ item: ItineraryItem) async {
|
||||
// Check if this is an update or create
|
||||
let isUpdate = customItems.contains(where: { $0.id == item.id })
|
||||
print("💾 [CustomItems] Saving item: '\(item.title)' (isUpdate: \(isUpdate))")
|
||||
let isUpdate = itineraryItems.contains(where: { $0.id == item.id })
|
||||
let title = item.customInfo?.title ?? "item"
|
||||
print("💾 [ItineraryItems] Saving item: '\(title)' (isUpdate: \(isUpdate))")
|
||||
print(" - tripId: \(item.tripId)")
|
||||
print(" - day: \(item.day), sortOrder: \(item.sortOrder)")
|
||||
|
||||
// Update local state immediately for responsive UI
|
||||
if isUpdate {
|
||||
if let index = customItems.firstIndex(where: { $0.id == item.id }) {
|
||||
customItems[index] = item
|
||||
if let index = itineraryItems.firstIndex(where: { $0.id == item.id }) {
|
||||
itineraryItems[index] = item
|
||||
}
|
||||
} else {
|
||||
customItems.append(item)
|
||||
itineraryItems.append(item)
|
||||
}
|
||||
|
||||
// Persist to CloudKit
|
||||
do {
|
||||
if isUpdate {
|
||||
let updated = try await CustomItemService.shared.updateItem(item)
|
||||
print("✅ [CustomItems] Updated in CloudKit: \(updated.title)")
|
||||
await ItineraryItemService.shared.updateItem(item)
|
||||
print("✅ [ItineraryItems] Updated in CloudKit: \(title)")
|
||||
} else {
|
||||
let created = try await CustomItemService.shared.createItem(item)
|
||||
print("✅ [CustomItems] Created in CloudKit: \(created.title)")
|
||||
_ = try await ItineraryItemService.shared.createItem(item)
|
||||
print("✅ [ItineraryItems] Created in CloudKit: \(title)")
|
||||
}
|
||||
} catch {
|
||||
print("❌ [CustomItems] CloudKit save failed: \(error)")
|
||||
print("❌ [ItineraryItems] CloudKit save failed: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteCustomItem(_ item: CustomItineraryItem) async {
|
||||
print("🗑️ [CustomItems] Deleting item: '\(item.title)'")
|
||||
private func deleteItineraryItem(_ item: ItineraryItem) async {
|
||||
let title = item.customInfo?.title ?? "item"
|
||||
print("🗑️ [ItineraryItems] Deleting item: '\(title)'")
|
||||
|
||||
// Remove from local state immediately
|
||||
customItems.removeAll { $0.id == item.id }
|
||||
itineraryItems.removeAll { $0.id == item.id }
|
||||
|
||||
// Delete from CloudKit
|
||||
do {
|
||||
try await CustomItemService.shared.deleteItem(item.id)
|
||||
print("✅ [CustomItems] Deleted from CloudKit")
|
||||
try await ItineraryItemService.shared.deleteItem(item.id)
|
||||
print("✅ [ItineraryItems] Deleted from CloudKit")
|
||||
} catch {
|
||||
print("❌ [CustomItems] CloudKit delete failed: \(error)")
|
||||
print("❌ [ItineraryItems] CloudKit delete failed: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1375,10 +1377,10 @@ struct TripDetailView: View {
|
||||
|
||||
// Otherwise, it's a custom item drop
|
||||
guard let itemId = UUID(uuidString: droppedId),
|
||||
let item = self.customItems.first(where: { $0.id == itemId }) else { return }
|
||||
let item = self.itineraryItems.first(where: { $0.id == itemId }) else { return }
|
||||
|
||||
// Append at end of day's items
|
||||
let maxSortOrder = self.customItems
|
||||
let maxSortOrder = self.itineraryItems
|
||||
.filter { $0.day == dayNumber && $0.id != item.id }
|
||||
.map { $0.sortOrder }
|
||||
.max() ?? 0.0
|
||||
@@ -1393,18 +1395,45 @@ struct TripDetailView: View {
|
||||
private func saveTravelDayOverride(travelAnchorId: String, displayDay: Int) async {
|
||||
print("💾 [TravelOverrides] Saving override: \(travelAnchorId) -> day \(displayDay)")
|
||||
|
||||
let override = TravelDayOverride(
|
||||
tripId: trip.id,
|
||||
travelAnchorId: travelAnchorId,
|
||||
displayDay: displayDay
|
||||
)
|
||||
|
||||
do {
|
||||
_ = try await TravelOverrideService.shared.saveOverride(override)
|
||||
print("✅ [TravelOverrides] Saved to CloudKit")
|
||||
} catch {
|
||||
print("❌ [TravelOverrides] CloudKit save failed: \(error)")
|
||||
// Parse travel ID to extract cities (format: "travel:city1->city2")
|
||||
let stripped = travelAnchorId.replacingOccurrences(of: "travel:", with: "")
|
||||
let parts = stripped.components(separatedBy: "->")
|
||||
guard parts.count == 2 else {
|
||||
print("❌ [TravelOverrides] Invalid travel ID format: \(travelAnchorId)")
|
||||
return
|
||||
}
|
||||
|
||||
let fromCity = parts[0]
|
||||
let toCity = parts[1]
|
||||
|
||||
// Find existing travel item or create new one
|
||||
if let existingIndex = itineraryItems.firstIndex(where: {
|
||||
$0.isTravel && $0.travelInfo?.fromCity.lowercased() == fromCity && $0.travelInfo?.toCity.lowercased() == toCity
|
||||
}) {
|
||||
// Update existing
|
||||
var updated = itineraryItems[existingIndex]
|
||||
updated.day = displayDay
|
||||
updated.modifiedAt = Date()
|
||||
itineraryItems[existingIndex] = updated
|
||||
await ItineraryItemService.shared.updateItem(updated)
|
||||
} else {
|
||||
// Create new travel item
|
||||
let travelInfo = TravelInfo(fromCity: fromCity, toCity: toCity)
|
||||
let item = ItineraryItem(
|
||||
tripId: trip.id,
|
||||
day: displayDay,
|
||||
sortOrder: 0, // Travel always comes first in day
|
||||
kind: .travel(travelInfo)
|
||||
)
|
||||
itineraryItems.append(item)
|
||||
do {
|
||||
_ = try await ItineraryItemService.shared.createItem(item)
|
||||
} catch {
|
||||
print("❌ [TravelOverrides] CloudKit save failed: \(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
print("✅ [TravelOverrides] Saved to CloudKit")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1413,7 +1442,7 @@ struct TripDetailView: View {
|
||||
enum ItinerarySection {
|
||||
case day(dayNumber: Int, date: Date, games: [RichGame])
|
||||
case travel(TravelSegment)
|
||||
case customItem(CustomItineraryItem)
|
||||
case customItem(ItineraryItem)
|
||||
case addButton(day: Int)
|
||||
|
||||
var isCustomItem: Bool {
|
||||
@@ -1807,7 +1836,7 @@ struct TripMapView: View {
|
||||
@Binding var cameraPosition: MapCameraPosition
|
||||
let routeCoordinates: [[CLLocationCoordinate2D]]
|
||||
let stopCoordinates: [(name: String, coordinate: CLLocationCoordinate2D)]
|
||||
let customItems: [CustomItineraryItem]
|
||||
let customItems: [ItineraryItem]
|
||||
let colorScheme: ColorScheme
|
||||
let routeVersion: UUID // Force re-render when routes change
|
||||
|
||||
@@ -1841,15 +1870,14 @@ struct TripMapView: View {
|
||||
|
||||
// Custom item markers
|
||||
ForEach(customItems, id: \.id) { item in
|
||||
if let coordinate = item.coordinate {
|
||||
Annotation(item.title, coordinate: coordinate) {
|
||||
if let info = item.customInfo, let coordinate = info.coordinate {
|
||||
Annotation(info.title, coordinate: coordinate) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Theme.warmOrange)
|
||||
.frame(width: 24, height: 24)
|
||||
Image(systemName: item.category.systemImage)
|
||||
Text(info.icon)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user