- Create shared TipsSection component for displaying planning tips - Add TipsSection to all 22 home content variants - Fix displayedTips population with onAppear in HomeView - Add map buttons to GameRowCompact (opens stadium in Apple Maps) - Add map buttons to TravelRowView (opens driving directions) - Add map buttons to CustomItemRowView (opens location when GPS available) - Add AppleMapsLauncher.openLocation() and openDirections() methods Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
173 lines
5.4 KiB
Swift
173 lines
5.4 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])
|
|
}
|
|
|
|
/// Opens a single location in Apple Maps
|
|
/// - Parameters:
|
|
/// - coordinate: Location to display
|
|
/// - name: Display name for the pin
|
|
static func openLocation(coordinate: CLLocationCoordinate2D, name: String) {
|
|
let placemark = MKPlacemark(coordinate: coordinate)
|
|
let mapItem = MKMapItem(placemark: placemark)
|
|
mapItem.name = name
|
|
mapItem.openInMaps(launchOptions: nil)
|
|
}
|
|
|
|
/// Opens driving directions between two points in Apple Maps
|
|
/// - Parameters:
|
|
/// - from: Starting location
|
|
/// - fromName: Display name for origin
|
|
/// - to: Destination location
|
|
/// - toName: Display name for destination
|
|
static func openDirections(
|
|
from: CLLocationCoordinate2D,
|
|
fromName: String,
|
|
to: CLLocationCoordinate2D,
|
|
toName: String
|
|
) {
|
|
let fromPlacemark = MKPlacemark(coordinate: from)
|
|
let fromItem = MKMapItem(placemark: fromPlacemark)
|
|
fromItem.name = fromName
|
|
|
|
let toPlacemark = MKPlacemark(coordinate: to)
|
|
let toItem = MKMapItem(placemark: toPlacemark)
|
|
toItem.name = toName
|
|
|
|
MKMapItem.openMaps(with: [fromItem, toItem], launchOptions: [
|
|
MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving
|
|
])
|
|
}
|
|
|
|
// 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)])
|
|
}
|
|
}
|
|
}
|