CoastCast

Live weather from dunes to shoreline across Michigan.

Overview

Live weather from dunes to shoreline across Michigan.

An iOS app I built for checking live weather at Michigan's beaches. You can browse beaches by region, save favorites, and see real-time conditions like temperature, wind, humidity, and visibility. with pull-to-refresh, unit toggling, and weather alerts. The frontend is SwiftUI with MVVM and async/await, no third-party dependencies, and the backend is a Python REST API I built and deployed on Render that pulls data from National Weather Service stations.

Highlights

  • Built a FastAPI backend that delivers Michigan beach, weather, and water-condition data through clean REST endpoints.
  • Integrated real-time NOAA and National Weather Service data with async API calls and reliable error handling.
  • Clean separation across view model, service layer, and persistence manager, each with a single responsibility.
  • Pull-to-refresh, unit toggling, search filtering, favoriting with persistence, error states with retry, and material-backed card layouts.

Tech Stack

  • SwiftUI
  • Python
  • FastAPI
  • Pydantic
  • HTTPX
  • Render

Team: 2

Role: Full-Stack Developer

Timeline: Mar 2026 – Mar 2026

Challenges & Solutions

01

Combining Three Data Sources Into One Fast Request

Challenge: Each beach needs weather conditions, buoy readings, and active alerts, all pulled from different federal sources with different formats and reliability. Fetching them one at a time would make every request slow, and if one source went down it would block everything else.

Solution: Designed the backend to request all three sources at the same time rather than waiting for each one to finish before starting the next. Each result is handled independently, so if one source is unavailable, the others still come through.

Result: Beach detail pages load in the time of the slowest single source instead of the combined total. A buoy going offline no longer affects weather data.

02

Delivering a Complete Beach Summary in One Request

Challenge: The app needed to show a full beach detail screen metadata, live weather, buoy conditions, and active alerts, all at once. That information lives in different places, and any piece of it could be missing at any given moment.

Solution: Built a single backend endpoint that looks up the beach first, then reaches out to each data source using that beach's specific identifiers. Everything gets packaged into one clean response. If a source fails, that section is left blank rather than crashing the whole response.

Result: The app makes one request and gets everything it needs to display the full detail screen. No extra calls, no assembly required on the client side.

03

Translating Raw Government Data Into Readable Conditions

Challenge: The National Weather Service sends all measurements in metric units, temperature in Celsius, wind speed in kilometers per hour, visibility in meters, pressure in Pascals. Showing those raw values directly would be confusing for most users checking beach conditions.

Solution: Built the app to store the original values and convert each measurement into the right unit for display. Users get imperial units by default, with a single tap to switch to Celsius. Any measurement that comes back missing shows a clean dash instead of an error.

Result: Users see familiar, readable conditions at a glance. The unit toggle works across every measurement, and missing data never breaks the screen.

Screenshots

Michigan Beaches browse screen showing beaches grouped by region

Browse by Region

Beaches organized by region with a search bar to filter by name or area.

Beach detail screen for Tawas Point State Park showing live weather conditions in Fahrenheit

Live Beach Conditions

Live conditions from the nearest weather station — temperature, wind, humidity, visibility, and more. Toggle between imperial and metric with one tap.

Beach detail screen for Sleeping Bear Dunes showing live weather conditions in Celsius

Metric Unit Toggle

Same view with Celsius active. The unit preference applies across every reading instantly.

What I Built

Backend — Python + FastAPI
  • REST API built with FastAPI and hosted on Render
  • Beach detail endpoint that stitches together weather, buoy, and alert data into one response
  • Parallel fetching with per-source error isolation so one failing source never breaks the rest
  • NDBC buoy parser for raw text format with missing value handling
  • Query parameter filtering for beaches by county, lake, and status
Frontend — SwiftUI
  • Beach list with search, regional grouping, and favorites backed by persistent storage
  • Beach detail view with temperature card, conditions grid, and alert banner
  • Unit conversion layer that reads API unit codes and supports a Fahrenheit/Celsius toggle
  • Pull-to-refresh and multi-state loading with stale data retention during background refreshes

Code Snippets

A few of the more interesting pieces from the backend and iOS client.

Multiple Data Fetches

Python

Fetches weather, buoy, and alert data simultaneously for a single beach. Failed calls resolve to None so the rest of the response still returns clean.

@app.get("/beaches/{beach_id}/details")
async def get_beach(beach_id: int):
    beach = next((b for b in sample_beaches if b["id"] == beach_id), None)
    if not beach:
        raise HTTPException(status_code=404, detail="Beach not found")

    weather_result, buoy_result, alerts_result = await asyncio.gather(
        fetch_weather_conditions(beach["nws_station_id"]),
        fetch_ndbc_conditions(beach["buoy_station"]),
        fetch_nws_alerts(beach["latitude"], beach["longitude"]),
        return_exceptions=True,
    )

    weather = weather_result if not isinstance(weather_result, Exception) else None
    buoy = buoy_result if not isinstance(buoy_result, Exception) else None
    alerts = alerts_result if not isinstance(alerts_result, Exception) else None

    return {
        "beach": beach["name"],
        "weather": weather,
        "buoy": buoy,
        "alerts": alerts,
					}

NDBC Text Parser

Python

Parses the NDBC buoy API's plain text response into structured data. Handles missing readings marked as MM and validates row length before mapping.

async def fetch_ndbc_conditions(station_id: str) -> WaterConditions:
    url = f"https://www.ndbc.noaa.gov/data/realtime2/{station_id}.txt"
    headers = {"User-Agent": "MichiganWaterAPI/1.0 (learning project)"}

    async with httpx.AsyncClient(timeout=10.0, headers=headers) as client:
        response = await client.get(url)
        response.raise_for_status()

    lines = response.text.strip().splitlines()
    if len(lines) < 3:
        raise ValueError(f"No usable NDBC data found for station {station_id}")

    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(
        station_id=station_id,
        water_temp_c=_parse_float(row.get("WTMP", "MM")),
        wave_height_m=_parse_float(row.get("WVHT", "MM")),
    )

Unit Conversion From API Values

Swift

Reads raw metric values from the NWS response and converts each measurement for display. Falls back to "--" when data is missing.

// Wind speed: API returns km/h
if let speedKMH = observation.windSpeed.value {
    let mph = speedKMH * 0.621371
    windMPH = String(format: "%.1f mph", mph)
} else {
    windMPH = "--"
}

// Visibility: API returns meters
if let meters = observation.visibility.value {
    let miles = meters / 1609.34
    visibility = String(format: "%.1f mi", miles)
} else {
    visibility = "--"
}

// Barometric pressure: API returns Pascals
if let pa = observation.barometricPressure?.value {
    let hpa = pa / 100.0
    pressure = String(format: "%.1f hPa", hpa)
} else {
    pressure = nil
}

Fahrenheit / Celsius Toggle

Swift

Stores raw Celsius from the API and recalculates the display string on toggle. The UI reads temperatureDisplay directly so the switch is instant with no refetch.

@Published var useCelsius: Bool = false
private var rawTempCelsius: Double?

func toggleUnit() {
    useCelsius.toggle()
    updateTemperatureDisplay()
}

private func updateTemperatureDisplay() {
    guard let tempC = rawTempCelsius else {
        temperatureDisplay = "--"
        return
    }
    if useCelsius {
        temperatureDisplay = "\(Int(tempC))°C"
    } else {
        let tempF = (tempC * 9.0 / 5.0) + 32.0
        temperatureDisplay = "\(Int(tempF))°F"
    }
}

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