Gassi-Treffen-Liste: Karte oben mit Treffpunkt-Pins (analog Verlorene/Giftköder)

- 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
This commit is contained in:
rene 2026-05-30 14:21:38 +02:00
parent 00dba257b5
commit 09a90f7325

View file

@ -1,4 +1,5 @@
import SwiftUI import SwiftUI
import MapKit
struct GassiTreffenList: View { struct GassiTreffenList: View {
@State private var location = OneShotLocation() @State private var location = OneShotLocation()
@ -6,57 +7,137 @@ struct GassiTreffenList: View {
@State private var isLoading = false @State private var isLoading = false
@State private var errorMessage: String? @State private var errorMessage: String?
@State private var showAdd = false @State private var showAdd = false
@State private var cameraPosition: MapCameraPosition = .automatic
@State private var tappedMeeting: WalkMeeting?
var body: some View { var body: some View {
content Group {
.toolbar { if let coord = location.coordinate {
ToolbarItem(placement: .topBarTrailing) { content(at: coord)
Button { } else if location.error != nil {
showAdd = true ContentUnavailableView(
} label: { "Kein Standort",
Label("Planen", systemImage: "plus") 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()
.task { await load()
location.request() }
await load() .onChange(of: location.coordinate?.latitude) { _, _ in
} if let c = location.coordinate { centerOn(c) }
.onChange(of: location.coordinate?.latitude) { _, _ in Task { await load() }
Task { await load() } }
} .refreshable { await load() }
.refreshable { await load() }
} }
@ViewBuilder private func content(at coord: CLLocationCoordinate2D) -> some View {
private var content: some View { VStack(spacing: 0) {
if isLoading && meetings.isEmpty { Map(position: $cameraPosition) {
ProgressView() UserAnnotation()
} else if let errorMessage, meetings.isEmpty { ForEach(meetings) { meeting in
ContentUnavailableView( Annotation(meeting.titel,
"Konnte nicht laden", coordinate: CLLocationCoordinate2D(latitude: meeting.lat, longitude: meeting.lon)
systemImage: "wifi.slash", ) {
description: Text(errorMessage) Button {
) tappedMeeting = meeting
} else if meetings.isEmpty { } label: {
ContentUnavailableView( Image(systemName: "pawprint.circle.fill")
"Noch keine Treffen", .font(.title2)
systemImage: "person.2", .foregroundStyle(.white, Color.accentColor)
description: Text("In 20 km Umkreis sind aktuell keine Gassi-Treffen geplant. Tippe oben rechts auf +, um eins zu planen.") .background(.white, in: Circle())
) .shadow(radius: 2)
} else { }
List(meetings) { meeting in .buttonStyle(.plain)
NavigationLink {
GassiTreffenDetail(meetingId: meeting.id, fallbackTitle: meeting.titel) {
await load()
} }
} 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)
))
} }
} }