CoastCast

Live weather from dunes to shoreline across Michigan.

3 Live Data Sources
4 Great Lakes Covered
1 API Call Per Beach
50+ Years of NWS & Attendance Data

Overview

Live weather from dunes to shoreline across Michigan.

An iOS app I built because I was tired of jumping between different websites just to check beach conditions in Michigan. You can browse beaches by region, save your favorites, and see live conditions like temperature, wind, and humidity. I built the whole thing - the SwiftUI app on the front end and a Python API on the back end that pulls live data from National Weather Service stations.

Highlights

  • Built a Python API with FastAPI that serves beach and weather data to the app.
  • Connected to NOAA and NWS for real data, and made sure the app keeps working when one of those sources goes down.
  • Kept the Swift code organized with separate files for fetching data, storing it, and showing it.
  • Added pull-to-refresh, search, favorites that save between sessions, and proper error messages when something fails.

Tech Stack

  • SwiftUI
  • Python
  • FastAPI
  • Pydantic
  • HTTPX
  • Render
  • WeatherKit
  • CoreML
  • CoreLocation
  • MapKit

Team: 2

Role: Full-Stack Developer

Timeline: Mar 2026 – May 2026

Challenges & Solutions

01

Getting Data from Three Sources Without Slowing Everything Down

Challenge: Each beach needs weather, buoy readings, and alerts, and they all come from different government sources. If I fetched them one at a time, the page would be slow, and if one source was down it would hold up everything else.

Solution: Made the backend kick off all three requests at the same time instead of waiting for each one to finish. Each result is handled on its own, so if one source fails the others still come back fine.

Result: The beach detail page loads as fast as the slowest single source, not all three added together. A buoy going offline doesn't affect the weather data at all.

02

Showing Everything on One Screen Without Extra Calls

Challenge: The detail screen needed to show the beach info, live weather, buoy conditions, and any alerts all at the same time. That data lives in different places and any of it could be missing.

Solution: Built one endpoint that grabs the beach first, then uses that info to go fetch everything else. It packages it all into one response. If one source fails, that part just comes back empty instead of breaking everything.

Result: The app only makes one request to load the full detail screen. No extra calls needed.

03

Converting Government Data Into Units People Actually Use

Challenge: NWS sends everything in metric - Celsius, km/h, meters, Pascals. Showing those numbers directly would just confuse people trying to check beach conditions.

Solution: The app stores the original values and converts each one before showing it. You get imperial by default with one tap to switch to Celsius. If a reading is missing it just shows a dash instead of an error.

Result: Everything looks normal at a glance. The unit toggle works on every measurement and missing data never breaks the screen.

Screenshots

CoastCast home screen showing hero banner, activity categories, and nearby beach recommendations

Home Screen

Shows nearby beaches and recommendations right when you open the app, with shortcuts to filter by activity type.

CoastCast search screen showing a filterable list of Michigan beaches with images and tags

Search & Filter

Search any beach by name or apply filters to narrow by lake, park type, or activity. Each result shows a photo, short description, and quick-glance tags.

CoastCast map view with teal beach pins along Michigan's west coast and a scrollable card tray below

Interactive Map

Every beach plotted on a live map with custom pins. Tap a marker or swipe the card tray at the bottom to preview drive time and water body at a glance.

McLain State Park detail screen showing air temperature, UV index, water temp, and hourly forecast

Full Beach Conditions

Air and water temperature, UV index with protection guidance, and a scrollable hourly forecast - all pulled live from the nearest NWS station.

CoastCast 7-day forecast screen with Thursday highlighted as the best day to visit

Best Day to Visit

Shows a 7-day forecast and highlights the best day to visit in gold so you don't have to guess.

Fort Wilkins State Park detail showing crowd meter with a now-moderate indicator and weekly bar chart

Crowd Meter

Real-time crowd level with a Light-to-Heavy indicator and a weekly bar chart so you can pick a quieter window before making the drive.

CoastCast favorites screen listing saved beaches with photos, descriptions, and lake tags

Favorites

Save any beach with one tap. The Favorites tab keeps them saved with photos and tags, ready whenever you need them.

What I Built

Backend - Python + FastAPI
  • REST API built with FastAPI and hosted on Render
  • Beach detail endpoint that combines weather, buoy, and alert data into one response
  • Fetches all data at the same time - if one source fails the rest still come through
  • Parser for NOAA's buoy data, which comes back as plain text with its own format for missing values
  • Filter beaches by county, lake, and status using query params
Frontend - SwiftUI
  • Beach list with search, grouped by region, with favorites that stay saved between sessions
  • Beach detail screen with temperature, conditions, and an alert banner
  • Converts all the metric values from the API to imperial, with a toggle to switch to Celsius
  • Pull-to-refresh that keeps showing old data while new stuff loads in the background

Code Snippets

