Files
Sportstime/SportsTime/Core/Services/AppleMapsLauncher.swift
Trey t 239d22a872 feat(trip): add Open in Apple Maps button to trip detail map
- Add floating action button (bottom-right) on trip map
- Create AppleMapsLauncher service to handle route opening
- Collect all routable stops (trip stops + custom items with coords)
- Handle >16 waypoints with alert offering to open in parts
- Silently skip stops without coordinates

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 10:59:53 -06:00

137 lines
4.1 KiB
Swift

//
// AppleMapsLauncher.swift
// SportsTime
//
// Service for opening trip routes in Apple Maps
//
import Foundation
import MapKit
import CoreLocation
/// Result of preparing waypoints for Apple Maps
enum AppleMapsLaunchResult {
/// Single route can be opened directly (16 waypoints)
case ready([MKMapItem])
/// Multiple routes needed (>16 waypoints)
case multipleRoutes([[MKMapItem]])
/// No routable waypoints available
case noWaypoints
}
/// Handles opening trip routes in Apple Maps with chunking for large trips
struct AppleMapsLauncher {
/// Maximum waypoints Apple Maps supports in a single route
static let maxWaypoints = 16
/// Represents a waypoint for the route
struct Waypoint {
let name: String
let coordinate: CLLocationCoordinate2D
let sortKey: (day: Int, order: Double)
}
// MARK: - Public API
/// Prepares waypoints from trip stops and custom items
/// - Parameters:
/// - stops: Trip stops with optional coordinates
/// - customItems: Custom itinerary items that may have coordinates
/// - Returns: Launch result indicating if route is ready or needs chunking
static func prepare(
stops: [TripStop],
customItems: [ItineraryItem]
) -> AppleMapsLaunchResult {
let waypoints = collectWaypoints(stops: stops, customItems: customItems)
guard !waypoints.isEmpty else {
return .noWaypoints
}
let mapItems = waypoints.map { waypoint -> MKMapItem in
let placemark = MKPlacemark(coordinate: waypoint.coordinate)
let item = MKMapItem(placemark: placemark)
item.name = waypoint.name
return item
}
if mapItems.count <= maxWaypoints {
return .ready(mapItems)
} else {
let chunks = chunk(mapItems, size: maxWaypoints)
return .multipleRoutes(chunks)
}
}
/// Opens a single route in Apple Maps
/// - Parameter mapItems: Array of map items (16)
static func open(_ mapItems: [MKMapItem]) {
guard !mapItems.isEmpty else { return }
MKMapItem.openMaps(with: mapItems, launchOptions: [
MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving
])
}
/// Opens a specific chunk of a multi-route trip
/// - Parameters:
/// - chunks: All route chunks
/// - index: Which chunk to open (0-based)
static func open(chunk index: Int, of chunks: [[MKMapItem]]) {
guard index >= 0 && index < chunks.count else { return }
open(chunks[index])
}
// MARK: - Private Helpers
private static func collectWaypoints(
stops: [TripStop],
customItems: [ItineraryItem]
) -> [Waypoint] {
var waypoints: [Waypoint] = []
// Add trip stops with coordinates
for (index, stop) in stops.enumerated() {
guard let coordinate = stop.coordinate else { continue }
waypoints.append(Waypoint(
name: stop.city,
coordinate: coordinate,
// Use stop number for day approximation, index for order
sortKey: (day: stop.stopNumber, order: Double(index))
))
}
// Add mappable custom items
for item in customItems {
guard let info = item.customInfo,
let coordinate = info.coordinate else { continue }
waypoints.append(Waypoint(
name: info.title,
coordinate: coordinate,
sortKey: (day: item.day, order: item.sortOrder)
))
}
// Sort by day, then by sort order within day
waypoints.sort { lhs, rhs in
if lhs.sortKey.day != rhs.sortKey.day {
return lhs.sortKey.day < rhs.sortKey.day
}
return lhs.sortKey.order < rhs.sortKey.order
}
return waypoints
}
private static func chunk(_ items: [MKMapItem], size: Int) -> [[MKMapItem]] {
stride(from: 0, to: items.count, by: size).map { start in
Array(items[start..<min(start + size, items.count)])
}
}
}