Tagebuch: Geolocation + Reverse-Geocoding beim Anlegen

- OneShotLocation beim Sheet-Öffnen, Toggle 'Standort verwenden' (Default an)
- CLGeocoder reverseGeocodeLocation (de_DE-Locale) füllt locationName aus
  Placemark.name, Fallback thoroughfare + locality. User kann überschreiben.
- gps_lat/gps_lon werden mit dem Eintrag gesendet, wenn Toggle an — die PWA
  rendert daraus die POI-Karte im Tagebuch.
This commit is contained in:
rene 2026-05-30 12:45:18 +02:00
parent 12f8ba0be8
commit 3059a5eb2c

View file

@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import PhotosUI import PhotosUI
import CoreLocation
struct AddDiaryEntrySheet: View { struct AddDiaryEntrySheet: View {
let dogId: Int let dogId: Int
@ -17,6 +18,10 @@ struct AddDiaryEntrySheet: View {
@State private var photoSelection: [PhotosPickerItem] = [] @State private var photoSelection: [PhotosPickerItem] = []
@State private var photoData: [Data] = [] @State private var photoData: [Data] = []
@State private var location = OneShotLocation()
@State private var attachLocation = true
@State private var isReverseGeocoding = false
@State private var saveState: SaveState = .idle @State private var saveState: SaveState = .idle
@State private var errorMessage: String? @State private var errorMessage: String?
@ -42,12 +47,55 @@ struct AddDiaryEntrySheet: View {
} }
Section { Section {
Toggle("Meilenstein", isOn: $isMilestone) Toggle("Meilenstein", isOn: $isMilestone)
TextField("Ort (optional)", text: $locationName)
TextField("Tags (komma-getrennt)", text: $tagsInput) TextField("Tags (komma-getrennt)", text: $tagsInput)
.textInputAutocapitalization(.never) .textInputAutocapitalization(.never)
.autocorrectionDisabled() .autocorrectionDisabled()
} }
Section {
Toggle(isOn: $attachLocation) {
Label("Standort verwenden", systemImage: "location.fill")
}
if attachLocation {
if let coord = location.coordinate {
HStack {
Image(systemName: "mappin.and.ellipse")
.foregroundStyle(Color.accentColor)
VStack(alignment: .leading, spacing: 2) {
if !locationName.isEmpty {
Text(locationName).font(.subheadline)
} else if isReverseGeocoding {
Text("Suche Ortsbezeichnung…")
.font(.caption)
.foregroundStyle(.secondary)
} else {
Text("Ort wird mitgespeichert")
.font(.subheadline)
}
Text(String(format: "%.5f, %.5f", coord.latitude, coord.longitude))
.font(.caption2.monospacedDigit())
.foregroundStyle(.tertiary)
}
}
} else if location.error != nil {
Label("Standort nicht verfügbar — wird ohne gespeichert", systemImage: "location.slash")
.font(.caption)
.foregroundStyle(.secondary)
} else {
HStack {
ProgressView()
Text("Hole Standort…").font(.caption).foregroundStyle(.secondary)
}
}
TextField("Ort überschreiben (optional)", text: $locationName)
}
} header: {
Text("Standort")
} footer: {
Text("Der Standort wird zusammen mit dem Eintrag gespeichert — banyaro.app rendert daraus deine POI-Karte.")
.font(.caption2)
}
Section { Section {
PhotosPicker( PhotosPicker(
selection: $photoSelection, selection: $photoSelection,
@ -93,10 +141,37 @@ struct AddDiaryEntrySheet: View {
.onChange(of: photoSelection) { _, items in .onChange(of: photoSelection) { _, items in
Task { await loadPhotos(from: items) } Task { await loadPhotos(from: items) }
} }
.task { location.request() }
.onChange(of: location.coordinate?.latitude) { _, _ in
guard let coord = location.coordinate, locationName.isEmpty else { return }
Task { await reverseGeocode(coord) }
}
.interactiveDismissDisabled(saveState != .idle) .interactiveDismissDisabled(saveState != .idle)
} }
} }
private func reverseGeocode(_ coord: CLLocationCoordinate2D) async {
isReverseGeocoding = true
defer { isReverseGeocoding = false }
let geocoder = CLGeocoder()
let loc = CLLocation(latitude: coord.latitude, longitude: coord.longitude)
do {
let placemarks = try await geocoder.reverseGeocodeLocation(loc, preferredLocale: Locale(identifier: "de_DE"))
guard let p = placemarks.first else { return }
// Sample: "Café Kaiser, Berlin" name first, then locality fallback
if let name = p.name, !name.isEmpty {
locationName = name
return
}
var parts: [String] = []
if let street = p.thoroughfare { parts.append(street) }
if let city = p.locality { parts.append(city) }
if !parts.isEmpty { locationName = parts.joined(separator: ", ") }
} catch {
print("Reverse geocode failed: \(error)")
}
}
@ViewBuilder @ViewBuilder
private var saveToolbarItem: some View { private var saveToolbarItem: some View {
switch saveState { switch saveState {
@ -132,14 +207,15 @@ struct AddDiaryEntrySheet: View {
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty } .filter { !$0.isEmpty }
let coord = attachLocation ? location.coordinate : nil
let body = DiaryCreateBody( let body = DiaryCreateBody(
datum: formatter.string(from: date), datum: formatter.string(from: date),
typ: isMilestone ? "meilenstein" : "eintrag", typ: isMilestone ? "meilenstein" : "eintrag",
titel: titel.trimmingCharacters(in: .whitespaces).isEmpty ? nil : titel.trimmingCharacters(in: .whitespaces), titel: titel.trimmingCharacters(in: .whitespaces).isEmpty ? nil : titel.trimmingCharacters(in: .whitespaces),
text: text.trimmingCharacters(in: .whitespaces).isEmpty ? nil : text.trimmingCharacters(in: .whitespacesAndNewlines), text: text.trimmingCharacters(in: .whitespaces).isEmpty ? nil : text.trimmingCharacters(in: .whitespacesAndNewlines),
tags: tags.isEmpty ? nil : tags, tags: tags.isEmpty ? nil : tags,
gpsLat: nil, gpsLat: coord?.latitude,
gpsLon: nil, gpsLon: coord?.longitude,
locationName: locationName.trimmingCharacters(in: .whitespaces).isEmpty ? nil : locationName, locationName: locationName.trimmingCharacters(in: .whitespaces).isEmpty ? nil : locationName,
isMilestone: isMilestone isMilestone: isMilestone
) )