Skip to main content

Interaktive Kartenübersicht

Beschreibung

Diese View zeigt eine interaktive Karte an, auf der Nutzer verschiedene Bereiche erkunden können. Durch Antippen von Standorten erhalten sie detaillierte Informationen; benutzerfreundliche Icons und Tooltips erleichtern die Interaktion und sorgen für Übersichtlichkeit.

🔍 Zweck

  • Stadtführer mit Sehenswürdigkeiten zum Antippen anzeigen
  • Event-Locations auf einer Messekarte präsentieren
  • Gastronomische Angebote auf einer Stadtkarte darstellen
  • Immobilienstandorte auf einer Karte visualisieren und auswählen lassen
  • Naturparks mit interaktiven Informationstafeln abbilden

📄 Codebeispiel

import SwiftUI
import MapKit

struct MapLocationPoint: Identifiable, Equatable {
    // Use name and coordinate hash to create a stable, predictable ID for static demo data.
    let id: String
    let name: String
    let coordinate: CLLocationCoordinate2D
    let iconName: String           // SF Symbol for pin/icon display
    let description: String?
    
    init(name: String, coordinate: CLLocationCoordinate2D, iconName: String, description: String? = nil) {
        self.name = name
        self.coordinate = coordinate
        self.iconName = iconName
        self.description = description
        // Generate stable id from name and coordinates to prevent multiple UUIDs for same data
        self.id = "\(name)_\(coordinate.latitude)_\(coordinate.longitude)"
    }
    
    static func == (lhs: MapLocationPoint, rhs: MapLocationPoint) -> Bool {
        // Compare all properties that define equality
        lhs.id == rhs.id
    }
}

// MARK: - Demo data

let demoLocations: [MapLocationPoint] = [
    MapLocationPoint(
        name: "Brandenburger Tor",
        coordinate: CLLocationCoordinate2D(latitude: 52.5163, longitude: 13.3777),
        iconName: "building.columns.fill",
        description: "Berühmtes Wahrzeichen Berlins"
    ),
    MapLocationPoint(
        name: "Museumsinsel",
        coordinate: CLLocationCoordinate2D(latitude: 52.5169, longitude: 13.4019),
        iconName: "paintpalette.fill",
        description: "Zahlreiche renommierte Museen"
    ),
    MapLocationPoint(
        name: "Tiergarten",
        coordinate: CLLocationCoordinate2D(latitude: 52.5145, longitude: 13.3501),
        iconName: "leaf.fill",
        description: "Grüne Oase mitten in der Stadt"
    ),
]

struct ContentView: View {
    @State private var mapRegion: MKCoordinateRegion = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 52.5170, longitude: 13.3889),
        span: MKCoordinateSpan(latitudeDelta: 0.04, longitudeDelta: 0.09)
    )
    
    @State private var selectedLocation: MapLocationPoint? = nil
    
    var body: some View {
        ZStack(alignment: .topLeading) {
            // Main interactive map with annotation items
            Map(coordinateRegion: $mapRegion,
                annotationItems: demoLocations
            ) { location in
                // Use MapAnnotation for custom marker with tap interaction.
                MapAnnotation(coordinate: location.coordinate) {
                    Button {
                        // On tap, set as selected (shows info sheet)
                        selectedLocation = location
                    } label: {
                        VStack(spacing: -2) {
                            Image(systemName: location.iconName)
                                .resizable()
                                .frame(width: 30, height: 30)
                                .foregroundColor(selectedLocation == location ? Color.accentColor : Color.blue)
                                // Show glow effect if selected
                                .shadow(
                                    color: selectedLocation == location ? Color.accentColor.opacity(0.5) : .clear,
                                    radius: selectedLocation == location ? 7 : 0
                                )
                            // Tooltip-style label below the icon
                            ZStack {
                                Capsule()
                                    .fill(Color.white.opacity(0.95))
                                    .shadow(radius: 2)
                                Text(location.name)
                                    .font(.caption2)
                                    .bold()
                                    .foregroundColor(.black)
                                    .padding(.horizontal, 7)
                                    .padding(.vertical, 1)
                            }
                            .fixedSize()
                            .opacity(selectedLocation != location ? 1 : 0)
                            // Small pointer triangle
                            TrianglePointer()
                                .fill(Color.white.opacity(0.95))
                                .frame(width: 12, height: 6)
                                .offset(y: -4)
                                .opacity(selectedLocation != location ? 1 : 0)
                        }
                        .animation(.spring(duration: 0.2), value: selectedLocation)
                    }
                    .buttonStyle(.plain)
                    .accessibilityLabel(Text(location.name))
                }
            }
            .ignoresSafeArea() // Use current API
            
            // Overlay info bar
            HStack {
                Image(systemName: "map.fill")
                    .foregroundColor(.accentColor)
                Text("Interaktive Karte – tippe auf Symbole!")
                    .fontWeight(.medium)
                    .foregroundColor(.primary)
            }
            .padding(12)
            .background(
                RoundedRectangle(cornerRadius: 14)
                    .fill(Color.white.opacity(0.9))
                    .shadow(radius: 3)
            )
            .padding([.top, .leading], 20)
        }
        // Sheet with detailed info for selected marker/location
        .sheet(item: $selectedLocation) { loc in
            VStack(spacing: 12) {
                Image(systemName: loc.iconName)
                    .resizable()
                    .scaledToFit()
                    .frame(width: 55, height: 55)
                    .foregroundColor(Color.accentColor)
                    .shadow(radius: 6)
                
                Text(loc.name)
                    .font(.title3)
                    .bold()
                
                if let desc = loc.description {
                    Text(desc)
                        .multilineTextAlignment(.center)
                        .font(.body)
                        .foregroundColor(.secondary)
                }
                
                Button(role: .cancel) {
                    selectedLocation = nil
                } label: {
                    Label("Schließen", systemImage: "xmark.circle")
                }
            }
            .padding()
            .presentationDetents([.height(230)])
        }
    }
}

// MARK: - Tooltip triangle pointer for marker label

struct TrianglePointer: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        // Draws a simple triangle pointer
        path.move(to: CGPoint(x: 0, y: 0))
        path.addLine(to: CGPoint(x: rect.width / 2, y: rect.height))
        path.addLine(to: CGPoint(x: rect.width, y: 0))
        path.closeSubpath()
        return path
    }
}

#Preview {
    NavigationStack {
        ContentView()
    }
}