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.


Live on TestFlight SwiftUI CoreML Python FastAPI WeatherKit WidgetKit App Intents / Siri Live Activities MapKit iOS 17+
54 Michigan Beaches
5 Great Lakes Covered
40yr ML Training Data
6+ Data Sources
2 Person Team

Demo Video

App flow: selecting a beach and viewing real-time conditions including crowd prediction, weather, buoy data, and alerts.

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
View on TestFlight View on GitHub

My Contributions

  • 1Map 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.
  • 2Beach 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.
  • 3CoreML 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.
  • 4App 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.
  • 5WidgetKit 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.
  • 6Live 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.
  • 7Three-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.
  • 8BeachViewModel 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

SwiftUI CoreML WeatherKit MapKit WidgetKit ActivityKit AppIntents UserNotifications BackgroundTasks SwiftData CoreLocation Python FastAPI XGBoost async/await URLCache

System Architecture

Layer Technology Purpose
WeatherWeatherKitCurrent conditions, 10-day forecast, hourly forecast
Buoy DataNOAA NDBC via FastAPIWater temperature, wave height, wave period
Water QualityBeach monitoring API via FastAPIE. coli levels and swim safety status
Weather AlertsNWS via FastAPIActive alerts with severity, urgency, and expiry
TrafficTomTom via FastAPIRoad speed and closure data near each beach
Holiday DetectionHoliday API via FastAPIUsed as a crowd prediction feature
Crowd PredictionCoreML (XGBoost)On-device classification — low, moderate, busy
PersistenceSwiftDataFavorites and user preferences
Home ScreenWidgetKitSmall and medium widgets, 30-min refresh
Dynamic IslandActivityKitLive beach conditions while traveling
SiriApp IntentsOpen beach or speak conditions hands-free
BackgroundBGTaskSchedulerHourly 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

CoastCast beach list view
Dashboard showing beach suggestions scored by the algorithm — today's pick, weekend pick, and top pick based on user location and preferences.
CoastCast beach detail view
Beach detail screen showing live buoy data, WeatherKit conditions, crowd meter powered by the on-device CoreML model, and NWS alerts.

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.

ASYNC LET PARALLEL FETCH
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.

COREML FEATURE ENGINEERING
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.

HAND-BUILT MAP CLUSTERING
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.

APP INTENT — SIRI CONDITIONS
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.

FASTAPI — BEACH DETAILS ENDPOINT
@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
    }
2-person team · March – April 2026 · SwiftUI · CoreML · Python · iOS · Live on TestFlight

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