A native iOS travel companion for San Francisco — 1,406 places, five layers of the city, one pocket-sized guidebook.
ExploreSF was built in a single day at SFHacks' Gator Sprint on May 1st, 2026 — a hackathon sprint held at SF State with the theme "Build for SF." The premise was simple: build something that makes San Francisco more discoverable. I shipped a complete iOS app from scratch in one sprint, presented it, and won 2nd prize.
Receiving the prize at SFHacks · Gator Sprint · May 1, 2026 · "Build for SF"
The idea had been forming for a while. San Francisco has layers that most people never reach — a park tucked behind a corporate lobby, a mural that has watched the neighborhood change for thirty years, a street corner where The Matrix was shot, a hidden garden on the fourteenth floor that's legally required to be open to the public. All of that exists, documented in city datasets, but scattered and unusable. What if it lived in one app, beautifully?
That question became ExploreSF.
ExploreSF is a curated, map-first travel guide to San Francisco. Five categories of city data — Film Locations, Public Open Spaces (POPOS), Parks & Recreation, Public Art, and Entertainment venues — are surfaced on a live MapKit map and browseable as an editorial list. Every pin connects to a rich detail view with Apple LookAround street-level imagery and, for film locations, live metadata from TMDB: posters, cast, director, rating, and more.
When you find places you want to visit, you bookmark them. When you are ready to go, you drop your saved places into the itinerary planner and the app builds a GPS-optimized, walkable route — ordered from your current location using a nearest-neighbor algorithm, spread across however many days you choose.
No account. No cloud. No subscriptions. Every dataset is bundled on-device.
![]() 🎬 Film Locations — 299 places |
![]() 🏛 Public Open Spaces — 81 places |
![]() 🎨 Public Art — 65 places |
![]() 🌳 Parks & Recreation — 254 places |
![]() 🎭 Entertainment Venues — 707 places |
![]() All categories at once |
Each category is its own color-coded layer. Toggle them independently from the bottom bar, or combine all five for a complete picture of the city.
![]() San Francisco, curated. |
![]() Pick what you want to see. |
![]() Save places, build a day. |
![]() Choose your layers |
![]() 1,406 places across five categories |
![]() Bookmarks, grouped by category |
![]() 244-acre detail sheet with LookAround |
![]() Apple LookAround — immersive street view |
![]() Select saved places, set options, generate |
![]() GPS-optimized route with numbered stops |
- Live MapKit map with color-coded markers for all five categories
- Park polygon boundaries rendered as semi-transparent
MapPolygonoverlays — see the actual footprint of every Recreation & Parks property - Utility overlays for public bathrooms, water fountains (with bottle-filler & dog fountain flags), and food trucks — each toggled independently from a floating side bar
- Category quick-toggle bar at the bottom — show exactly the layers you want, switch in one tap
- Pin tap opens a bottom sheet with place summary, LookAround preview, and a deep-link into Apple Maps for directions
- Editorial accordion list across all five categories — 1,406 places total
- Live search across titles, addresses, and location names
- Per-category filtering:
- Film — Release year, neighborhood, actor (async TMDB person search)
- Parks — Neighborhood, park type
- POPOS — Space type, features (indoor, food service, art, restrooms)
- Art — Art type, medium
- Entertainment — Venue type, neighborhood
- Actor filter runs a TMDB person search and collects all titles from their film & TV credits, then narrows the map to only those filming locations
- Every category has a full-screen detail modal: header, LookAround hero, info table, and directions button
- Film detail goes further — TMDB-backed poster, rating, tagline, overview, director, top cast, genres, runtime, and a complete list of every filming location, each one tappable to fly to that pin on the map
- POPOS detail surfaces amenity chips (indoor/outdoor, food, art, restrooms, seating) and opening hours
- Available at every pin across all five categories
- Singleton scene cache with rate limiting (≤ 40 requests/minute, ≥ 1.5s between requests) and in-flight task deduplication — well within Apple's API limits even when panning across hundreds of pins
- Lazy-loaded thumbnails in list rows and detail sheets; falls back to a category-colored icon placeholder when street-level imagery is unavailable
- Tap the bookmark icon on any pin, list row, or detail screen to save a place
- SwiftData persistence with a compound key (
category:originalId) for uniqueness - Saved tab groups bookmarks by category with swipe-to-delete and multi-select bulk delete
- Select any combination of saved places and tap Generate Itinerary
- Nearest-neighbor routing from your GPS position — greedily picks the closest unvisited stop each step for a practical walking order
- Options: cap the total number of stops; spread stops across 1–7 days for a multi-day trip
- Day selector in both the Itinerary tab and on the map; per-day stop lists with a visual progress bar
- Each stop has a completion toggle — mark it visited as you walk
- Itinerary map mode: numbered stop annotations + dashed
MapPolylineconnector per day
The planner solves a practical variant of the Travelling Salesman Problem — not optimally (NP-hard), but well enough for city walking.
When you tap Generate, the app:
- Anchors at your GPS location via CoreLocation — or the geographic centroid of your selected places if location is denied.
- Runs nearest-neighbor: from the current position, find the closest unvisited stop (Haversine distance), move there, repeat until the stop budget is exhausted.
- Distributes across days by dividing the ordered list into equal-length chunks — Day 1 gets the first N stops in route order, Day 2 the next N, and so on.
- Writes to SwiftData — the plan persists across restarts, and each stop records a
completedAttimestamp when checked off.
The algorithm runs synchronously on the @MainActor (the dataset is small enough that greedy nearest-neighbor completes in microseconds), while the CoreLocation anchor lookup uses Swift structured concurrency.
| Language | Swift |
| UI Framework | SwiftUI — @Observable throughout, no ObservableObject |
| Maps | MapKit — Map, Marker, Annotation, MapPolyline, MapPolygon, MKLookAroundScene |
| Persistence | SwiftData — SavedPlace, ItineraryPlan, ItineraryStop |
| External API | TMDB — movie/TV search, detail, credits, actor filmography |
| Concurrency | Swift structured concurrency — Task.detached for data loading, async/await for TMDB & LookAround |
| Location | CoreLocation — GPS anchor for itinerary start point |
| State | @Observable ViewModels, @Environment injection, UserDefaults for onboarding & category persistence |
| Data | Five bundled GeoJSON datasets — no network calls for place data |
| Design System | Custom editorial palette — New York serif headlines, warm cream travel-magazine aesthetic |
| Deployment Target | iOS 26+ |
| Dependencies | None |
ExploreSF/
├── App/
│ ├── ExploreSFApp.swift ← @main, SwiftData container, environment setup
│ └── Assets.xcassets/
│
├── Navigation/
│ ├── AppRouter.swift ← @Observable: selectedTab, activeCategories, pendingPin (UserDefaults-backed)
│ └── AppRouterView.swift ← Onboarding → CategoryPicker → TabView
│
├── DesignSystem/
│ ├── AppColors.swift ← Paper/ink/card/accent tokens (#F6F1E8 cream palette)
│ └── AppFonts.swift ← New York serif headlines, eyebrow style, body scale
│
├── Models/
│ ├── AppCategory.swift ← enum: film/popos/park/art/entertainment — color, icon, displayName
│ ├── PlacePin.swift ← Shared lightweight map-pin value type
│ ├── FilterState.swift ← Per-category filter parameters
│ ├── SavedPlace.swift ← @Model: bookmarks
│ ├── ItineraryPlan.swift ← @Model: active trip plan
│ ├── ItineraryStop.swift ← @Model: individual stop with completion state
│ ├── FilmLocation / FilmEntry ← Film data + TMDB grouping
│ ├── POPOS/ ← POPOSPlace + GeoJSON decode
│ ├── Parks/ ← ParkPlace + ParkPolygon + GeoJSON decode
│ ├── Art/ ← ArtPlace + GeoJSON decode
│ └── TMDB/ ← TMDBDetail, TMDBCredits, search types
│
├── Services/
│ ├── DataStore.swift ← @Observable: async-loads all datasets at app start
│ ├── Load{Category}Data.swift ← One nonisolated loader per dataset
│ ├── TMDBService.swift ← Actor-isolated TMDB client with response caching
│ └── LookAroundSceneCache.swift ← Rate-limited singleton (≤40 req/min)
│
├── ViewModels/
│ ├── MapViewModel.swift ← Map state, filtered pins, visible polygons, actor filter
│ ├── BrowseListViewModel.swift ← List state, search, poster lazy-loading
│ ├── ItineraryManager.swift ← Nearest-neighbor planner, completion tracking
│ ├── MovieDetailViewModel.swift ← TMDB detail + credits async fetch
│ └── {Category}DetailViewModel ← LookAround per category
│
├── Views/
│ ├── Map/ ← ExploreMapView, pin sliders, filter sheet, category bar
│ ├── Browse/ ← BrowseListView + per-category row views
│ ├── Saved/ ← SavedView (bookmarks + itinerary tabs)
│ ├── Itinerary/ ← ItineraryPlanView, setup sheet, stop rows
│ ├── MovieDetail/ ← Full film detail with TMDB data
│ ├── {Category}Detail/ ← Full detail modals per category
│ ├── CategoryPicker/ ← Multi-select modal
│ ├── Onboarding/ ← 3-panel carousel with custom SwiftUI illustrations
│ └── Shared/ ← BookmarkButton, SearchBar, LookAroundPreview, PosterImage
│
└── Resources/
├── Film_Locations_in_San_Francisco.geojson
├── Privately_Owned_Public_Open_Spaces.geojson
├── Recreation_and_Parks_Properties.geojson
├── Public_Art.geojson
└── Active_Entertainment_Permits.geojson
Requirements: Xcode 17 and an iOS 26 simulator or device. One external API key required.
git clone https://github.com/keyursavalia/ExploreSF.git
cd ExploreSF
open ExploreSF.xcodeprojIn Config.swift, add your TMDB API key:
static let tmdbAPIKey = "YOUR_KEY_HERE"Press Cmd R. All five place datasets load from bundled GeoJSON — no backend, no database setup. TMDB is the only network dependency; the app degrades gracefully if the key is absent (film rows appear without posters or cast info).
- EntertainmentDetail + Art deep links — full detail modals matching the depth of Film and POPOS
- Food Trucks overlay — the dataset is bundled and parsed; surface it on the map alongside bathrooms and fountains
- Offline TMDB cache — persist poster and credit data to SwiftData so film detail works without a connection
- Home screen widget — glanceable "place of the day" drawn from your saved bookmarks
- Share sheet — export a day's itinerary as formatted text to send to whoever you're exploring with
- Accessibility pass — full VoiceOver audit across all five category detail screens and the itinerary flow
Fork the repo, branch from main, one fix or feature per PR. Commit prefixes: init: / update: / fix:. Bug reports and ideas are welcome as GitHub issues.
All place data is sourced from DataSF — the City and County of San Francisco's open data portal. Datasets are published under the Public Domain Dedication and License (PDDL).
| Dataset | Publisher | Link |
|---|---|---|
| Film Locations in San Francisco | SF Film Commission | data.sfgov.org ↗ |
| Privately Owned Public Open Spaces (POPOS) | SF Planning Department | data.sfgov.org ↗ |
| Recreation and Parks Properties | SF Recreation and Parks Department | data.sfgov.org ↗ |
| Public Art | SF Arts Commission | data.sfgov.org ↗ |
| Entertainment Commission's Places of Entertainment | SF Entertainment Commission | data.sfgov.org ↗ |
| San Francisco Public Bathrooms and Water Fountains | SF Recreation and Parks / Public Works | data.sfgov.org ↗ |
Film and TV metadata (posters, cast, ratings, overviews) is provided by the TMDB API. This product uses the TMDB API but is not endorsed or certified by TMDB.
MIT · © 2026 Keyur Savalia
















