Eine iOS-App zum Finden, Bewerten und Sammeln von Dönerläden. Offline-First, Community-getrieben, mit Gamification-Mechaniken die an Pokémon Go und Spotify Wrapped erinnern.
Die Döner-App verbindet eine persönliche Stempelkarte mit einem sozialen Layer: Nutzer checken bei Läden ein, bewerten nach Sauce/Fleisch/Brot, verfolgen ihre eigene Döner-Geschichte und sehen was Freunde gerade essen.
Stack:
- iOS: Swift 6, SwiftUI, MapKit, SwiftData
- Backend: Vapor (Swift), PostgreSQL + PostGIS, Docker
- Daten: OpenStreetMap via Overpass API
Die Karte ist der Einstiegspunkt. Sie lädt Dönerläden dynamisch per Overpass API basierend auf dem sichtbaren Kartenausschnitt — aber nur wenn der Span klein genug ist (< 0.5°), um Timeouts zu vermeiden. Ein Caching-Layer in SwiftData verhindert unnötige Netzwerkaufrufe: Regionen werden erst nach 24h neu geladen.
UI-Details:
- Kein Standard-Apple-Maps-Look: POI-Layer deaktiviert, 3D-Elevation aktiv, Fokus liegt auf Döner-Pins
- Custom Pin-Views zeigen Ladenname + lokalen Besuchszähler direkt auf der Karte
- Favoriten-Filter per Heart-Button oben rechts: zeigt nur gemerkte Läden, Pins werden pink
- Backend-Overlay ist non-blocking: OSM-Daten erscheinen sofort, Community-Ratings werden still nachgeladen
Der Check-In ist bewusst schnell gehalten. Ein Rotary Picker dreht sich durch 6 Food-Types (Döner Classic, Hähnchen, Lamm, Falafel, Salat, Sonstiges) — oberstes Symbol = ausgewählt.
UI-Details:
- Bestätigung: großer oranger Kreis mit Checkmark — klar, unübersehbar
- Confetti-Animation: Emoji-Partikel des gewählten Food-Types fallen herunter (nicht generisches Konfetti)
- Check-In speichert lokal in SwiftData zuerst, Backend-Sync läuft danach asynchron
Reviews haben ein 5-Sterne-Gesamtrating plus drei optionale Dimensions-Ratings: Sauce, Fleisch, Brot. Das Gesamtrating berechnet sich automatisch als Durchschnitt der Dimensionen, kann aber manuell überschrieben werden.
UI-Details:
- Dimensions-Ratings sind optional — wer nur eine Zahl will, kann das
- Beim Review kann eine Community-Notiz vorgeschlagen werden: kurze Freitext-Beschreibung die für alle Nutzer des Ladens sichtbar ist (z.B. "Besonders gute Knoblauchsoße")
- One-Per-User-Per-Place: zweiter Review überschreibt den ersten, Aggregate werden neu berechnet
Das Profil ist der persönliche Döner-Pass.
Stempel-Tiers (6 Stufen):
| Tier | Ab | Farbe |
|---|---|---|
| Dönerneuling | 0 | Grau |
| Dönerfreund | 5 | Braun |
| Dönerfan | 15 | Orange |
| Dönerprofi | 30 | Rot |
| Dönermeister | 60 | Lila |
| Dönerlegende | 100 | Gold |
UI-Details:
- 10×10 Stempel-Grid, pro Tier gefüllt dargestellt
- Progress-Bar zum nächsten Tier mit Text ("noch 8 bis Dönerprofi")
- Food-Konsum-Stats als Bar-Chart mit prozentualer Verteilung
- "Seit [Monat/Jahr]"-Badge basiert auf dem ersten Check-In als Join-Date
11 unlockbare Badges in einem 4×4-Grid. Locked/Unlocked visuell klar unterschieden.
| Badge | Bedingung |
|---|---|
| First Bite | Erster Check-In |
| Critic | Erste Bewertung |
| Regular | 5× gleicher Laden |
| Explorer | 10 verschiedene Läden |
| Connoisseur | 50 verschiedene Läden |
| Berlin Tour | 5 Läden in Berlin |
| Hamburg Tour | 5 Läden in Hamburg |
| Stamp Collector Silver | Tier "Dönerfan" erreicht |
| Stamp Collector Gold | Tier "Dönermeister" erreicht |
| Night Owl | Check-In nach 22:00 Uhr |
| Social Butterfly | 5+ Freunde |
Full-Screen-Experience mit 5 animierten Seiten im Spotify-Wrapped-Stil:
- Total Besuche des Jahres
- Lieblings-Food-Type + Anzahl
- Top-Läden mit Besuchszahl
- Unique Läden entdeckt
- Summary
UI-Details:
- Spring-Physics-Übergänge zwischen Seiten
- Wechselnde Gradient-Backgrounds pro Seite
- Zentrierte große Typografie, Emoji-basierte Visuals
Bidirektionale Freundschaften (Requester/Addressee-Modell). Nutzer finden sich per Display-Name-Suche.
- Eingehende Anfragen erscheinen oben in der Friends-View
- Slide-to-Delete für bestehende Freundschaften
- Friend Request verhindert Duplikate und Selbst-Anfragen
Persönliches Ranking aller Läden wo der Nutzer interagiert hat. Drei Sortieroptionen per Segmented Picker:
- Bewertung — nach Durchschnitts-Rating
- Besuche — nach Visit-Count
- Zuletzt besucht — chronologisch
Top-3 bekommen Trophy-Badges statt Zahlen.
iOS/DoenerApp/
├── App/ # Entry Point, ModelContainer, ScenePhase
├── Core/
│ ├── Network/ # APIClient, VisitSyncService, ReviewSyncService, SyncQueueService
│ ├── Persistence/ # SwiftData Models
│ └── Auth/ # AuthStore, KeychainStore
├── Features/
│ ├── Map/ # MapView, MapViewModel, OverpassService
│ ├── CheckIn/ # CheckInSheet, ConfettiView
│ ├── PlaceDetail/ # PlaceDetailView, ReviewSheet
│ ├── Discover/ # DiscoverView, DiscoverViewModel
│ ├── Feed/ # FeedView
│ ├── Profile/ # ProfileView, AchievementsView, WrappedView
│ ├── Friends/ # FriendsView, FriendSearchView
│ ├── Ranking/ # RankingView
│ └── Settings/ # SettingsView
└── Shared/ # Wiederverwendbare Views, Extensions
Prinzipien:
- MVVM mit
@ObservableViewModels +@MainActor - Offline-First: SwiftData als lokale Quelle, Backend-Sync asynchron
- Dependency Injection via SwiftUI Environment
- Sync-Queue: Gescheiterte Backend-Syncs werden als
PendingSyncOperationpersistiert und beim nächsten App-Vordergrund verarbeitet
Backend/Sources/App/
├── Controllers/
│ ├── AuthController.swift
│ ├── PlaceController.swift
│ ├── ReviewController.swift
│ ├── VisitController.swift
│ └── FriendController.swift
├── Models/
│ ├── User.swift
│ ├── DoenerPlace.swift
│ ├── Review.swift
│ ├── Visit.swift
│ └── Friendship.swift
└── Migrations/
API-Routen:
| Methode | Pfad | Beschreibung |
|---|---|---|
| POST | /auth/apple |
Apple Sign-In |
| GET | /auth/me |
Aktueller User |
| PATCH | /users/me |
Display-Name ändern |
| GET | /places |
Läden in Radius |
| GET | /places/top_nearby |
Top-bewertete in Radius |
| GET | /places/trending |
Trending (letzte N Tage) |
| POST | /places/by_osm/:id/visits |
Visit erstellen (Place wird auto-angelegt) |
| POST | /places/by_osm/:id/reviews |
Review upserten |
| GET | /places/by_osm/:id/summary |
Community-Summary |
| GET | /users/search |
Nutzer per Display-Name suchen |
| GET | /friends |
Freundschaften abrufen |
| POST | /friends/requests |
Freundschaftsanfrage senden |
| POST | /friends/requests/:id/accept |
Anfrage annehmen |
| DELETE | /friends/:id |
Freundschaft entfernen |
Besonderheiten:
- Lazy Place Creation: Visits und Reviews legen den
DoenerPlace-Eintrag bei Bedarf automatisch an - Aggregate werden nach jedem Review neu berechnet (avgRating, reviewCount)
- Auth: Apple Identity Token wird dekodiert (HMAC-SHA256 Session-Token statt vapor/jwt — Kompatibilitätsproblem mit Swift-Toolchain)
Voraussetzung: OrbStack oder Docker Desktop, Swift-Toolchain.
cd Backend
# Postgres starten (Port 5434, da 5432 ggf. belegt)
docker compose up -d db
# Backend starten
DB_PORT=5434 swift run App serve --hostname 0.0.0.0 --port 8080Das Backend ist dann unter http://<Mac-LAN-IP>:8080 erreichbar.
- Xcode öffnen:
open iOS/DoenerApp.xcodeproj - Backend-URL anpassen falls nötig:
iOS/DoenerApp/Core/Network/APIConfig.swift- Oder direkt in der App unter Settings → Backend-URL
- Signing-Team setzen (Free Provisioning reicht für Entwicklung)
- Auf echtem Gerät deployen (Simulator hat kein Sign-in with Apple)
Netzwerk-Hinweis: iPhone und Mac müssen im gleichen WiFi sein. Die App erlaubt LAN-HTTP via NSAllowsLocalNetworking in der Info.plist.
Falls kein Sign-in with Apple verfügbar:
# Backend starten mit Dev-Login aktiviert
ALLOW_DEV_LOGIN=true DB_PORT=5434 swift run App serve --hostname 0.0.0.0 --port 8080In der App unter Settings → Dev-Login erscheint dann ein zusätzlicher Login-Button.
# 5 Freundschaften für letzten User anlegen (entsperrt Social Butterfly Achievement)
./scripts/seed_friendships.sh- Glassmorphism:
.ultraThinMaterialdurchgehend für Sheets und Overlays - Orange als Akzentfarbe: konsistent für alle interaktiven Elemente
- Spring-Physics: Animationen basieren auf Spring-Kurven, nicht linearen Timings
- Offline-First: Jede Aktion wird lokal gespeichert bevor sie das Netzwerk berührt
- Emoji als UI: Statt Icon-Sets werden Emojis für Food-Types, Achievements und Confetti verwendet — spart Assets, wirkt lebendiger
Siehe BACKLOG.md für den detaillierten Aufgabenstand.
Nächste Schritte (Sprint 3):
- Freunde-Feed: gemeinsamer Activity Stream
- Push Notifications: Friend Requests, Freund bei bekanntem Laden
- Leaderboards unter Freunden
- TestFlight-Distribution (sobald Apple Developer Account aktiv)