A few pieces from the backend and iOS app that I thought were worth showing.

Running Blocking Code Inside an Async API

Python

The pandas data processing is blocking code, but the rest of the API is async. asyncio.to_thread() lets me run it without rewriting it. return_exceptions=True means if one source fails, the rest of the response still comes back clean.

alerts_result, water_quality = await asyncio.gather(
    get_beach_alerts_safe(beach["lake"]),
    asyncio.to_thread(get_water_quality_safe, beach_id),
    return_exceptions=True,
)

alerts = alerts_result if not isinstance(alerts_result, Exception) else []
wq = water_quality if not isinstance(water_quality, Exception) else None

Parsing NOAA's Plain Text Buoy Data

Python

NOAA sends buoy readings as plain text and uses "MM" to mean a value is missing. Using row.get("WTMP", "MM") means even if a column is completely missing from the response, it still falls back to "MM" and gets handled the same way as a real missing reading.

columns = lines[0].split()
first_row = lines[2].split()
if len(first_row) < len(columns):
    raise ValueError(f"Malformed NDBC row for station {station_id}")
row = dict(zip(columns, first_row))

return WaterConditions(
    water_temp_c=_parse_float(row.get("WTMP", "MM")),
    wave_height_m=_parse_float(row.get("WVHT", "MM"))
)

Predicting Crowd Levels from Three Sources

Swift

Takes the WeatherKit forecast, buoy water temp, and a CoreML model I trained and runs each day of the week through the crowd predictor. The whole thing runs on-device so there's no extra API call.

func loadCrowdPredictions(response: BeachDetailResponse) {
    let waterTemp = buoyData?.waterTempC.map { $0 * 9/5 + 32 }

    guard let today = weatherKitService.dailyForecast.first else { return }
    todayCrowd = crowdPredictor.predict(
        for: .now,
        tempMax: Double(today.highF), tempMin: Double(today.lowF),
        precipitation: today.chanceOfPrecipitation,
        windMax: today.windSpeed.converted(to: .milesPerHour).value,
        waterTemp: waterTemp, isHoliday: response.holiday
    )

    forecastCrowd = weatherKitService.dailyForecast.map { day in
        crowdPredictor.predict(
            for: day.date,
            tempMax: Double(day.highF), tempMin: Double(day.lowF),
            precipitation: day.chanceOfPrecipitation,
            windMax: day.windSpeed.converted(to: .milesPerHour).value,
            waterTemp: waterTemp, isHoliday: response.holiday
        )
    }
}

Refreshing All Favorites at Once

Swift

When the app refreshes, it fetches weather and alerts for all your saved beaches at the same time instead of one by one. Once everything comes back it checks if any beaches are worth a notification - a great beach day, conditions hitting a threshold, or a severe weather alert.

func refresh(
    favorites: [Beach], scoringService: BeachScoringService,
    weatherService: WeatherKitService, apiService: MichiganWaterAPIService,
    userLocation: CLLocation?, at time: Date
) async {
    guard !favorites.isEmpty else { cancelAll(); return }

    var conditions: [Int: BeachConditions] = [:]
    var alertsByBeach: [Int: [AlertFeature]] = [:]

    await withTaskGroup(of: (Int, BeachConditions?, [AlertFeature]).self) { group in
        for beach in favorites {
            group.addTask {
                async let weather = weatherService.fetchConditions(
                    latitude: beach.coordinates.latitude,
                    longitude: beach.coordinates.longitude)
                async let details = try? apiService.fetchBeachDetails(beachID: beach.id)
                let (w, d) = await (weather, details)
                return (beach.id, w, d?.alerts ?? [])
            }
        }
        for await (id, condition, alerts) in group {
            if let condition { conditions[id] = condition }
            alertsByBeach[id] = alerts
        }
    }

    cancelAll()
    scheduleTopFavoriteAlert(...)
    scheduleThresholdAlert(...)
    scheduleSevereAlertIfNeeded(alertsByBeach: alertsByBeach, favorites: favorites)
}

Next Iterations

Next I want to expand the beach list to pull live from the API instead of hardcoding five entries. I'd also add buoy data to the detail screen once the ice melts and the NDBC starts reporting again. Things like wave height, water temp, and wave period so users get the full picture before heading out. On the backend side I want to add caching so the API isn't hitting NWS and NDBC on every single request, and I'd look into adding hourly forecasts so users can plan around conditions changing later in the day.

Outcome

CoastCast started because I wanted real beach conditions for Michigan in one place instead of jumping between three different government sites. Building the Python API was the first time I owned the full backend, figuring out how to pull federal data sources, handle failures gracefully, and show a clean response that can be used in Swift. On the frontend side it reinforced how I think about state management and keeping the UI responsive when the data behind it is unpredictable. The app works end to end, one tap, one screen, everything you need to know before you go.

More Case Studies