In a multi-platform mobile product β iOS and Android β Clean Architecture and native UI still leave a practical problem: how do you change home-screen composition without rewriting SwiftUI and Android views every time, and without letting the two platforms drift apart?
That is what server-driven UI (SDUI) is for in this shape: the server owns screen composition; clients keep native presentation. A render document describes what appears and in what order; each platform maps those sections with its own toolkit β SwiftUI on iOS, Compose on Android. I put this into practice in HornsApp and Muvin: a shared screen contract (Kotlin Multiplatform), with layout updates delivered from the server.
A concrete scenario
Think of a home screen as an ordered list of section types, for example:
- a hero or featured carousel
- a βupcomingβ list with a title
- a promotional card
- a call-to-action block
Brand A might show carousel β list β CTA. Brand B might show list β promo β carousel. Same section types, different composition. Same native components underneath. When marketing or product wants a new order, the change can ship with the document β not a full client release for every experiment.
Without a server-driven approach, that difference often becomes duplicated view code β or flavor forks and #if brand branches that are hard to keep honest across platforms.
What you are trying to protect
Three goals show up again and again in this scenario:
1. One contract for two platforms. Android and iOS should agree on what a βscreen documentβ is: which section types exist, what payload each one carries, which navigation targets are allowed. One definition of the home screen, two native apps.
2. White-label without rewriting UI. New brand β new configuration from the backend, not a new app fork. Shared components stay shared; only composition changes.
3. Layout changes without redeploying look-and-feel. Moving a section or swapping order should be a server/config change when possible. Fonts, colors, motion, and accessibility stay in native UI (SwiftUI / Compose).
If your team is living through βsmall layout tickets that touch every client,β you are already in this use case.
How to split the system
A clear way to draw the boundaries:
βββββββββββββββββββββββββββββββββββββββββββββ β Server / remote render document β β Which sections exist, and in what order β βββββββββββββββββββββββββββββββββββββββββββββ€ β Shared layer (e.g. Kotlin Multiplatform) β β Screen definition, navigation targets β βββββββββββββββββββββββββββββββββββββββββββββ€ β Native presentation β β Android Compose Β· SwiftUI β βββββββββββββββββββββββββββββββββββββββββββββ€ β Navigation (Router) β β Tab switches, pushed screens, links β βββββββββββββββββββββββββββββββββββββββββββββ
Read it top to bottom as a lesson: who owns structure, who owns the contract, who owns presentation, who owns navigation between screens. Each layer should have one job. The design is about what can cross a boundary β and what should not.
Structure vs presentation
Teach this distinction early β it is what keeps SDUI from becoming βremote Viewsβ:
- Structure = βthere is a carousel, then a titled listβ (owned by the document / server).
- Presentation = βhow that carousel looks and animates on each platformβ (owned by the client).
The render document defines section types and order. Different brands reuse the same types with different composition.
On the UI side, map each section type to a native component. Views should render a typed model β they should not parse raw JSON and invent layout policy themselves. That keeps presentation testable and keeps the document boring (in a good way).
Why a shared layer helps
A shared module (Kotlin Multiplatform is one option) can own:
- section type definitions
- navigation targets
- the shape of the render document
Both apps then ask the same question from the same place: what should this screen contain? Parsing and validation live here so you do not maintain two incompatible models. This layer is not UI β that is the whole point.
SwiftUIβs job on iOS
On iOS, SwiftUI owns the experience: layout polish, accessibility, gestures, platform conventions. Translate the shared contract into a small set of presentation models the view layer understands β the same mapping idea Android uses for its native components.
Rule of thumb: if it is visual behavior, it is native. If it is composition of the screen, it is server-driven data.
Navigation as its own concern
The document can say βopen Upcomingβ or βopen this URL.β That is a destination, not a navigation implementation. A router turns destinations into actions: switch tab, push detail, open in-app browser.
Views report intent; they do not own the full navigation graph. As the home screen grows, that separation keeps the flow readable on both platforms.
Checklist you can take to your team
- Composition is server-driven. Screen structure lives in a remote document, not scattered in views.
- Presentation is native. Each platform keeps its own look and feel.
- The contract is shared. iOS and Android read one screen definition.
- Navigation is centralized. Views pass targets; one layer executes them.
- Updates can go live. Fetch on launch (and optional push/socket) without rewriting how sections render.
What you gain (and what you trade)
You pay a bit of upfront structure: contracts, mappers, a router, and a delivery path. You gain a screen model that scales across platforms and brands β layout experiments and white-label composition can move with the server while clients stay native and shippable.
If that sounds like a problem your team already has β multi-platform home screens, white-label variants, layout tickets that never stay in sync β this SDUI design is meant to be reusable. Start with the shared contract and native maps; wire live delivery when product needs it.
Next in the series: how the mapping layer turns shared models into SwiftUI views, how navigation stays aligned with Android, and how one SwiftUI base supports more than one brand.
I build native iOS and Android apps and write about designs that survive real shipping. If this is a problem your team is facing, let's connect on LinkedIn.