Tagebuch: Detail-Ansicht + tappbare Listen-Zeilen

DiaryDetailView mit Header (Datum, Meilenstein-Badge, Ort), Foto-
Galerie als TabView mit Page-Indicator, Volltext, Tags als Chips,
Mini-Karte für GPS-Eintrag mit Tap nach Apple Maps, und 'Eintrag
löschen' mit Bestätigungs-Alert → DELETE /api/dogs/{id}/diary/{eid}.

Liste in TagebuchView jetzt mit NavigationLink statt nur Anzeige.

App-Store-Material:
- AppStore/marketing.md mit Name, Untertitel, Beschreibung 2700
  Zeichen, Keywords, Privacy-Antworten, Reviewer-Notiz
- AppStore/screenshots/ — 8 Stück 1320x2868 (iPhone 17 Pro Max 6.9'')
This commit is contained in:
rene 2026-05-30 14:53:44 +02:00
parent 7848817cbe
commit 89d1d47ca4
11 changed files with 491 additions and 2 deletions

280
AppStore/marketing.md Normal file
View file

@ -0,0 +1,280 @@
# App Store Connect — Metadaten für Ban Yaro Go
Alle Texte sind so geschrieben, dass sie an den Apple-Zeichenlimits sitzen
und die Funktionen abbilden, die wirklich in der App sind. Wenn du was
ändern willst — am besten direkt hier editieren, dann hast du es für die
nächste Version griffbereit.
---
## App-Name (max. 30 Zeichen)
**Ban Yaro Go** *(11 Zeichen)*
## Untertitel (max. 30 Zeichen — wird unter dem Namen angezeigt)
Zur Auswahl, alle ≤30:
1. **Gassi-Tracker für Hundefans** *(27)* ← Empfehlung
2. **Gassi-Touren tracken & teilen** *(29)*
3. **Touren, Wetter, Treffen — Hund** *(30)*
4. **Dein Hund unterwegs** *(20)*
## Kategorien
- **Primär:** Gesundheit & Fitness
- **Sekundär:** Lifestyle
*(Alternative für Primär: „Lifestyle", wenn die Fitness-Aspekte zu schmal
wirken. Apples Reviewer akzeptiert beides — Gesundheit & Fitness ranked
in der Regel besser, weil HealthKit aktiv genutzt wird.)*
## Altersfreigabe
**4+** — keine sensiblen Inhalte. Beim App-Store-Fragebogen alles auf
„Keine" stellen außer:
- „Unbeschränkter Web-Zugriff": Nein (banyaro.app ist eingebettete Linkliste, keine WebView)
- „User-Generated Content": **Ja, selten** (Fotos, Tagebuch, Forumsverweis) — Apple verlangt dann „Moderation vorhanden" — auf banyaro.app gibt's Moderation, das passt
---
## Werbender Text / Promotional Text (max. 170 Zeichen)
*(Kann nach Veröffentlichung jederzeit ohne Re-Submission geändert werden — z. B. für Aktionen oder neue Features.)*
> Lass dich beim Gassi nicht ablenken — Ban Yaro Go zeichnet deine Tour auf, warnt vor Giftködern und zeigt das Wetter, das wirklich für deinen Hund passt.
*(167 Zeichen)*
---
## Beschreibung (max. 4000 Zeichen)
```
Ban Yaro Go ist die native iOS-Begleit-App zu banyaro.app, der Community
für Hundefreund:innen. Unterwegs zählt nicht jede Funktion — sondern
das, was du wirklich brauchst, wenn du draußen bist.
WAS DU UNTERWEGS BEKOMMST
• Gassi-Tour aufzeichnen — GPS-Track im Hintergrund, sogar bei
gesperrtem Bildschirm, mit Live-Aktivität in der Dynamischen Insel
und automatischer Pause beim Stehenbleiben
• Apple Health Integration — auf Wunsch wird jede Tour als
Spaziergang-Workout mit Route in Apple Health geschrieben
• Fotos vom Spaziergang — direkt mit Geolocation, hängen am
richtigen Streckenpunkt und landen im Tagebuch deines Hundes
• Gassi-Wetter — speziell für Hunde: gefühlte Asphalttemperatur,
Zecken-Warnung, UV-Index, Niederschlagsverlauf nächste Stunden,
basierend auf Apples WeatherKit
• Giftköder-Karte — andere Hundefreund:innen melden gesichtete
Köder mit Position und Foto, du siehst sie als Pin auf der Karte
GEMEINSAM UNTERWEGS
• Gassi-Treffen planen — Termin und Treffpunkt festlegen, andere
in der Nähe sehen das und können beitreten, alles mit Navigation
in Apple Maps
• Stamm-Gassi-Zeiten — deine regelmäßigen Runden eintragen, lokale
Erinnerungen auf dem iPhone, andere finden Verabredungen über
banyaro.app
• Verlorene Hunde — Melde-Pin mit Foto und Beschreibung in
der Umgebung, damit Suchhilfe schnell ankommt
ZUHAUSE
• Heim-Tab mit täglich wechselndem Hundebild und Schnellzugriff
auf deine wichtigsten Listen
• Tagebuch — Erlebnisse, Meilensteine, Fotos und GPS-Punkte zu
deinem Hund festhalten
• Ausgaben — Futter, Tierarzt, Spielzeug nach Kategorien
protokollieren
• GPX-Import — Tracks aus anderen Apps (Komoot, Outdooractive,
Files-App) per Teilen-Menü als Tour übernehmen
PRIVAT, KOSTENLOS, OPTIONAL
Ban Yaro Go ist Teil von banyaro.app — der gemeinnützigen Community
für Hundefreund:innen. Die Basis ist kostenlos, dauerhaft, ohne
Werbung. Premium-Funktionen (z. B. erweiterte Statistiken oder
unbegrenzte Cloud-Touren) kannst du auf banyaro.app buchen — die App
auf dem iPhone bleibt unabhängig nutzbar.
Hinweis zum Standort: Ban Yaro Go braucht „Standort: Immer", damit
deine Tour weiterläuft, wenn das iPhone in der Hosentasche steckt
oder der Bildschirm aus geht. Die Daten bleiben auf deinem Gerät und
landen nur in deinem banyaro-Konto, wenn du die Tour speicherst.
banyaro.app im Browser ergänzt diese App um die Sachen, die am
großen Bildschirm besser sind: ausführliche Tour-Karten, Forum,
Hunde-Profil-Pflege, Statistik und mehr.
Datenschutz: banyaro.app/datenschutz
Support: banyaro.app/help
```
*(ca. 2700 Zeichen — Apple-Limit ist 4000)*
---
## Keywords (max. 100 Zeichen, kommagetrennt)
```
hund,gassi,tracker,gps,tour,hundebesitzer,hundewetter,giftköder,tagebuch,community,workout
```
*(96 Zeichen — bewusst knapp unter Limit. Apple zählt Leerzeichen mit; deshalb keine.)*
**Tipps zur Auswahl:**
- „Hund" und „Gassi" sind die wichtigsten Anker
- „Tracker" + „GPS" decken die Tracking-Funktion ab
- „Giftköder" ist ein USP — selten in anderen Apps
- „Hundewetter" — Komposita-Keyword das wenig Konkurrenz hat
- App-Name darf NICHT in Keywords (Apple bezieht ihn automatisch ein)
---
## Support- und Marketing-URLs
- **Support URL** (Pflicht): `https://banyaro.app/help`
- **Marketing URL** (optional): `https://banyaro.app`
- **Datenschutz-URL** (Pflicht): `https://banyaro.app/datenschutz`
*(Wenn die Help/Datenschutz-Seiten noch nicht existieren, baue sie auf
banyaro.app — die App referenziert sie direkt aus Settings + Reviewer-
Notiz, und Apple prüft Datenschutz-URL.)*
---
## App-Datenschutz-Fragebogen (in App Store Connect, Kapitel „App-Datenschutz")
Das deckt sich exakt mit dem PrivacyInfo.xcprivacy. Antworten:
1. **„Erfasst die App Daten?"** — Ja
2. **Welche?**
- **Kontaktinformationen** → E-Mail-Adresse, Name
- **Standort** → Genauer Standort + Ungefährer Standort
- **Benutzerinhalt** → Fotos oder Videos, Sonstiger Benutzerinhalt (Tagebuch, Ausgaben-Notizen)
- **Bezeichner** → Benutzer-ID (banyaro-Account-ID)
- **Sonstige Daten** → Gesundheits- und Fitnessdaten (Workouts via HealthKit)
3. **Verwendung jeweils:** „App-Funktion"
4. **Mit Benutzer verknüpft:** Ja, bei allen (du hast einen Account)
5. **Wird für Tracking verwendet:** Nein, bei keinem
---
## Reviewer-Notiz (max. ~4000 Zeichen, freier Text)
```
Hallo Apple-Review-Team,
vielen Dank für die Prüfung von Ban Yaro Go.
Ban Yaro Go ist die native iOS-Ergänzung zur kostenlosen Community-
Plattform banyaro.app für Hundefreund:innen im deutschsprachigen Raum.
— DEMO-LOGIN —
Die App erfordert ein banyaro-Konto. Bitte nutzt zum Testen:
E-Mail: <reviewer@banyaro.app>
Passwort: <BITTE VOR EINREICHUNG SETZEN>
Der Account enthält zwei Beispielhunde, einige Touren, Tagebuch-
Einträge und Ausgaben. Sämtliche Server-Daten gehören diesem Demo-
Account und werden nach Review-Abschluss auf Wunsch gelöscht.
— HINTERGRUND-STANDORT (UIBackgroundModes location) —
Ban Yaro Go zeichnet auf Wunsch des Nutzers Gassi-Touren als
Polyline-Track auf. Während einer aktiven Tour läuft die App im
Hintergrund, damit der Track auch bei gesperrtem Display oder in der
Hosentasche weitergeführt wird. Zur Sichtbarmachung läuft parallel
eine Live-Aktivität in der Dynamic Island. Außerhalb einer aktiven
Tour wird kein Standort im Hintergrund erfasst.
Test der Funktion: Anmelden → Tab „Touren" → „Neue Tour", die
Aufnahme starten und das iPhone sperren — die Polyline wächst weiter,
die Live-Aktivität zeigt die laufende Strecke.
— HEALTHKIT (NSHealthUpdateUsageDescription) —
Ban Yaro Go schreibt ausschließlich (toShare:) Workouts vom Typ
„Walking" mit zugehöriger Route in Apple Health. Es werden keine
Health-Daten gelesen (read: []). Die NSHealthShareUsageDescription
existiert nur deshalb, weil iOS sie für jede HealthKit-Integration
abfragt — die App liest selbst nichts.
Aktivierung in der App: Mehr → Aufnahme → „Apple Health Sync".
— WEATHERKIT (Entitlement aktiv für app.banyaro.ios) —
Tab „Wetter" nutzt WeatherKit für hundefreundliche
Vorhersagen (Asphalttemperatur, UV, Zecken-Hinweis,
Niederschlag pro Stunde). Es werden keine Wetterdaten an Dritte
weitergegeben.
— FOTOS / KAMERA —
Fotos werden während einer Tour zur Punktmarkierung erzeugt
(Geolocation des Aufnahmezeitpunkts) und mit dem Tagebuch des
ausgewählten Hundes verknüpft. Es werden keine Fotos automatisch
hochgeladen — nur die, die der Nutzer aktiv mit „Sichern" bestätigt.
— GPX-IMPORT —
Andere Apps (z. B. Komoot, Files.app) können GPX-Tracks per
Teilen-Menü an Ban Yaro Go schicken. Die App registriert sich für
die UTI com.topografix.gpx als „Alternate Handler" (sie versucht
nicht, Standard-App für GPX zu werden). Der Nutzer bekommt einen
Bestätigungs-Dialog und kann den Track als Tour übernehmen oder nur
in Apple Maps öffnen.
— KONTO-LÖSCHUNG —
Mehr → „Konto löschen" entfernt das banyaro-Konto vollständig
(DELETE /api/profile/account, zweistufige Bestätigung). Damit wird
die Verpflichtung aus iOS-16.4-Guidelines erfüllt.
— PRIVATSPHÄRE —
Es findet kein Tracking im Sinne der App-Tracking-Transparency statt
(NSPrivacyTracking=false). Es werden keine IDFA, keine Drittanbieter-
Analytik und keine Werbe-SDKs eingebunden. Datenschutz-Richtlinie:
https://banyaro.app/datenschutz
Bei Fragen erreicht ihr uns unter mail@motocamp.de.
Vielen Dank und freundliche Grüße,
das banyaro-Team
```
*(Mit Demo-Login ergänzen, bevor du auf „Submit" klickst.)*
---
## Versionsinformationen
- **Versionsnummer:** 1.0
- **Build-Nummer:** 1
- **Mindest-iOS:** 17.0
- **iPad-Support:** Nein (aktuell nur iPhone, Layout darauf optimiert)
---
## Test-Account Setup (für dich, bevor du einreichst)
1. In banyaro.app registrieren als `reviewer@banyaro.app` (oder ähnlich)
2. 2 Test-Hunde anlegen (z. B. „Luna, Border Collie" + „Max, Mischling")
3. 12 Touren vor dem Hochladen aufzeichnen (oder GPX importieren)
4. 12 Tagebuch-Einträge
5. 1 Gassi-Treffen für nächste Woche planen
6. Login-Daten in obige Reviewer-Notiz eintragen
Apple kann sonst die Hauptfunktion nicht testen → Rejection.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

View file

@ -0,0 +1,203 @@
import SwiftUI
import MapKit
import CoreLocation
struct DiaryDetailView: View {
let dogId: Int
@State var entry: DiaryEntry
let onChange: () async -> Void
@Environment(\.dismiss) private var dismiss
@State private var photoIndex = 0
@State private var showDeleteConfirm = false
@State private var isDeleting = false
@State private var errorMessage: String?
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
header
if let media = entry.mediaItems, !media.isEmpty {
gallery(media)
}
if let text = entry.text, !text.isEmpty {
Text(text)
.font(.body)
.padding(.horizontal, 14)
}
if let tags = entry.tags, !tags.isEmpty {
tagsRow(tags)
}
if let lat = entry.gpsLat, let lon = entry.gpsLon {
locationCard(lat: lat, lon: lon)
}
deleteButton
if let errorMessage {
Text(errorMessage).font(.footnote).foregroundStyle(.red).padding(.horizontal, 14)
}
}
.padding(.vertical)
}
.navigationTitle(entry.titel?.isEmpty == false ? entry.titel! : "Eintrag")
.navigationBarTitleDisplayMode(.inline)
.alert("Eintrag löschen?", isPresented: $showDeleteConfirm) {
Button("Abbrechen", role: .cancel) {}
Button("Löschen", role: .destructive) { Task { await delete() } }
} message: {
Text("Dieser Tagebucheintrag und alle zugehörigen Fotos werden endgültig entfernt.")
}
}
// MARK: - Header
private var header: some View {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 8) {
if entry.isMilestoneFlag || entry.typ == "meilenstein" {
Label("Meilenstein", systemImage: "star.fill")
.font(.caption.bold())
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.orange.opacity(0.2), in: Capsule())
.foregroundStyle(.orange)
}
if let typ = entry.typ, !typ.isEmpty, typ != "meilenstein" {
Text(typ.capitalized)
.font(.caption.bold())
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.accentColor.opacity(0.15), in: Capsule())
.foregroundStyle(Color.accentColor)
}
Spacer()
if let datum = entry.datum {
Text(DiaryUtil.format(datum))
.font(.subheadline.monospacedDigit())
.foregroundStyle(.secondary)
}
}
if let loc = entry.locationName, !loc.isEmpty {
Label(loc, systemImage: "mappin.and.ellipse")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.padding(.horizontal, 14)
}
// MARK: - Photo gallery
private func gallery(_ media: [DiaryMedia]) -> some View {
VStack(spacing: 8) {
TabView(selection: $photoIndex) {
ForEach(Array(media.enumerated()), id: \.element.id) { idx, m in
AsyncImage(url: URL(string: "https://banyaro.app\(m.url)")) { phase in
switch phase {
case .success(let img): img.resizable().scaledToFit()
case .failure: Image(systemName: "photo")
.font(.largeTitle)
.foregroundStyle(.secondary)
default: ProgressView()
}
}
.frame(maxWidth: .infinity)
.tag(idx)
}
}
.tabViewStyle(.page(indexDisplayMode: media.count > 1 ? .always : .never))
.frame(height: 320)
.background(Color.black.opacity(0.04))
.clipShape(RoundedRectangle(cornerRadius: 16))
.padding(.horizontal, 14)
if media.count > 1 {
Text("\(photoIndex + 1) / \(media.count)")
.font(.caption.monospacedDigit())
.foregroundStyle(.secondary)
}
}
}
// MARK: - Tags
private func tagsRow(_ tags: [String]) -> some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 6) {
ForEach(tags, id: \.self) { tag in
Text("#" + tag)
.font(.caption.bold())
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background(Color.secondary.opacity(0.15), in: Capsule())
}
}
.padding(.horizontal, 14)
}
}
// MARK: - Location
private func locationCard(lat: Double, lon: Double) -> some View {
VStack(alignment: .leading, spacing: 8) {
Label("Ort", systemImage: "location.fill")
.font(.caption.bold())
.foregroundStyle(.secondary)
.padding(.horizontal, 14)
Map(initialPosition: .region(MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: lat, longitude: lon),
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
))) {
Annotation(entry.titel ?? "Eintrag",
coordinate: CLLocationCoordinate2D(latitude: lat, longitude: lon)) {
Image(systemName: "pawprint.circle.fill")
.font(.title2)
.foregroundStyle(.white, Color.accentColor)
.background(.white, in: Circle())
}
}
.frame(height: 180)
.clipShape(RoundedRectangle(cornerRadius: 12))
.padding(.horizontal, 14)
.onTapGesture { openInMaps(lat: lat, lon: lon) }
}
}
private func openInMaps(lat: Double, lon: Double) {
let item = MKMapItem(placemark: MKPlacemark(
coordinate: CLLocationCoordinate2D(latitude: lat, longitude: lon)))
item.name = entry.titel ?? "Tagebuch-Eintrag"
item.openInMaps(launchOptions: nil)
}
// MARK: - Delete
private var deleteButton: some View {
Button(role: .destructive) {
showDeleteConfirm = true
} label: {
HStack {
if isDeleting { ProgressView() }
Image(systemName: "trash")
Text("Eintrag löschen").bold()
}
.frame(maxWidth: .infinity, minHeight: 44)
}
.disabled(isDeleting)
.padding(.horizontal, 14)
.padding(.top, 10)
}
private func delete() async {
isDeleting = true
errorMessage = nil
defer { isDeleting = false }
do {
try await APIClient.shared.delete("/api/dogs/\(dogId)/diary/\(entry.id)")
await onChange()
dismiss()
} catch {
errorMessage = error.localizedDescription
}
}
}

View file

@ -50,9 +50,15 @@ struct TagebuchView: View {
systemImage: "book",
description: Text("Tippe oben rechts auf +, um deinen ersten Eintrag anzulegen.")
)
} else {
} else if let dog = activeDog.activeDog {
List(entries) { entry in
DiaryRow(entry: entry)
NavigationLink {
DiaryDetailView(dogId: dog.id, entry: entry) {
await load()
}
} label: {
DiaryRow(entry: entry)
}
}
.listStyle(.plain)
}