CoastCast
A full-platform iOS app delivering real-time beach conditions for 54 Michigan beaches — with on-device ML, Siri Shortcuts, Live Activities, and a Python FastAPI backend.
Demo Video
Problem
Michigan has 54 significant public beaches across five Great Lakes — and no single tool to answer the question every Michigan resident asks before heading out: Is it worth going to the beach today? Existing weather apps don't show water temperature, wave height, or crowd density. Beach-specific sites are outdated and fragmented. CoastCast was built to solve this with a single, intelligent platform.
Challenge-Based Learning
Challenge: Build a full-platform beach intelligence app that aggregates data from multiple live sources, predicts crowd conditions using machine learning, and surfaces information across the entire iOS ecosystem — in the app, on the home screen, in the Dynamic Island, and through Siri.
Approach: Designed a Python FastAPI backend to aggregate six data sources, trained a CoreML classifier on 40 years of historical data, and built native integrations for WidgetKit, Live Activities, App Intents, and background refresh.
Outcome: A production-ready iOS platform that tells users in seconds whether their favorite Michigan beach is worth visiting — from anywhere on their phone.
Project Snapshot
- Platform: iOS 17+ (SwiftUI) with Python FastAPI backend deployed on Render
- Scope: 54 Michigan beaches across Lakes Michigan, Huron, Superior, Erie, and Ontario
- Type: Real-time beach conditions platform with ML, widgets, Siri, and Live Activities
- Team: 2 developers (George Clinkscales + Jaiden Henley)
- My Role: SwiftUI lead — map, scoring algorithm, App Intents, WidgetKit, notifications, Live Activities
- Timeline: March – April 2026
My Contributions
- Map screen with hand-built clustering algorithm. Implemented a grid-based spatial bucketing system that groups beaches at zoom-out and switches to individual pins when zoomed in. Grid size scales dynamically with zoom level. Written entirely from scratch without a third-party library.
- Beach scoring engine. Designed a weighted scoring algorithm that evaluates temperature, wind speed, precipitation chance, UV index, proximity to user location, crowd tolerance, and favorites status to rank beach suggestions for today and the upcoming weekend.
- CoreML crowd prediction integration. Worked with my partner to train an XGBoost classifier on 40 years of state and national park beach attendance data, then converted it to CoreML and built the Swift-side feature engineering pipeline including cyclical month encoding, fallback water temperature lookup, and derived features. Runs fully on-device with zero network overhead.
- App Intents and Siri Shortcuts. Built two custom App Intents — one that navigates directly to a beach in the app, and one that fetches live conditions and speaks them back through Siri without opening the app. Exposed the Beach type as an AppEntity with name-based search.
- WidgetKit home screen widget. Implemented small and medium widget sizes using AppIntentTimelineProvider so users can configure which beach to track. The widget fetches live buoy water temperature from the backend and refreshes every 30 minutes.
- Live Activities and Dynamic Island. Built a beach tracker using ActivityKit that shows crowd level, water temp, and UV index in the Dynamic Island compact and expanded states as well as the Lock Screen.
- Three-tier notification system. Designed and implemented daily best beach alerts (scored at delivery time), score threshold alerts (fires when a favorite exceeds a score of 70), and severe weather alerts for NWS Severe and Extreme events. All three refresh hourly via BGTaskScheduler.
- BeachViewModel parallel data fetching. Used async let to fire the FastAPI backend call and WeatherKit simultaneously, cutting load time roughly in half compared to sequential fetching.
Tech Stack
System Architecture
| Layer | Technology | Purpose |
|---|---|---|
| Weather | WeatherKit | Current conditions, 10-day forecast, hourly forecast |
| Buoy Data | NOAA NDBC via FastAPI | Water temperature, wave height, wave period |
| Water Quality | Beach monitoring API via FastAPI | E. coli levels and swim safety status |
| Weather Alerts | NWS via FastAPI | Active alerts with severity, urgency, and expiry |
| Traffic | TomTom via FastAPI | Road speed and closure data near each beach |
| Holiday Detection | Holiday API via FastAPI | Used as a crowd prediction feature |
| Crowd Prediction | CoreML (XGBoost) | On-device classification — low, moderate, busy |
| Persistence | SwiftData | Favorites and user preferences |
| Home Screen | WidgetKit | Small and medium widgets, 30-min refresh |
| Dynamic Island | ActivityKit | Live beach conditions while traveling |
| Siri | App Intents | Open beach or speak conditions hands-free |
| Background | BGTaskScheduler | Hourly notification refresh |
Key Features
On-Device ML
Crowd prediction model trained on 40 years of state park data. Runs with zero latency and no internet required. Features include cyclical month encoding, holiday detection, and water temperature.
Smart Map
Custom grid-based clustering algorithm shows beach counts when zoomed out and individual pins when zoomed in. Scrollable card strip updates in real time as the user pans across Michigan.
Siri Integration
Two App Intents — one opens a beach in the app, one fetches live conditions and speaks them back through Siri without opening the app at all.
Home Screen Widget
Configurable WidgetKit widget in small and medium sizes. Users pin their favorite beach and see water temp, crowd level, and UV index without unlocking their phone.
Live Activities
Dynamic Island and Lock Screen tracker that shows real-time beach conditions while the user is driving. Compact, expanded, and minimal Dynamic Island regions all supported.
Smart Notifications
Three notification types: daily best beach (scored at delivery time), score threshold alerts, and severe NWS weather alerts. All refresh hourly in the background.
Beach Scoring Engine
Weighted algorithm scores beaches by temperature, wind, precipitation, UV, proximity, crowd tolerance, and favorites status. Produces today and weekend recommendations.
Water Quality Alerts
Real-time E. coli monitoring data displayed as swim safety warnings directly on the beach detail screen. NWS severe weather alerts shown with severity and expiry.
Key Screens
Code Highlights
Parallel Data Fetching — BeachViewModel
The beach detail screen fires both the FastAPI backend and WeatherKit simultaneously using async let, cutting load time roughly in half.
func loadBeach(id: Int) async {
// Fire both requests simultaneously
async let backendResult: BeachDetailResponse? = {
try? await apiService.fetchBeachDetails(beachID: id)
}()
async let weatherDone: Void = weatherKitService.fetchWeather(
latitude: coords.latitude,
longitude: coords.longitude
)
// Await both — neither blocks the other
let response = await backendResult
await weatherDone
if let response { loadCrowdPredictions(response: response) }
}
CoreML Feature Engineering — CrowdPredictor
The crowd prediction pipeline engineers 12 features from raw inputs before running inference. Month is encoded cyclically so the model understands that December and January are seasonally close.
func predict(for date: Date, tempMax: Double, tempMin: Double,
precipitation: Double, windMax: Double,
waterTemp: Double?, isHoliday: Bool) -> CrowdLevel {
let month = Calendar.current.component(.month, from: date)
// Cyclical encoding so model understands seasonal proximity
let monthSin = sin(2 * .pi * Double(month) / 12.0)
let monthCos = cos(2 * .pi * Double(month) / 12.0)
// Fallback water temp from historical Lake Michigan averages
let monthlyWaterTemp: [Int: Double] = [4:41,5:48,6:57,7:67,8:70,9:62,10:52,11:43]
let resolvedWaterTemp = waterTemp ?? monthlyWaterTemp[month] ?? 55.0
let featureDict: [String: Any] = [
"month_sin": monthSin, "month_cos": monthCos,
"temp_max": tempMax, "temp_range": tempMax - tempMin,
"precipitation": precipitation, "wind_max": windMax,
"water_temp_f": resolvedWaterTemp,
"is_peak_summer": (month == 7 || month == 8) ? 1.0 : 0.0,
"is_holiday": isHoliday ? 1.0 : 0.0
]
// ... run inference
}
Map Clustering Algorithm — MapViewModel
A grid-based bucketing system groups beaches into clusters when zoomed out. Grid size scales with zoom level so clusters remain appropriately sized at any altitude.
func makeClusters() -> [BeachCluster] {
guard let region = lastRegion else { return [] }
var buckets: [String: [Beach]] = [:]
// Grid size scales with zoom — tighter grid when more zoomed in
let gridSize = max(region.span.latitudeDelta / 6.0, 0.2)
for beach in filteredBeaches {
let latKey = Int(beach.coordinates.latitude / gridSize)
let lonKey = Int(beach.coordinates.longitude / gridSize)
buckets["\(latKey)-\(lonKey)", default: []].append(beach)
}
return buckets.values.map { beaches in
let avgLat = beaches.map { $0.coordinates.latitude }.reduce(0, +) / Double(beaches.count)
let avgLon = beaches.map { $0.coordinates.longitude }.reduce(0, +) / Double(beaches.count)
return BeachCluster(
coordinate: CLLocationCoordinate2D(latitude: avgLat, longitude: avgLon),
beaches: beaches
)
}
}
Siri Shortcut — GetBeachConditionsIntent
Users can ask Siri for beach conditions without opening the app. The intent fetches live WeatherKit data, runs the scoring algorithm, and returns a spoken response.
struct GetBeachConditionsIntent: AppIntent {
static var title: LocalizedStringResource = "Get Beach Condition"
static var openAppWhenRun: Bool = false // Never opens the app
@Parameter(title: "Beach") var beach: BeachEntity
func perform() async throws -> some IntentResult & ProvidesDialog {
guard let matchedBeach = Beach.allBeaches.first(where: { $0.id == beach.id }),
let conditions = await WeatherKitService().fetchConditions(
latitude: matchedBeach.coordinates.latitude,
longitude: matchedBeach.coordinates.longitude
) else {
return .result(dialog: "Couldn't fetch conditions.")
}
let service = BeachScoringService(favoritesRepo: NoFavoritesRepository())
let result = service.score(matchedBeach, snapshot: conditions.current, type: .today, userLocation: nil)
return .result(dialog: "\(matchedBeach.beachName): \(result.reason). Temperature is \(conditions.current.tempF.rounded()).")
}
}
Python FastAPI Backend
The backend aggregates six data sources into a single endpoint response. Built with FastAPI and deployed on Render.
@app.get("/beaches/{beach_id}/details")
async def get_beach_details(beach_id: int):
# Fetch all sources concurrently
buoy, alerts, traffic, water_quality, holiday = await asyncio.gather(
fetch_ndbc_buoy(beach_id),
fetch_nws_alerts(beach_id),
fetch_tomtom_traffic(beach_id),
fetch_water_quality(beach_id),
check_holiday()
)
return {
"beach": BEACH_NAMES.get(beach_id),
"buoy_data": buoy,
"alerts": alerts,
"traffic": traffic,
"water_quality": water_quality,
"holiday": holiday
}
Outcome
CoastCast evolved from a simple weather app into a full iOS platform. It surfaces beach intelligence across every surface of the iPhone — in the app, on the home screen, in the Dynamic Island, and through Siri — without requiring the user to open anything. The on-device CoreML model means crowd predictions work offline with zero latency. The notification system ensures users never miss a perfect beach day or a dangerous swim advisory.
What I Learned
- How to train, convert, and integrate a machine learning model end-to-end from Python to Swift
- The App Intents framework and how to expose app functionality to Siri and Shortcuts
- WidgetKit timeline management and AppIntentTimelineProvider configuration
- Live Activities and Dynamic Island layout regions with ActivityKit
- BGTaskScheduler for background refresh and how to test it in development
- The importance of not hardcoding API keys in source files
Next Iteration
- Expand to App Store submission
- Add wave height and water clarity data to scoring algorithm
- Build watchOS complication for wrist-glance beach conditions
- Improve crowd model with real-time attendance data as it becomes available
- Add beach photo uploads so users can share current conditions