UI/UX Spec — iPhone Companion
Source And Scope
This spec is the canonical native implementation guide for the iPhone companion surface of
Meridian. It is a scaled, scoped derivative of the iPad spec in
ui_ux_spec.md. Where this file is silent, the iPad spec’s behavior,
visual system, and Aero components apply, adapted to compact width per
design_principles_iphone.md.
Build the UI with:
- SwiftUI for chrome, sheets, lists, the inspector, and the attribute form.
- MapLibre Native iOS for the live map canvas (now with pitch).
- Native materials plus the shared grain overlay for Aero glass.
- Native system sheet detents (medium / large) for panels.
- SF Symbols where they match; the bundled icon set where parity is required.
Do not copy prototype HTML/CSS/JS into production. Recreate behavior and the visual system
natively. The iPhone is a universal-iOS build of the existing target gated by
PlatformCapabilities.isPhone, not a fork.
iPhone Scope (Authoritative)
In scope for v1 iPhone:
- View iPad/Mac-authored projects from iCloud Drive (and Open from Files as a fallback).
- Pan, zoom, rotate (bearing), and tilt (pitch) the map. “Orbit” = bearing + pitch around
the map center, not 3D terrain.
- Toggle layer visibility only.
- Switch the basemap among online presets (Standard + Satellite). Satellite presets
accept a provider key entered inline and stored in the local Keychain (never written to
the iCloud package). The custom-URL form and PMTiles import stay on iPad/Mac.
- Save, fly to, rename, and delete map bookmarks (saved views). Fly-to restores the full
camera including pitch.
- Read-only tap-to-measure: tap points for live geodesic distance, close 3+ points to a
polygon for area. No geometry is created and it never marks the document dirty.
- Tap authoritative features and edit attributes via the existing
feature-form /
UpdateProperties path.
- Add text point notes and callout notes via existing non-authoritative markup layers.
- Save back to iCloud Drive with conflict detection — no last-writer-wins, no auto-merge.
Explicitly out of scope on iPhone (do not surface, not even disabled): geometry
drawing/editing, snapping, the tool dock and draw bar, layer reorder/opacity/style/labels/
filters/lock/rename/delete/metadata, GeoJSON import, Open Data Library, spatial analysis,
analysis history, field calculator, export, PDF map layout, blank project creation,
duplication, and multi-selection workflows.
Global App Structure
Primary surfaces (only these exist on iPhone):
- Phone Project Library (iCloud document list + Open from Files).
- Phone Map Workspace (full-bleed map + compact chrome).
- Feature Inspector + Attribute Form (bottom sheets over the workspace).
- Layers Sheet (visibility toggles).
- Basemap Sheet (online preset switcher + inline provider key).
- Bookmarks Sheet (save / fly-to / rename / delete saved views).
- Measure bar (read-only tap-to-measure overlay; not a sheet).
- Conflict Resolution Sheet.
- Notes action (markup creation in place; no dedicated screen).
- Design System tokens and reusable components (shared with iPad/Mac).
The Basemap, Bookmarks, and Measure entries live behind a single Map menu in the bottom
action cluster so the cluster stays thumb-reachable on the narrowest iPhones.
There is no iPhone Settings screen, Project Gallery management grid, Attribute Table,
Create Feature flow, Map Layout, or Open Data Library in v1.
Global state concepts the iPhone observes (narrow subset):
- Active project / open document session.
- Document location and status: local, iCloud available, downloaded, edited locally,
conflict, unavailable.
- Layer visibility.
- Selected feature (single).
- Current edit session and dirty (needs-publish) state.
- Viewport: center, zoom, bearing, pitch, scale, coordinates.
Reusable Components
Reuse the shared Aero components (glass, icon button, primary button, ghost button,
coordinate readout). The iPhone replaces the iPad’s persistent panels and tool dock with a
compact top status pod, a bottom action cluster, and sheets. The drawing-related components
(tool dock, draw bar, multi-selection bar) do not appear on iPhone.
Aero Glass
Same three-layer anatomy (material + 1 px border + inner highlight + soft shadow + grain).
Used sparingly: top status pod, bottom action cluster, coordinate chip, navigation cluster
chips, toasts, and sheet headers. Sheet bodies that carry dense content (attribute form,
layers list) use the near-opaque surface-sheet variant for legibility.
Phone Status Pod
Top-leading floating context, the iPhone analogue of the iPad Project Pod merged with save
state.
Content:
- Back chevron in an accent-soft tile → returns to the Phone Project Library.
- Project name (single line, truncates with tail).
- Mono document/save state line: e.g.
iCloud · synced, edited · 3 changes,
conflict, offline copy.
Behavior:
- The state line updates live as edits accrue and as document status changes.
- In very narrow width, the state line collapses to a single status glyph + the Save
affordance carries the dirty count.
Save Affordance
A single primary control that commits the local edit session and publishes the package to
iCloud. It is the iPhone’s most important action.
Specs:
- Top-trailing, or merged into the bottom action cluster — choose one consistent placement;
recommended top-trailing so it pairs with the status pod and stays clear of the thumb
zone used for map gestures.
- Clean state: ghost/quiet style, label
Saved.
- Dirty state: accent fill, label
Save to iCloud with a mono change count where useful.
- Publishing: in-progress spinner; never blocks map interaction.
Bottom Action Cluster
Floating bottom-center glass cluster, the iPhone’s reachable command surface (replaces the
iPad tool dock). It contains only viewing/annotation actions — never drawing tools.
Content (left to right):
- Layers — opens the Layers Sheet.
- Map — a menu of map tools: Basemap (open the Basemap Sheet), Bookmarks (open the
Bookmarks Sheet), and Measure (begin read-only tap-to-measure).
- Add Note — a menu to begin text or callout note placement.
- Inspector — present only when a feature is selected; opens/expands the Feature
Inspector. Hidden when no feature is selected.
While measuring, a compact glass measure bar appears above the cluster with the live
distance/area readout (SF Mono), a Line⇄Area toggle (enabled once 3 points exist), an undo,
and a Done action.
Specs:
- Frosted glass pill, 24 pt radius, 8 pt padding.
- Icon buttons 46 to 48 pt, icon above 10.5 pt label.
- Active state uses accent fill / accent-soft as per Aero.
- Sits inside the safe area, clear of the home indicator.
Coordinate Readout
Same as iPad: bottom-leading glass chip, accent dot, latitude/longitude and zoom in SF
Mono, legible over all basemaps. Updates with map-center/crosshair context.
Navigation Cluster
Bottom-trailing map controls, compacted for iPhone.
Content:
- Scale bar (SF Mono label).
- Compass (accent-deep needle) — tap resets bearing to north.
- Pitch reset — tap returns pitch to 0 (flat); may be combined with the compass into a
single “reset to flat north” control with a clear accessible label.
- Optional zoom +/- stack (gestures are primary; include the stack only if it does not
crowd the safe area).
Rules:
- Do not duplicate MapLibre’s built-in controls when custom controls are shown.
- The reset control must be obvious — users tilt/rotate by accident on phones and need a
one-tap recovery.
Screen: Phone Project Library
Purpose:
- The iPhone’s first screen. Browse and open iCloud Drive
.gispad packages authored on
iPad/Mac, with clear document states. No creation, import, or management beyond opening.
Layout:
- Single-column scrolling list (not the iPad multi-column grid + nav rail).
- Top: title (
Projects), and an overflow/menu for Open from Files… and an account/iCloud
status affordance. No New Project tile.
- Rows are full-width project cards with a cached map thumbnail, project name, mono
metadata (layer count, last edited), and a document-state badge.
Document-state badges (first-class, not color-only):
iCloud — available, not downloaded → tapping triggers download with progress.
Downloaded — local copy current.
Edited — local working-copy changes not yet published.
Conflict — remote changed since the working copy’s base revision.
Unavailable — iCloud off / not signed in / network needed; row explains the reason
calmly and offers Open from Files….
Behavior:
- Tapping an available-but-not-downloaded project downloads it (coordinated copy into the
app-local working directory), then opens the workspace.
- Tapping a downloaded/edited project opens the workspace directly.
- Tapping a conflict row may open the workspace and surface the conflict on next publish, or
offer reload/conflict-copy choices up front (see Conflict Resolution Sheet).
Open from Files… is the fallback for packages outside the app iCloud container.
- Empty state: a calm message that projects are created on iPad or Mac and stored in iCloud
Drive, plus the
Open from Files… action. Do not show a New Project tile or generic
empty illustration cards.
Screen: Phone Map Workspace
Purpose:
- The core viewing, attribute-editing, and note surface.
Layout:
- Full-bleed MapLibre canvas; all chrome floats above it.
- Top-leading: Phone Status Pod.
- Top-trailing: Save Affordance.
- Bottom-center: Bottom Action Cluster.
- Bottom-leading: Coordinate Readout.
- Bottom-trailing: Navigation Cluster (scale, compass + pitch reset, optional zoom).
- Portrait is primary; landscape supported with the same anchoring.
Map interaction:
- Pan, pinch-zoom, two-finger rotate (bearing), two-finger vertical drag to tilt (pitch).
- Pitch defaults to 0, clamped to a comfortable max; bearing and pitch each have a one-tap
reset (see Navigation Cluster).
- Pan/zoom/rotate/tilt stay smooth while sheets are presented.
Selection:
- Tapping the map hit-tests visible authoritative layers only, tuned for fingertip
tolerance. Markup/notes are non-authoritative and are not attribute-selectable.
- A hit selects the feature (single selection), shows accent selection styling on the map
(selected fill/stroke per Aero; vertex handles stay hidden — there is no vertex editing),
and reveals the Inspector entry in the action cluster + presents the Feature Inspector at
the medium detent.
- Tapping empty map space clears selection and dismisses the inspector.
Camera persistence:
- Center, zoom, bearing, and pitch persist locally with the working copy and are included
on explicit publish. Camera-only browsing must not raise an unsaved-work prompt.
Sheet: Layers (Visibility Only)
Purpose:
- Toggle visibility of the project’s layers. Nothing else.
Layout:
- Bottom sheet, medium detent by default, expandable to large for long layer lists.
- Header:
Layers + mono layer count.
- Rows: layer swatch, layer name, mono feature count, and an eye toggle as a full-height
(≥48 pt) tap target.
Behavior:
- Eye toggle changes visibility live on the map.
- Hidden rows use the eye-off icon and reduced opacity.
- No reorder, opacity, style, labels, filters, lock, rename, delete, or metadata editing —
these controls do not appear.
- Toggling visibility marks the document as needing publish (it is persisted project state).
- Locked or otherwise non-editable layers still toggle visibility; the restriction only
affects attribute editing, surfaced in the inspector.
Sheet: Feature Inspector
Purpose:
- Read a tapped authoritative feature and enter the attribute form.
Layout:
- Bottom sheet at the medium detent over the map (map stays interactive above it).
- Header: accent icon tile + feature title.
- Mono subtitle: geometry summary, e.g.
Polygon · 5 vertices.
- Read-only WGS84 measurement tiles (area / perimeter / length / vertex count) where
available, from the geospatial core, in SF Mono.
- Key/value field list with top hairline separators.
- Primary action:
Edit Attributes (opens the Attribute Form at the large detent).
Behavior:
- Expanding the sheet to large shows more fields without leaving the map.
- If the feature’s layer is hidden or locked, show a calm inline note and disable
Edit Attributes rather than failing on save.
- Dismissing clears selection.
Purpose:
- Edit the attributes of the selected authoritative feature. Attributes only — no geometry.
Layout:
- Large-detent / full-height sheet, phone-optimized (single column, generous fields).
- Header: feature title + mono geometry/layer subtitle +
Cancel and Save.
- Editable schema fields (text, number, select/domain where defined).
- Read-only spatial values (area, perimeter, length) in SF Mono, sourced from the core.
- Validation messages inline next to the relevant field.
Behavior:
Save validates required attributes, then creates a local UpdateProperties edit-session
change and marks the document as needing publish. It does not publish to iCloud by
itself — that is the separate global Save affordance.
- Blocking validation errors prevent save; warnings stay visible but do not strand the edit.
Cancel discards the form edits without touching the edit session.
- Do not expose editor-only IDs, draft/session IDs, or render-helper data as editable
fields.
- A short toast confirms the local edit, e.g.
Updated · save to iCloud when ready.
Flow: Add A Note
Purpose:
- Place lightweight, non-authoritative text/callout annotations.
Steps:
- Tap
Add Note in the bottom action cluster.
- Choose
Text point or Callout (a compact picker; default to text point if only one is
offered initially).
- Tap the map to place the note anchor.
- A compact editor sheet captures the note text (and callout target/leader for callouts),
reusing
createMarkupLayer / addMarkupItem / updateMarkupItem.
- Confirm places the note; it renders immediately on the map as non-authoritative markup.
Rules:
- Markup is excluded from analysis/export rules and is never attribute-selectable as an
authoritative feature.
- Freehand, arrows, shapes, and any geometry editing remain deferred and must not appear.
- Adding/editing a note marks the document as needing publish.
Flow: Save To iCloud (Publish + Conflict Detection)
Purpose:
- Commit local edits (layer visibility, attribute edits, notes, camera) and publish the
.gispad package back to iCloud Drive safely.
Happy path:
- The Save Affordance shows dirty state with a change count.
- Tapping it commits the local edit session, checkpoints SQLite, and stages a clean package
(omit
-wal, -shm, and rebuildable cache artifacts; run integrity/privacy checks;
keep basemap credentials and editor-only IDs out of the package).
- The app re-reads the source package revision/fingerprint in iCloud.
- If unchanged, it atomically replaces the iCloud package, advances the stored base
revision, clears dirty state, and shows a confirmation toast.
Conflict path:
- If the source revision changed since the working copy’s base revision, block the
overwrite and present the Conflict Resolution Sheet. Never last-writer-wins, never
auto-merge in v1.
Sheet: Conflict Resolution
Layout:
- Focused modal sheet with a calm explanation: the iCloud copy changed since you started
editing.
- Three explicit, equal-weight choices:
- Reload iCloud version — discard the local working copy (with a clear warning about
losing local edits) and re-open the current iCloud package.
- Save as conflict copy — write the phone working copy alongside the original as a
distinct conflict-copy package in iCloud, preserving both.
- Cancel — keep editing locally; nothing is published.
Rules:
- Copy must be calm and non-accusatory; never show raw revision hashes as the primary
message (they may appear in a details disclosure).
- The destructive option (Reload) must clearly state what local work is lost.
Document States And Status Display
The iPhone treats document availability and sync as first-class, visible state. Surface
these consistently in the Project Library badge and the Status Pod line:
iCloud available (not downloaded) → offer download with progress.
Downloading → progress, cancellable where possible.
Downloaded / synced → clean, current.
Edited locally (needs publish) → dirty; Save affordance is accent-filled.
Conflict → remote changed; routes to the Conflict Resolution Sheet on publish.
iCloud unavailable → explain (signed out / iCloud Drive off / offline) and offer
Open from Files….
Do not rely on color alone for any of these — pair with glyph and text.
Camera And Pitch
MapCameraState is extended with pitch (default 0). Codable defaults, Rust DTO/manifest
camera persistence, bookmarks, navigation callbacks, and the MapLibre apply/readback paths
all round-trip pitch.
- iPad and Mac camera defaults are unchanged by the addition of pitch.
- PDF/layout snapshots remain top-down (pitch 0) unless a future layout phase explicitly
opts into pitch.
- Persist center, zoom, bearing, and pitch with the working copy; include on explicit
publish; never force an unsaved-work prompt for camera-only browsing.
Adaptive Behavior
Primary target: iPhone portrait.
- Keep the map full-bleed in both orientations.
- Use system sheet detents; only one primary sheet presented at a time (replace, don’t
stack).
- Keep the bottom action cluster, coordinate readout, and navigation cluster inside the
safe area and clear of the home indicator.
- In landscape, keep the same anchoring; allow sheets to use a narrower fixed width if the
system presents them as form sheets.
- Larger iPhones and smaller iPhones share the layout; scale spacing, not control sizes,
below the minimum touch targets.
Dark and light:
- Follow system theme; all Aero tokens have light/dark variants; grain slightly stronger in
dark mode.
Accessibility
Required:
- Accessible labels for every icon-only control: back, save, layers, add note, inspector,
compass/bearing reset, pitch reset, zoom.
- Dynamic Type across the Project Library, sheets, inspector, and attribute form; prefer
reflow/scroll over truncation at large sizes.
- VoiceOver labels for selected feature, layer visibility state, document state, dirty
state, and the three conflict choices.
- Sufficient contrast for text and chips over light and dark maps.
- Do not rely on color alone for selection, dirty, document-state, locked-layer, or conflict
states.
Map accessibility:
- Provide an accessible summary for the selected feature.
- Provide an accessible description of camera reset controls (e.g. “Reset map to flat,
north up”).
Implementation Checklist
Platform setup:
[ ] TARGETED_DEVICE_FAMILY = "1,2" and iPhone orientations; iPad/Mac unchanged.
[ ] PlatformCapabilities.isPhone flag and phone routing in AppRootView.
iCloud documents:
[ ] ProjectDocumentCoordinator (list / open / download / publish / conflict-copy /
discard) over iCloud Drive .gispad packages via NSFileCoordinator.
[ ] Package ProjectDocumentRevision in project.json with migration defaults.
[ ] Clean publish: SQLite checkpoint, omit -wal/-shm/cache, integrity + privacy
checks, atomic replace.
[ ] Conflict detection on revision/fingerprint before overwrite.
iPhone screens and sheets:
[ ] PhoneProjectLibraryView with document-state badges + Open from Files….
[ ] PhoneWorkspaceView + PhoneWorkspaceModel (narrow model over shared services).
[ ] Status Pod + Save Affordance.
[ ] Bottom Action Cluster (Layers / Add Note / Inspector).
[ ] Layers Sheet (visibility only).
[ ] Feature Inspector sheet.
[ ] Attribute Form sheet (UpdateProperties edit session).
[ ] Add Note flow (text/callout markup).
[ ] Conflict Resolution sheet.
[ ] Coordinate readout + Navigation cluster with bearing/pitch reset.
Camera:
[ ] MapCameraState.pitch codable migration + Rust DTO/manifest/bookmark round-trip.
[ ] MapLibre apply/readback for pitch; one-tap reset to flat north.
[ ] iPad/Mac defaults unchanged; layout snapshots remain top-down.
State:
[ ] Document location/status on gallery items (ProjectSummary extension).
[ ] Dirty (needs-publish) state from visibility, attribute edits, and notes.
[ ] Selection sync between map and inspector (single selection).