Repository Public
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// MapView.swift — la CARTE UNIFIÉE : les résultats de plusieurs univers autour3// de l'utilisateur (logements, propriétés, emplois, restos, sorties) sur une4// même carte, épingles teintées par univers, filtre par univers.5import SwiftUI6import MapKit7import CoreLocation89struct UnifiedMapView: View {10 @State private var camera: MapCameraPosition = .region(11 MKCoordinateRegion(center: .init(latitude: 46.81, longitude: -71.21), // Québec12 span: .init(latitudeDelta: 0.35, longitudeDelta: 0.35))13 )14 @State private var items: [KAItem] = []15 @State private var enabled: Set<String> = ["lou-ka", "immo-ka", "resto-ka", "sorti-ka", "job-ka"]16 @State private var loading = false17 @State private var selection: KAItem?18 @State private var visibleRegion: MKCoordinateRegion?19 @StateObject private var location = LocationOnce()20 @Environment(\.colorScheme) private var scheme21 @Environment(\.dismiss) private var dismiss2223 private var mapUniverses: [Universe] {24 Ecosystem.all.filter { ["lou-ka", "immo-ka", "resto-ka", "sorti-ka", "job-ka"].contains($0.id) }25 }2627 var body: some View {28 NavigationStack {29 Map(position: $camera, selection: $selection) {30 UserAnnotation()31 ForEach(items.filter { enabled.contains($0.universeID) }) { item in32 if let lat = item.latitude, let lon = item.longitude,33 let u = Ecosystem.universe(item.universeID) {34 Marker(item.title, systemImage: u.symbol,35 coordinate: .init(latitude: lat, longitude: lon))36 .tint(u.accent)37 .tag(item)38 }39 }40 }41 .mapStyle(.standard(elevation: .flat, pointsOfInterest: .excludingAll))42 .onMapCameraChange(frequency: .onEnd) { ctx in visibleRegion = ctx.region }43 .safeAreaInset(edge: .top) { chips }44 .safeAreaInset(edge: .bottom) { bottomBar }45 .sheet(item: $selection) { item in46 NavigationStack { ItemDetailView(item: item) }47 .presentationDetents([.medium, .large])48 }49 .navigationTitle("Carte")50 .navigationBarTitleDisplayMode(.inline)51 .toolbar {52 ToolbarItem(placement: .topBarLeading) {53 Button { dismiss() } label: { Image(systemName: "xmark.circle.fill").foregroundStyle(.secondary) }54 .accessibilityLabel("Fermer la carte")55 }56 }57 .task {58 location.request()59 await load()60 }61 .onChange(of: location.coordinate != nil) {62 if let c = location.coordinate {63 camera = .region(.init(center: c, span: .init(latitudeDelta: 0.25, longitudeDelta: 0.25)))64 Task { await load() }65 }66 }67 }68 }6970 private var chips: some View {71 ScrollView(.horizontal, showsIndicators: false) {72 HStack(spacing: 8) {73 ForEach(mapUniverses) { u in74 let on = enabled.contains(u.id)75 Button {76 Haptics.tap()77 if on { enabled.remove(u.id) } else { enabled.insert(u.id) }78 } label: {79 Label(u.wordmark, systemImage: u.symbol)80 .font(.system(.caption, design: .monospaced).weight(.bold))81 .padding(.horizontal, 11).padding(.vertical, 8)82 .background(on ? u.accent : KATheme.surface(scheme), in: Capsule())83 .foregroundStyle(on ? .white : .secondary)84 .overlay(Capsule().strokeBorder(.black.opacity(0.25), lineWidth: 1))85 }86 }87 }88 .padding(.horizontal, 14).padding(.vertical, 8)89 }90 .background(.ultraThinMaterial)91 }9293 private var bottomBar: some View {94 HStack {95 Text(loading ? "Chargement autour d'ici…"96 : "\(items.filter { enabled.contains($0.universeID) && $0.latitude != nil }.count) résultats sur la carte")97 .font(.system(.caption, design: .monospaced).weight(.bold))98 Spacer()99 Button {100 Haptics.rigid()101 Task { await load() }102 } label: {103 Label("Chercher ici", systemImage: "arrow.clockwise")104 .font(.caption.weight(.bold))105 .padding(.horizontal, 13).padding(.vertical, 9)106 .background(KATheme.inkLight, in: Capsule())107 .foregroundStyle(KATheme.lime)108 }109 }110 .padding(.horizontal, 14).padding(.vertical, 9)111 .background(.ultraThinMaterial)112 }113114 /// Charge les items géolocalisés des univers actifs (autour de la vue).115 private func load() async {116 loading = true117 var all: [KAItem] = []118 await withTaskGroup(of: [KAItem].self) { group in119 for u in mapUniverses {120 group.addTask { (try? await UniverseService.fetch(u, limit: 80)) ?? [] }121 }122 for await batch in group { all.append(contentsOf: batch) }123 }124 items = all.filter { $0.latitude != nil && $0.longitude != nil }125 loading = false126 if !items.isEmpty { Haptics.tap() }127 }128}129130/// Demande de localisation « une fois » (jamais de suivi continu).131final class LocationOnce: NSObject, ObservableObject, CLLocationManagerDelegate {132 @Published var coordinate: CLLocationCoordinate2D?133 private let manager = CLLocationManager()134135 func request() {136 manager.delegate = self137 if manager.authorizationStatus == .notDetermined {138 manager.requestWhenInUseAuthorization()139 } else {140 manager.requestLocation()141 }142 }143144 func locationManagerDidChangeAuthorization(_ m: CLLocationManager) {145 if m.authorizationStatus == .authorizedWhenInUse || m.authorizationStatus == .authorizedAlways {146 m.requestLocation()147 }148 }149 func locationManager(_ m: CLLocationManager, didUpdateLocations locs: [CLLocation]) {150 coordinate = locs.first?.coordinate151 }152 func locationManager(_ m: CLLocationManager, didFailWithError error: Error) {}153}154