From 09a90f7325d02a0ccf06be57963aeba35ea7f9fe Mon Sep 17 00:00:00 2001 From: rene Date: Sat, 30 May 2026 14:21:38 +0200 Subject: [PATCH] =?UTF-8?q?Gassi-Treffen-Liste:=20Karte=20oben=20mit=20Tre?= =?UTF-8?q?ffpunkt-Pins=20(analog=20Verlorene/Giftk=C3=B6der)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Map(position:) mit UserAnnotation + Pin pro Treffen (pawprint.circle.fill) - Tap auf Pin öffnet das Detail-Sheet (mit 'Schließen'-Button) - Re-Center-Button oben rechts, .onAppear zentriert auf User - Drunter wie gehabt: Liste mit NavigationLink, Empty-State, Spinner --- BanYaroGo/Views/GassiTreffenList.swift | 163 ++++++++++++++++++------- 1 file changed, 122 insertions(+), 41 deletions(-) diff --git a/BanYaroGo/Views/GassiTreffenList.swift b/BanYaroGo/Views/GassiTreffenList.swift index eaa6345..c476877 100644 --- a/BanYaroGo/Views/GassiTreffenList.swift +++ b/BanYaroGo/Views/GassiTreffenList.swift @@ -1,4 +1,5 @@ import SwiftUI +import MapKit struct GassiTreffenList: View { @State private var location = OneShotLocation() @@ -6,57 +7,137 @@ struct GassiTreffenList: View { @State private var isLoading = false @State private var errorMessage: String? @State private var showAdd = false + @State private var cameraPosition: MapCameraPosition = .automatic + @State private var tappedMeeting: WalkMeeting? var body: some View { - content - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Button { - showAdd = true - } label: { - Label("Planen", systemImage: "plus") + Group { + if let coord = location.coordinate { + content(at: coord) + } else if location.error != nil { + ContentUnavailableView( + "Kein Standort", + systemImage: "location.slash", + description: Text(location.error ?? "") + ) + } else { + ProgressView("Hole Standort…") + } + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + showAdd = true + } label: { + Label("Planen", systemImage: "plus") + } + } + } + .sheet(isPresented: $showAdd) { + AddWalkSheet(coord: location.coordinate) { await load() } + } + .sheet(item: $tappedMeeting) { meeting in + NavigationStack { + GassiTreffenDetail(meetingId: meeting.id, fallbackTitle: meeting.titel) { + await load() + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("Schließen") { tappedMeeting = nil } } } } - .sheet(isPresented: $showAdd) { - AddWalkSheet(coord: location.coordinate) { await load() } - } - .task { - location.request() - await load() - } - .onChange(of: location.coordinate?.latitude) { _, _ in - Task { await load() } - } - .refreshable { await load() } + } + .task { + location.request() + await load() + } + .onChange(of: location.coordinate?.latitude) { _, _ in + if let c = location.coordinate { centerOn(c) } + Task { await load() } + } + .refreshable { await load() } } - @ViewBuilder - private var content: some View { - if isLoading && meetings.isEmpty { - ProgressView() - } else if let errorMessage, meetings.isEmpty { - ContentUnavailableView( - "Konnte nicht laden", - systemImage: "wifi.slash", - description: Text(errorMessage) - ) - } else if meetings.isEmpty { - ContentUnavailableView( - "Noch keine Treffen", - systemImage: "person.2", - description: Text("In 20 km Umkreis sind aktuell keine Gassi-Treffen geplant. Tippe oben rechts auf +, um eins zu planen.") - ) - } else { - List(meetings) { meeting in - NavigationLink { - GassiTreffenDetail(meetingId: meeting.id, fallbackTitle: meeting.titel) { - await load() + private func content(at coord: CLLocationCoordinate2D) -> some View { + VStack(spacing: 0) { + Map(position: $cameraPosition) { + UserAnnotation() + ForEach(meetings) { meeting in + Annotation(meeting.titel, + coordinate: CLLocationCoordinate2D(latitude: meeting.lat, longitude: meeting.lon) + ) { + Button { + tappedMeeting = meeting + } label: { + Image(systemName: "pawprint.circle.fill") + .font(.title2) + .foregroundStyle(.white, Color.accentColor) + .background(.white, in: Circle()) + .shadow(radius: 2) + } + .buttonStyle(.plain) } - } label: { - TreffenRow(meeting: meeting) } } + .frame(height: 260) + .ignoresSafeArea(edges: .top) + .onAppear { centerOn(coord) } + .overlay(alignment: .topTrailing) { + Button { + centerOn(coord) + } label: { + Image(systemName: "location.fill") + .font(.callout.bold()) + .foregroundStyle(Color.accentColor) + .padding(10) + .background(.thinMaterial, in: Circle()) + .shadow(radius: 2) + } + .padding(.top, 60) + .padding(.trailing, 12) + } + + if isLoading && meetings.isEmpty { + ProgressView().padding(.top, 30) + Spacer() + } else if let errorMessage, meetings.isEmpty { + ContentUnavailableView( + "Konnte nicht laden", + systemImage: "wifi.slash", + description: Text(errorMessage) + ) + .padding(.top, 30) + Spacer() + } else if meetings.isEmpty { + ContentUnavailableView( + "Noch keine Treffen", + systemImage: "person.2", + description: Text("In 20 km Umkreis sind aktuell keine Gassi-Treffen geplant. Tippe oben rechts auf +, um eins zu planen.") + ) + .padding(.top, 30) + Spacer() + } else { + List(meetings) { meeting in + NavigationLink { + GassiTreffenDetail(meetingId: meeting.id, fallbackTitle: meeting.titel) { + await load() + } + } label: { + TreffenRow(meeting: meeting) + } + } + .listStyle(.plain) + } + } + } + + private func centerOn(_ coord: CLLocationCoordinate2D) { + withAnimation(.easeInOut(duration: 0.4)) { + cameraPosition = .region(MKCoordinateRegion( + center: coord, + span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1) + )) } }