SPB Git

Repository Public

MapView.swift 6.4 KB 154 lines swift

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
Files 063bd98dc2bd4f684732135976520f56d84742b6
Menu

Theme