Server-Driven UI using SwiftUI

Elyes DER
6 min readDec 13, 2022

Introduction

WWDC2017, Apple introduced Codable, a powerful and convenient tool for working with data in Swift, and it is widely used in a variety of applications and contexts. A variety of applications, right ? SwiftUI, introduced in WWDC2019, as an intuitive framework for building user interfaces and provides a declarative syntax for creating user interfaces in a way that is easier to read and write than traditional imperative approaches. View in SwiftUI can conform to Codable, means that a view can be serialized and/or deserialized for storing, caching and ... networking.

What if we can request a View from a server ?

It's what we call Server-driven UI, it allows building user interfaces that are dynamic and responsive to changes in data on the server. This is particularly useful in modern applications, where data is often generated or updated in real-time and needs to be reflected in the user interface as quickly as possible.

Server-driven UIs also make it easier to build applications that are scalable and maintainable. Basically, the server is responsible for managing and updating the data that is displayed in the user interface, it is easier to make changes to the user interface without having to update the client-side code. This can save time and effort, and it can also help to prevent inconsistencies between the data on the server and the data displayed on screen.

In this blog post, I will introduce you to some key aspect and pseudo-implementation of Server-Driven UI using SwiftUI.

Approach

First and foremost, implementations differs based on real word problems we intended to solve, the context of its usage and the trade-offs to consider. This project was intended to explore the de-serialization of a view, it’s properties and user-interactions that are retrieved from the server. A JSON content are grabbed from the server as a model structure of the view and rendered on-screen.

The class diagram below is used to model the objects that make up the serialization and rendering engine, to display the relationship between the components, and to describe what kind of data, configurations is used.

Let’s briefly define each object model:

Window: used as a parent container, holds the view hierarchy and implements different global configuration, ex: Device Orientation, Scroll wrapper, etc...

Container: defined by a frame, a configuration, an action, and wraps one global Layout.

Layout: Main content layout of type `Z`(aka ZStack), `V`(aka VStack), …

Content View: An array of sub-views that are recursively rendered inside the container.

To summarize, a Window object is requested from the server, wraps a container made of a global layout and sub-containers. Each layout contains a set of primitive SwiftUI.View with a configuration and a set of actions.

Fundamentals

This approach only made possible thanks to the new Layout type introduced in SwiftUI 3. The container object will hold layouts conforming to the new Layout protocol, which offers containers based on server configuration.

Views conforms to Codable protocol and have multiple configuration properties that are handled by SwiftUI.ViewModifier.

Implementation

After defining each object model conforming to the diagram, we need to properly decode and build a SwiftUI View based on the API response, render it correctly on the screen using given configuration.

Configurations will be implemented using native or customViewModifier.

Window

A server response is wrapped in aWindow components that mimic SwiftUI.WindowGroup, handles global view configurations and call the render method of its component to generate the view's hierarchy.

Window object as a parent of the response

Container

Each container have its own layout grouper, a set of primitive Views that conform to ViewRenderer protocol. Containers can also implement different actions.

Container holds a main Layout, and a set of sub-views of type Container

Layout

First window’s container, implements the method protocol LayoutRenderer.render() function to tell how they are drawn on screen based on the configuration and returns any Layout conforming object.

Layout containers (aka VStack, HStack) conforms to LayoutRenderer

Using LayoutRenderer will return a generic composable view of type HStackLayout, VStackLayout, ZStackLayout that will hold, arrange the sub-views.

View

Represent a primitive SwiftUI.View with a configuration. Configuration such .frame, Padding, cornerRadius are handled by SwiftUI.Modifier .

Supported primitive views

List of supported Views are currently limited for testing purposes, but It should be easy to implement more.

Screen rendering

An engine acts as a view model, responsible for data request from server and decode it, returns a window, and it’s sub-views that are rendered on screen.

RequestAndRenderView() function actually runs a basic URLSession.request and decode data using a JSONDecoder as follows:

let decoder = JSONDecoder()
let window = try decoder.decode(Window.self, from: data)
return window

In this section, a pseudocode is presented to keep this post about the approach clear and understandable. In the following section, we will show on-screen basic UI Components requested from an API.

Hands On

For the examples below, a side by side comparison between the API JSON response and the resulting view shown on screen.

Example 1: Basic

Let’s have a vertical aligned Image with a text description.

Basic example of an AsyncImage with a text description

Example 2: Scrollable content

Let's slightly adopt the above basic view and show it in a horizontal scrollable list.

Horizontal scrollable card views

Example 3: Multiple views layout

For this last example, let’s combine multiple views requested each from separate APIs.

Multiple views layout

Challenges

Server driven UI have its own benefits and its drawbacks. First, let’s talk about benefits

Benefits

  1. Improved scalability: examples shown above demonstrates how an application can be extended, scaled up or down to accommodate changes or user demand.
  2. Easy to maintain, easy to deploy: making changes and updates to the interface can happen without requiring a new submit to the App Store and wait for a review or even users to download new versions of the application, it’s an instant deployment.
  3. A/B Testing: can run simultaneous number of tests based on criteria’s, this will fuel data analytics.
  4. Security: Application doesn’t hold any kind of data or view hierarchy in its source code, reverse engineer it’s binary will actually return an empty container.

Drawbacks

  1. Performance: While the backend has to process each user request and generate a new page or a fragment, servers under heavy load can alter the user experience and make the application less responsive.
  2. Performance 2: Response payload can return a big amount of data, which causes a high memory footprint used by our application to decode, especially if there are many components to render.
  3. Data flow: Most of the views rendered serves as an output, but managing user inputs such as actions, data input or interactions can be quite challenging and complex.
  4. Complexity: Can be more complex to implement and maintain, since they require the development and deployment of both server-side and client-side code. Moreover, Each native UI component has to be extended, correctly adapted to conform to API response.

Summer up

Server-driven UI is an approach that relies on the server to generate and deliver the user interface, rather than the client’s application. This approach allows for the creation of dynamic and interactive apps, as well as the ability to reuse the same UI code across different clients. In this article, we discussed the basics and the fundamentals of server-driven UI, showed pseudo-implementation, provided examples of how it can be used in code using SwiftUI, and talked about the faced challenges, including its advantages and disadvantages.

Conclusion

In this post, we introduced a new emerging paradigm, explained one of many approaches and used native Apple frameworks to achieve the result. As mentioned earlier, implementations can differ from one to another, based on real word problem. This approach made it easy to process, build and render correctly, however, it also has its challenges and drawbacks, including potential performance issues and complex implementations.

Thank you for tuning in, I hope it provided some useful information and insights about this approach. As a software engineer with a passion for writing about complex computer science subjects, I strive to make technical topics accessible and interesting to a wide audience. I welcome any feedback or questions you may have, and I look forward to sharing more of my knowledge and experiences with you in the future.

Reach me out on Twitter.

--

--

Elyes DER

Enthusiast Software Engineer, iOS Developer @Oodrive