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:
parent
00dba257b5
commit
09a90f7325
1 changed files with 122 additions and 41 deletions
|
|
@ -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)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue