SwiftUI renderer for React Native
By: Tomasz Sapeta
Tags: User Interface, Swiftui, Mobile Development, React, React Native

Last year at Software Mansion we announced our full-time commitment to Open Source, and since then, we’ve managed to grow our React Native open source team which has greatly impacted the way our open source libraries like react-native-gesture-handler, react-native-reanimated or react-native-screens are maintained. Our responsiveness on issues and pull requests is now much better, developers get new releases on a regular basis, and we also have the opportunity to work on new investments in the area of React Native Open Source. A prominent example of such an investment is the work on the next iteration of the Reanimated library (read more in the announcement post). But we constantly seek out for new ways to contribute our skills to the development of React Native platform and its ecosystem, and it feels like the best way to do so in our position is to undertake new projects and experiment with some ideas that we have. In this post, I want to share the story of one such experiment we decided to try out recently and the lesson we learned from it — the experiment was about building a SwiftUI-backed renderer for React Native.

Motivation

Ever since React became mainstream we have observed a trend of new UI systems being designed around very similar, declarative principles. This trend goes beyond JavaScript frameworks, and recently reached the native mobile ecosystem with SwiftUI (by Apple) and Jetpack Compose (for Android) being the new contenders for becoming the default UI systems for their corresponding platforms.

We believe that React Native will need to adapt to those new ways of interacting with the platform-specific native UI APIs — the problem here is that there are many different ways of approaching such an integration, and none of them seemed trivial to us. React has been designed to work with tree-based UI systems (e.g., DOM, Android’s view system, UIKit, AppKit) by performing direct tree manipulations like adding new nodes or changing node attributes. With SwiftUI we no longer interface with the UI tree structure directly, and in fact the underlying tree does not even need to exist in some cases. This means that in React Native we can no longer hold references to native view instances and further use these references to make updates to the UI.

With a number of different ideas in mind, we decided to build a prototype implementation of the SwiftUI renderer for React Native — a drop-in replacement for the core’s UIKit renderer which may allow us to remove the dependency on UIKit altogether. It is important to note that embedding custom SwiftUI components in React Native is already possible (this can be done using a “hosting” container where components written with SwiftUI can be placed as a part of the UIKit view structure — we recommend this great post by Alexey Kureev which covers this approach). However, building a React Native renderer is a completely different thing that opens up a new set of possibilities and not only brings full control over how all view primitives are rendered, but also allows to freely build nested hierarchies of such views.

SwiftUI

SwiftUI is a declarative UI framework introduced by Apple in 2019. The keyword “declarative” makes it sound similar to React, doesn’t it? Behind the scenes, SwiftUI follows the same principles as React and React Native, especially the reconciliation concept that makes the rendering engine only re-render those views whose state has changed.

We are already seeing Apple invest heavily in unifying the software running on all of its platforms (and even hardware by switching Macs to ARM architecture). This also applies to the way they recommend developers build apps for these platforms. I wouldn’t be surprised if some new UI features come only to SwiftUI, just like how new features or frameworks are made available from Swift but not from Objective-C. Additionally, SwiftUI has some other advantages over UIKit:

First-class support for all Apple platforms: iOS, iPadOS, macOS, watchOS, tvOS and extensions such as iOS widgets or watchOS complications.

It provides many great perks right out of the box like handy built-in SVG-like shapes and visual effects such as shadows, masks, blurred views or even changing a view’s saturation.

A more composable approach. Instead of having a base class ( UIView ) that is heavy and complex as it has to know how to handle a lot of different properties, SwiftUI uses lightweight primitive views responsible for applying only specific kinds of effects. Every view in SwiftUI is just a struct that implements the View protocol, thus we’re not dealing with any base class.

) that is heavy and complex as it has to know how to handle a lot of different properties, SwiftUI uses lightweight primitive views responsible for applying only specific kinds of effects. Every view in SwiftUI is just a struct that implements the protocol, thus we’re not dealing with any base class. A much easier and cleaner way to create custom native components. It’s as easy as writing a few lines of code.

The scope of the research

During this research I tried to cover some core, currently existing functionality of React Native. The goal was to build a functioning React Native app demo without relying on UIKit for rendering any views. Below I listed the steps that I have gone through as part of this project:

Investigate how to integrate with Fabric and how to map shadow nodes to SwiftUI views instead of UIView s.

s. Use Yoga layout metrics to set the proper view size and offset.

Listen for shadow tree updates and update the UI accordingly.

Read component props passed from the JavaScript side and be able to use them in Swift.

Implement adding some basic styles like: background, color, border, and opacity.

Deal with text and its frame size that we must calculate and pass on to Yoga. Native text view takes only as much space as it needs and so Yoga itself cannot measure it properly in a cross-platform manner.

Send native events to JavaScript using event emitters.

Implement some of the most commonly used components like Button , ScrollView , Image and TextInput to check how much of the existing functionality we’re able to cover.

, , and to check how much of the existing functionality we’re able to cover. Try out some SwiftUI features such as masking a view with another, adding a shadow to the view or adding a blur.

I managed to put all the bricks together and came up with a foundation that allows us to build and run a simple application with proper layout, some styling and interactions. As it would be too much for a single blog post to discuss the implementation details, in this article we only focus on the results and limitations that we discovered while we will share some more insights about the internals in the follow up blog post.

iOS Widgets

iOS 14.0 introduced a new kind of app extensions — widgets. It turns out that they don’t support the traditional way of making apps based on UIKit. Surprisingly, UIKit is available there, however, it’s impossible to use it for rendering, making them the first feature of iOS that is designed to only support SwiftUI as the UI framework. The lack of UIKit support means we cannot use React Native for building widgets, and even if we know how we can embed SwiftUI components within the React Native view tree that still wouldn’t help here. However, since widgets can’t be too complex visually (they occupy a small portion of the screen and aren’t really interactive), we would only have to expose a small number of SwiftUI components to React Native. This sounds easy, but it’s actually a long way off. The SwiftUI-based React Native rendering layer would work here, even if the main app were to use the standard React Native renderer.

macOS

Recently, Apple announced the Mac Catalyst project (also known as UIKitForMac) which enables developers to make iPad apps available for macOS users too. That is amazing and good enough for many projects that don’t have to care too much about this platform and don’t use any of its platform-specific features. However it was never meant to be a fully-fledged solution to developing macOS apps. Some people got it running in React Native with a few modifications, but it isn’t going to be officially supported any time soon.

I must applaud Microsoft for putting a lot of effort into creating and maintaining react-native-macos which is a react-native fork with a lot of patches that makes React Native work on macOS based on the OS native UI framework — AppKit. As it turned out, React Native uses UIKit framework in many more places than just the UI-related codebase, so I had to use some of those patches to make my SwiftUI renderer compile on this platform.

Limitations

Life is real, nothing is ideal. With SwiftUI we gain a lot but lose something else. To put it bluntly—SwiftUI is still not as powerful as UIKit or AppKit and probably won’t be for the next few years.

For instance, I found it quite painful to work with text inputs ( TextField in SwiftUI’s nomenclature). The only thing that can configured is the value and placeholder. It’s not possible to change the tappable area, get cursor position nor read text selection. To support multiline input we have to use TextEditor which doesn’t even let us know once something is changed. Moreover, triggering on-demand events such as focus or blur is not possible and most methods that we call on components’ refs have no counterparts in SwiftUI yet. Lastly, there is no way to scroll to a specific point in scroll views like you can in UIKit. Instead, we can only scroll to a subview by its identifier (although this may actually be a good thing because it’s what we usually want to do).

Conclusion

A SwiftUI renderer may be a good solution to bring React Native to more devices, with just one UI codebase used across all Apple platforms. Of course, it’s hard to predict the direction of SwiftUI development and how fast we’ll get to the point where using SwiftUI is a reasonable choice, but it’s always better to be prepared

Here is a sneak peek of a React Native app and a widget that I’ve been using as a playground, entirely rendered by SwiftUI. It presents all of the currently implemented functionality.

In the next blogpost I’ll describe the technical side, show you some examples, and finally make the repository with our proof of concept public and open it for contributions.

… to be continued soon.