ADR-0088 — iOS app redesign foundation: native SwiftUI, brand-aligned design system, graph-render spike (Track iOS 2.1, Fase A)¶
- Status: Accepted (foundation) — graph-render choice is Proposed, pending on-device verification (see §Graph rendering)
- Date: 2026-07-18
- Invariants touched: I2 (graph layout stays server-side / FA2-precomputed — the redesign consumes
GET /graphcoords, it does not run a client force layout), I8 (this ADR + branch is the D7 artefact for Fase A) - Feature refs: F1 (app shell / navigation), F4 (graph viewer), F15 (cross-platform — native iOS client), F16 (theming, light/dark parity)
- Workstream: 2.1 — Track iOS 2.0, Fase A (design foundation)
Context¶
The owner's directive (explicit, not inferred): the iOS app needs a complete redesign — graphics, performance, usability — and must end up at least comparable to the desktop app. This is not an API refresh of the existing client.
State of play at the start of Fase A:
- A native SwiftUI app already lives in
ios/(Synapse.xcodeproj, XcodeGen-generated fromproject.yml, bundle idai.synapse.mobile). It is functional but built against a pre-2.0 API and — critically — its design tokens (ios/Synapse/Theme/Theme.swift) were ported from an old mobile design handoff (Synapse.dc.html). Those tokens use Apple-system indigo (#4F46E5) as the accent and literal pure black (#000000) for primary label and dark background. Both violate the current Synapse brand: the desktop v1.7.0 / 1.9.3 "UI kit" uses brand blue#2563ebas the accent and never pure black (light ink#0f1729, dark ground deep-navy#0b1120). The iOS visual language had drifted from the desktop's. - Three stale branches were inspected before starting:
feature/ios-target— an abandoned Tauri v2 web-wrapper iOS attempt. Parked. Itsios/tree is a net deletion vsmain(main is far ahead). Not resurrected — see the platform decision below for why a web wrapper is the wrong tool for this redesign.feat/ios-native-app— the original native-app commit. Itsios/diff vsmainis a net deletion:mainalready contains it plus every subsequent refinement. Superseded.feature/ios-neural-refresh— an early "neural" visual pass (theNeuralMotif/AuroraBackgroundmotifs). Also a net deletion vsmain: those motifs already live inmain'sios/Synapse/Shared/Components.swift. Superseded.- Conclusion: nothing to salvage.
mainis the most advanced iOS state; all three branches are behind it. Fase A builds forward frommain, it does not merge any of them.
Fase A is the design foundation: it must land (1) this ADR, (2) a brand-aligned SwiftUI design system as real views, (3) a real, demoable navigation skeleton, and (4) mock Home + Wiki screens in light and dark — so the owner can approve the visual direction before Fase B wires real API-backed surfaces onto the new shell.
Decision 1 — Native SwiftUI is confirmed as the platform (not Tauri / web-wrapper)¶
The redesign target — "at least comparable to the desktop app" on mobile — is a UX target, not a "render the same pixels" target. Meeting it requires mobile-native interaction patterns that a web wrapper cannot deliver convincingly:
- Native navigation: a real
TabView+NavigationStackwith the system push/pop transitions, large-title collapse, swipe-back edge gesture, and toolbar semantics. A web wrapper reimplements these in JS and always feels a step behind. - Gestures & haptics: swipe actions on list rows, pull-to-refresh, context menus,
sensoryFeedback. These are first-class in SwiftUI and awkward-to-impossible to make feel right in a WKWebView shell. - Performance & memory: a native list (
List/LazyVStack) with cell reuse beats a DOM/virtualised web list inside a webview for scroll smoothness and memory on a phone. - System integration: Dynamic Type, SF Symbols, safe-area handling, Reduce Motion, light/dark following the system — all free and correct natively.
- Head start: the existing native app is a real asset (networking layer, Codable models, feature screens). A web wrapper would throw that away to re-embed the desktop SPA, which is explicitly the layout we do not want to copy literally on a phone.
The desktop's own responsive CSS (ADR-0057, the ≤767px tier) already proves the web UI can
run on a phone — but that is the fallback, a shrunk desktop. The whole point of this track is
to go past that to a genuinely native experience. Tauri/web-wrapper is rejected for the
Synapse iOS app. feature/ios-target stays parked.
Decision 2 — Brand-aligned SwiftUI design system (supersedes the legacy Theme.swift)¶
A new ios/Synapse/DesignSystem/ package is the source of truth for the redesign's visual
language, ported faithfully from the desktop frontend/src/styles/theme.css (the 1.9.3 UI
kit), translated to native iOS idioms:
- Color tokens (
SynColor) — light/dark dynamic colours resolved against the systemcolorScheme, matching the desktop token-for-token: - accent
#2563eb(light) /#58a6ff(dark); accent-strong, accent-soft. - ink
#0f1729(light) /#e7ecf7(dark) — never#000000; ground#ffffff(light) /#0b1120deep-navy (dark) — never#000000. - the per-type jewel-tone palette (concept violet, entity blue, source teal, synthesis
indigo, comparison copper, query amber), lightened for the navy ground in dark, matching
the desktop
--syn-type-*values. - the brand gradient
#1d4ed8 → #4338ca → #7c3aed, used with restraint (wordmark, hero, primary CTA). - Type scale (
SynFont) — built on the native text styles so Dynamic Type works, with the wordmark tightened (-0.02emtracking) to echo the desktop Geist wordmark. - Spacing / radius tokens (
SynSpace,SynRadius) — the desktop 4/6/8/10/12/16 spacing ramp and 7/9/12/pill radii. - Core components as real views:
SynButton(primary/secondary/ghost/destructive),SynCard,SynChip,SynListRow,SynEmptyState,SynSkeleton(shimmer honouring Reduce Motion). Icons use SF Symbols chosen per meaning rather than transliterating the desktop lucide set.
The legacy Theme.swift / Components.swift are not deleted (the existing feature screens
still compile against them, so nothing is lost for Fase B to port), but they are marked legacy:
the redesign shell renders only SynColor, so no pure-black token is ever painted in the
new experience. Migrating the remaining real screens off Theme onto SynColor is Fase B work.
Decision 3 — Navigation: native 5-tab shell (Home · Wiki · Chat · Graph · More)¶
The desktop's 3-panel shell (tree / chat / preview) is not copied literally — that layout
is wrong for a phone. Instead a native TabView exposes the five primary destinations, each an
independent NavigationStack. This is the demoable skeleton: Home and Wiki carry realistic mock
content; Chat, Graph and More are honest placeholders built from the design system. Home is
new relative to the old shell (which led with Wiki) and becomes the landing surface (F18 home
dashboard lineage). The app entry (SynapseApp) now presents the redesign root; the old
RootTabView and its screens remain compiled (available for Fase B to port) but are not the
runtime entry.
Graph rendering — documented spike (I2-preserving either way)¶
The knowledge graph (F4) is the one surface where "native vs embed" is a genuine engineering
fork. Both candidates consume the server-side, FA2-precomputed coordinates from GET /graph
— neither runs a force layout on the device, so I2 holds in both cases. The question is only
who draws the precomputed points.
| (a) WKWebView embed of the sigma.js viewer | (b) Native renderer (Canvas / SpriteKit / Metal) | |
|---|---|---|
| I2 compliance | ✅ coords server-side, sigma just renders | ✅ coords server-side, native code just renders |
| Engineering cost | ~none — reuse the shipped web viewer | High — build + maintain a second renderer |
| Native feel | Poor — a webview island inside a native app; gestures/haptics/theming don't match the rest | Excellent — real pinch/pan, momentum, haptics, SF-styled controls, exact theme match |
| Maintenance | Follows the web viewer for free | Diverges; every graph feature done twice |
| Perf on device | Sigma is WebGL, usually fine; webview bridge + memory overhead unknown on older phones | Potentially best (SpriteKit/Metal), but unproven until built and measured |
| Consistency w/ redesign goal | Undercuts "at least comparable to desktop" — it is the desktop, embedded | Fully delivers the redesign intent |
Recommendation: (b) a native renderer — but staged. For Fase A the Graph tab is a
placeholder. The recommendation is to build the native renderer in Fase B starting with the
lightest technology that meets the bar — SwiftUI Canvas first (draw the precomputed nodes
/ edges, add MagnificationGesture + DragGesture for pinch/pan), escalating to SpriteKit
only if Canvas can't hold frame rate on a large vault, and to Metal only if SpriteKit
can't. This keeps the second renderer as small as the performance target allows.
Pragmatic hedge: ship (a) the WKWebView embed as a fallback / behind a flag in early Fase B so the Graph tab is functional while (b) is built and measured. If on-device numbers for (b) disappoint, (a) is the safety net — I2 is satisfied either way, so this hedge costs nothing architecturally.
What I could NOT verify — needs the owner's sign-off before Fase B locks the choice¶
A physical-device performance check is a prerequisite before committing to (b), and I cannot perform it from this environment (no physical iOS device; only the Simulator is reachable). The following must be measured on a real device against a real vault and signed off by the owner:
- fps panning/zooming a realistic graph (hundreds–thousands of nodes) — target a smooth ~60 fps (120 on ProMotion) with no hitching.
- memory footprint of the native renderer vs the webview embed under the same graph.
- gesture responsiveness — pinch/pan latency, momentum feel, haptic timing.
Until those numbers exist and the owner approves, the graph-render decision is Proposed, not Accepted. The rest of this ADR (native SwiftUI, design system, navigation) is Accepted.
Consequences¶
- The redesign has a brand-correct foundation: the new shell can never paint pure black and its accent is the real brand blue, matching the desktop token-for-token.
- The existing native app is preserved (compiles, not deleted) so Fase B ports real data into the new shell screen-by-screen with the old screens as reference — no big-bang rewrite.
- Two visual languages coexist transiently (legacy
Themevs newSynColor). This is an accepted, time-boxed state; Fase B's exit criterion includes deletingTheme.swiftonce the last real screen is migrated. - The graph renderer is deliberately left open with an I2-safe fallback, so Fase A does not gamble on an unverified performance assumption.
Verification (Fase A)¶
- Build:
xcodegen generate(project regenerated to include the new files) + a Simulator build (xcodebuild … -destination 'platform=iOS Simulator,name=iPhone 17'). What was actually run in this environment, and what still needs a human on a real Mac/device, is recorded in the PR description under "Verified locally vs needs owner". The honest posture per the track's "chiedere, non aggirare" mantra: Simulator evidence is not device evidence.
Future work (Fase B and beyond)¶
- Build the native graph renderer (Canvas → SpriteKit → Metal escalation), keep the WKWebView embed behind a flag as the fallback, run the on-device perf check, get owner sign-off.
- Migrate the real feature screens (Wiki list/detail, Search, Chat, Review, Ingest, Research,
Settings) onto
SynColor+ the new components; then deleteTheme.swift/ legacyComponents.swift. - Wire the redesigned Home + Wiki to live API data (F18 home dashboard, F1/F4).
Addendum — Fase C (Track 2.1, GA parity)¶
Fase C closed out parity: Graph, Review, Sources/Activity and a complete Settings
(providers/vault/language) are live against the 2.0.0 backend, and the legacy
Theme.swift + Features/ tree from item 2 above is deleted — SynColor is now
the sole visual language, so no pure-black token exists anywhere.
The graph-render decision remains Proposed, not Accepted. Fase C shipped the
ADR's recommended native SwiftUI Canvas renderer, but deliberately behind a
swappable seam: makeGraphRenderer(kind:) (Redesign/Graph/GraphRenderer.swift)
is the one place the concrete renderer is chosen, so a .webViewSigma case + a
SigmaWebGraphView can replace it later without touching GraphScreen, the view
model, or the DTOs. I2 holds either way — both consume the server-precomputed
GET /graph coords; the Canvas never runs a layout.
What is still owed by the owner before the decision can be marked Accepted (carried over unchanged from Fase A — no physical iOS device was reachable in Fase B or C either, Simulator only):
- fps panning/zooming a realistic vault (hundreds–thousands of nodes) on device;
- memory of the native renderer vs a WKWebView-sigma embed under the same graph;
- gesture responsiveness (pinch/pan latency, momentum, haptics).
If those numbers disappoint on a real device, flip the seam to the WKWebView fallback; the architecture already accommodates it at zero cost to the rest of the app.