This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Native SwiftUI macOS app (10.15+ deployment target is 15.0) for live NZ Transport Agency traffic cameras, road events, and VMS signs. No tests, no package manager, no analytics, no backend — just swiftc against the macOS SDK plus an Xcode project that points at the same sources.
Two parallel build paths exist; both must keep working:
- Xcode:
NZTATraffic.xcodeproj, shared schemeNZTA Traffic. The project referencesSources/*.swift,Resources/Info.plist, andResources/NZTATraffic.icnsdirectly — adding a new.swiftfile means adding it to the Xcode project too. - Shell:
./build_app.sh— invokesswiftcdirectly (no SwiftPM), producesbuild/NZTA Traffic.app, ad-hoc signs it. Universalarm64 + x86_64by default; override withARCHS="$(uname -m)" ./build_app.shfor a single-arch build during local iteration.MACOSX_DEPLOYMENT_TARGEToverrides the min OS.
CLI release build:
xcodebuild -project NZTATraffic.xcodeproj -scheme "NZTA Traffic" -configuration Release -destination 'generic/platform=macOS' build./package_dmg.sh rebuilds via build_app.sh, stages with an Applications symlink, and emits dist/NZTA-Traffic-<version>-macOS-<arch>.dmg. The version comes from CFBundleShortVersionString in Resources/Info.plist — bump it there.
Run the built app: open "build/NZTA Traffic.app".
Five Swift files under Sources/, organized by layer not feature:
NZTATrafficApp.swift—@mainentry,WindowGroup+ secondaryWindow(id: "help"), andNZTATrafficCommandswhich replaces the standard About panel and Help menu items.TrafficAPIService.swift— thinURLSessionwrapper over three NZTA REST v4 endpoints (/cameras/all,/events/all/10,/signs/vms/all) athttps://trafficnz.info/service/traffic/rest/4. Each fetch has a…Result()variant that converts throws toResultso the store can surface per-section errors without one failure killing the others.TrafficStore.swift—@MainActorObservableObjectholding cameras / events / VMS / per-section loading state / per-section errors /lastUpdated/imageCacheToken.loadAllData()fans out the three fetches concurrently withasync letand applies them independently. TheimageCacheTokenis bumped on every refresh and appended to camera image URLs to bustURLCache.Models.swift— all decodable types plus payload wrappers (CamerasPayload→CameraResponse→[TrafficCamera], etc., matching the NZTA JSON shape). HelperscleanText(_:),formatVMSMessage(_:), and theKeyedDecodingContainerextension at the bottom (decodeLossyString,decodeLossyDouble) exist because the upstream API is loose-typed (numbers as strings, missing fields, embedded display-control tokens). New decoded fields should reuse these helpers rather than callingdecodedirectly.Views.swift— single 1300-line file containingContentView(header / filter bar / segmented tab picker / tab content), one view perTrafficTab(CamerasTabView,RoadEventsTabView,VMSTabView,TrafficMapTabView,AboutView), plus theAppHelpViewshown in the secondary window and shared chrome (ErrorBanner,LoadingView,EmptyStateView,Badge,StatCard).
ContentView owns the single TrafficStore and the three filter strings (region / highway / search), then asks the store for filtered/sorted slices per tab via filteredCameras/filteredEvents/filteredVMSSigns. Filtering and sorting live in the store, not in the views — when extending filters, add the predicate to the model's matches(region:highway:search:) method and to the relevant store accessor.
The map tab (TrafficMapTabView) consumes the same filtered slices and renders them through a TrafficMapLayer enum (cameras / events / vms) using MapKit. Coordinate parsing lives on each model as mapCoordinate — features without coordinates are silently dropped from the map and counted as unmappedCount.
Manual refresh: ⌘R or the toolbar button calls store.loadAllData(). Auto-refresh: @AppStorage("nzta.autoRefreshEnabled") and @AppStorage("nzta.refreshIntervalSeconds") (clamped 30–600) drive a Task loop in ContentView.configureAutoRefresh(). Toggling either @AppStorage value cancels and reschedules the task — don't bypass configureAutoRefresh.
- The NZTA API is the only data source. There is no proxy, no caching layer, no auth. Don't add one without reason.
- VMS message strings arrive with embedded display-control tokens; always run them through
formatVMSMessagebefore showing. - String/number fields from the API can be either type or absent — go through
decodeLossyString/decodeLossyDoubleandcleanText, not the rawdecodecalls. - Errors are per-section by design (
store.errors[.cameras]etc.) and rendered viaErrorBannerinside each tab. A failed fetch zeroes that section's array but leaves the others intact. - Camera image URLs must be suffixed with
?t=\(store.imageCacheToken)to bust the system URL cache after a refresh.