A native iOS app (SwiftUI + MapKit + Swift Charts, iOS 17+) for terrain line-of-sight. It is a client of the SkyQuant Weather server’s LOS API — the same engine behind the web app’s 📐 Line of Sight panel — and does no terrain math of its own beyond preparing the profile chart.
MKMapView
between a flat standard map, flat satellite imagery with road and place
labels (to place a point on an exact building; switching between Map and
Satellite keeps the camera where it is) and a hybrid map with realistic
elevation, which iOS 17 draws as a 3-D globe when zoomed out (switching to
Globe flies the camera out to 20,000 km over the current centre; back to
Map or Satellite returns to a regional view). The choice is remembered.POST /api/los/viewshed. A cold server can take
several minutes (terrain tiles download first); the app waits up to 11
minutes and shows elapsed time with a Cancel button. The visible area is
drawn in green with a dashed range ring; stats show ground elevation,
observer MSL and the range swept. The viewshed colour is user-selectable
in Settings → Viewshed color (picker, preview, Reset to default); it
applies to the map at once, with no Save.POST /api/los for that target. The
path is drawn along the great circle (blue = clear, red dashed = blocked)
with a verdict and a terrain-profile chart; drag across the chart to read
distance, terrain, LOS altitude and clearance. Tap again to move the
target. Edit opens the parameter form over the drawn viewshed:
Recompute viewshed reruns it for the same origin and replaces the
result, Back to results returns without touching it (a tap on the map
while editing moves the origin, which drops the viewshed). Clear drops
the viewshed (keeps the origin); New origin starts over.The app always runs in dark mode (UIUserInterfaceStyle = Dark in the
generated Info.plist, plus .preferredColorScheme(.dark) at the root), so the
map, sheets and chart ignore the system appearance.
The icon is the “LOS” wordmark with the O as a viewshed ring (lit wedge toward
the target) and a sight line from the cyan origin to the amber target. Source:
design/AppIcon.svg (1024×1024, Inter ExtraBold); the shipped PNG is
SkyQuantLOS/Assets.xcassets/AppIcon.appiconset/AppIcon.png. Re-render the
PNG from the SVG at 1024 px after any edit (an iOS icon must be opaque).
The app connects to the server address built into it (below) — nothing to
type. Settings (gear) can override it with another server (written by Save;
empty = the built-in server, and Use built-in server drops a stored
override). Either way the server must be a SkyQuant Weather build whose /api/los and /api/los/viewshed accept obs_msl_m (absolute
observer altitude) — needed for an MSL origin altitude. AGL origins work on
any server with the LOS routes.
Use https: App Transport Security blocks plain http to a public host.
| Request | Fields |
|---|---|
POST /api/los/viewshed |
obs_lat, obs_lon, ONE of obs_agl_m / obs_msl_m, ONE of tgt_agl_m / tgt_msl_m, max_range_km, az_step: 1, range_step_m: 250 |
POST /api/los |
the same obs_* / tgt_* choice, tgt_lat, tgt_lon, obs_terrain_m (from the viewshed, so both share one observer ground), n_samples: 500 |
Errors come back as { "error": "…" } and are shown in a red banner, as are
HTTP failures, timeouts and malformed JSON.
cp Config/Local.xcconfig.example Config/Local.xcconfig # then edit the URL
open SkyQuantLOS.xcodeproj
Config/Local.xcconfig is where your server’s address goes; it is gitignored
and is the only place in the checkout that may contain it. The committed
Config/Server.xcconfig (the app target’s base configuration) sets
SKYQUANT_BASE_URL empty and #include?s the local file; Config/Info.plist
bakes the value into the built app’s Info.plist as SkyQuantBaseURL, so the
app connects on launch with nothing to type.
Mind the xcconfig comment trap: // starts a comment anywhere on a line,
so write the URL as https:/$()/your-server.example ($() expands to
nothing), or give a bare host (your-server.example — the app adds
https://). Config/Local.xcconfig.example explains it.
Built without Local.xcconfig, the app has no built-in address and opens
Settings on launch to ask for one. In either build, a URL typed in Settings is
an override and wins while it is stored; a broken override is reported,
never silently replaced by the built-in. The server hostname is never
committed to this repository — only the gitignored Config/Local.xcconfig
and, for an override, the device’s UserDefaults hold one.
Config/Local.xcconfig must carry the production address before archiving —
a Release build made without it ships an app with no address that asks the
user to type one.
Xcode 16 or newer (the project uses file-system-synchronized folders, project
format 77). To run on a device put your Apple Developer Team ID in the same
Config/Local.xcconfig (DEVELOPMENT_TEAM = ABCDE12345 — the project file
says $(inherited) for every target, so do NOT pick a team under Signing &
Capabilities: Xcode would write it into project.pbxproj and block the next
git pull), then Run. ⌘U runs the unit tests (SkyQuantLOSTests: request
bodies, response decoding, viewshed geometry, profile math, viewshed colour,
server resolution).
The project file was written by hand. If it ever misbehaves, regenerate it from the equivalent XcodeGen spec:
brew install xcodegen
xcodegen generate
SkyQuantLOS/
SkyQuantLOSApp.swift
Models/ AltitudeReference, Units, LOSRequests, LOSResponses, ProfileMath,
ViewshedStyle
Geo/ ViewshedGeometry (GeoJSON → MKPolygon, ring, geodesic path)
Networking/ APIClient (+ LOSError, AppSettings)
State/ LOSSession (the app's state machine)
Views/ ContentView, LOSMapView, SetupSheet, ResultPanel,
ProfileChartView, SettingsView
SkyQuantLOSTests/
Config/ Server.xcconfig (committed, empty default), Info.plist
(SkyQuantBaseURL), Local.xcconfig.example; Local.xcconfig is
gitignored and holds the real address
design/ AppIcon.svg