Karten: zentriert auf Standort + Re-Center-Button

- GiftkoederView: Map.position als State (war initialPosition, einmalig),
  Re-Center-Button rechts oben (location.fill in Capsule auf material),
  bei Standort-Update wird Kamera mit easeInOut animiert auf Position
- VerloreneHundeView: dieselbe Map-Struktur ergänzt, fehlte komplett.
  Pins als magnifyingglass.circle.fill in Akzentfarbe, tappable → öffnet
  LostDogDetailView als Sheet (mit eigenem 'Schließen'-Button)
This commit is contained in:
rene 2026-05-30 13:37:42 +02:00
parent fb00468c8c
commit a6ea7b5b8f
2 changed files with 147 additions and 44 deletions

View file

@ -7,6 +7,7 @@ struct GiftkoederView: View {
@State private var isLoading = false @State private var isLoading = false
@State private var errorMessage: String? @State private var errorMessage: String?
@State private var showReport = false @State private var showReport = false
@State private var cameraPosition: MapCameraPosition = .automatic
var body: some View { var body: some View {
Group { Group {
@ -37,6 +38,7 @@ struct GiftkoederView: View {
} }
.task { location.request() } .task { location.request() }
.onChange(of: location.coordinate?.latitude) { _, _ in .onChange(of: location.coordinate?.latitude) { _, _ in
if let c = location.coordinate { centerOn(c) }
Task { await load() } Task { await load() }
} }
.sheet(isPresented: $showReport) { .sheet(isPresented: $showReport) {
@ -48,10 +50,7 @@ struct GiftkoederView: View {
private func content(at coord: CLLocationCoordinate2D) -> some View { private func content(at coord: CLLocationCoordinate2D) -> some View {
VStack(spacing: 0) { VStack(spacing: 0) {
Map(initialPosition: .region(MKCoordinateRegion( Map(position: $cameraPosition) {
center: coord,
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
))) {
UserAnnotation() UserAnnotation()
ForEach(alerts) { alert in ForEach(alerts) { alert in
Annotation(alert.typ ?? "Giftköder", Annotation(alert.typ ?? "Giftköder",
@ -67,6 +66,20 @@ struct GiftkoederView: View {
} }
.frame(height: 260) .frame(height: 260)
.ignoresSafeArea(edges: .top) .ignoresSafeArea(edges: .top)
.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 alerts.isEmpty && !isLoading { if alerts.isEmpty && !isLoading {
ContentUnavailableView( ContentUnavailableView(
@ -85,6 +98,15 @@ struct GiftkoederView: View {
} }
} }
private func centerOn(_ coord: CLLocationCoordinate2D) {
withAnimation(.easeInOut(duration: 0.4)) {
cameraPosition = .region(MKCoordinateRegion(
center: coord,
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
))
}
}
private func load() async { private func load() async {
guard let coord = location.coordinate else { return } guard let coord = location.coordinate else { return }
isLoading = true isLoading = true

View file

@ -8,57 +8,138 @@ struct VerloreneHundeView: View {
@State private var isLoading = false @State private var isLoading = false
@State private var errorMessage: String? @State private var errorMessage: String?
@State private var showReport = false @State private var showReport = false
@State private var cameraPosition: MapCameraPosition = .automatic
@State private var tappedDog: LostDog?
var body: some View { var body: some View {
content Group {
.navigationTitle("Verlorene Hunde") if let coord = location.coordinate {
.navigationBarTitleDisplayMode(.inline) content(at: coord)
.toolbar { } else if location.error != nil {
if location.coordinate != nil { ContentUnavailableView(
ToolbarItem(placement: .topBarTrailing) { "Kein Standort",
Button { systemImage: "location.slash",
showReport = true description: Text(location.error ?? "")
} label: { )
Label("Melden", systemImage: "exclamationmark.bubble") } else {
} ProgressView("Hole Standort…")
}
}
.navigationTitle("Verlorene Hunde")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
if location.coordinate != nil {
ToolbarItem(placement: .topBarTrailing) {
Button {
showReport = true
} label: {
Label("Melden", systemImage: "exclamationmark.bubble")
} }
} }
} }
.sheet(isPresented: $showReport) { }
if let coord = location.coordinate { .sheet(isPresented: $showReport) {
ReportLostDogSheet(coord: coord) { await load() } if let coord = location.coordinate {
} ReportLostDogSheet(coord: coord) { await load() }
} }
.task { }
location.request() .sheet(item: $tappedDog) { dog in
await load() NavigationStack {
LostDogDetailView(dog: dog)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Schließen") { tappedDog = nil }
}
}
} }
.onChange(of: location.coordinate?.latitude) { _, _ in }
Task { await load() } .task {
} location.request()
.refreshable { await load() } await load()
}
.onChange(of: location.coordinate?.latitude) { _, _ in
if let c = location.coordinate { centerOn(c) }
Task { await load() }
}
.refreshable { await load() }
} }
@ViewBuilder private func content(at coord: CLLocationCoordinate2D) -> some View {
private var content: some View { VStack(spacing: 0) {
if isLoading && lostDogs.isEmpty { Map(position: $cameraPosition) {
ProgressView() UserAnnotation()
} else if let errorMessage, lostDogs.isEmpty { ForEach(lostDogs) { dog in
ContentUnavailableView("Konnte nicht laden", systemImage: "wifi.slash", description: Text(errorMessage)) Annotation(dog.name,
} else if lostDogs.isEmpty { coordinate: CLLocationCoordinate2D(latitude: dog.lat, longitude: dog.lon)
ContentUnavailableView( ) {
"Keine vermissten Hunde", Button {
systemImage: "checkmark.circle", tappedDog = dog
description: Text("In 25 km Umkreis sind aktuell keine Vermisstmeldungen aktiv.") } label: {
) Image(systemName: "magnifyingglass.circle.fill")
} else { .font(.title2)
List(lostDogs) { dog in .foregroundStyle(.white, Color.accentColor)
NavigationLink { .background(.white, in: Circle())
LostDogDetailView(dog: dog) .shadow(radius: 2)
} label: { }
LostDogRow(dog: dog) .buttonStyle(.plain)
}
} }
} }
.frame(height: 260)
.ignoresSafeArea(edges: .top)
.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 && lostDogs.isEmpty {
ProgressView().padding(.top, 30)
Spacer()
} else if let errorMessage, lostDogs.isEmpty {
ContentUnavailableView(
"Konnte nicht laden",
systemImage: "wifi.slash",
description: Text(errorMessage)
)
.padding(.top, 30)
Spacer()
} else if lostDogs.isEmpty {
ContentUnavailableView(
"Keine vermissten Hunde",
systemImage: "checkmark.circle",
description: Text("In 25 km Umkreis sind aktuell keine Vermisstmeldungen aktiv.")
)
.padding(.top, 30)
Spacer()
} else {
List(lostDogs) { dog in
NavigationLink {
LostDogDetailView(dog: dog)
} label: {
LostDogRow(dog: dog)
}
}
.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)
))
} }
} }