SwiftUI renderer for React Native (Part II)

Tomasz Sapeta
Software Mansion
Published in
6 min readFeb 3, 2021

--

In this series of two blog posts I describe the research break I took from my regular work at Software Mansion to investigate whether it’s feasible to build a custom React Native renderer that would be based on SwiftUI instead of UIKit.

In the first post I explained what was the motivation behind this, what the goals were and what problems I ran into. Now, it’s the time to dive deeper and talk more about implementation details. Let me start by saying that the repository with the result of my work is finally available publicly at https://github.com/software-mansion-labs/react-native-swiftui 🥳

Fabric

As React Native is now going through the process of re-architecting its UI-layer, it seemed obvious to consider using the new implementation, called Fabric. If you haven’t heard about Fabric yet, I highly recommend reading Lorenzo’s blogpost that will give you an overview. In our case, the most important aspect of using Fabric vs the old UIManager approach is that Fabric provides a nice abstraction that allows for building custom native component renderers.

Once we settled on building our SwiftUI renderer on top of Fabric, there were two components of the system that required our attention: React-Fabric and React-RCTFabric. The difference between them is that Fabric is a core cross-platform implementation of UI manager and also an interface for the proper rendering implementation, which is what RCTFabric provides. The latter heavily depends on UIKit so the goal was to replace this implementation completely and therefore remove dependency on UIKit altogether.

As you can see, the new Fabric architecture provides an API that makes it possible to swap out the layer that integrates with the platform native components thus allowing for this implementation to be completely replaced by something that does not depend on things like the UIKit library.

The Surface

Any time you want to let React Native run your JavaScript bundle and take care of what’s rendered inside the view, you use React’s root view and accompanying bridge. That’s where your app starts from. Since the AppDelegate is specific to UIKit applications, we need to use UIHostingController to integrate SwiftUI within the existing app. To do so, the default root view needs to be replaced by our own SwiftUI-backed view that uses the hosting controller and acts as a bridge between UIKit’s and SwiftUI’s view hierarchy.

Tree mutations

In RCTFabric all mutations on the view tree are handled by MountingManager which is responsible for creating, removing and updating UIKit views — in other words it manages the shadow and UIKit view trees and keeps them in sync. In SwiftUI however we don’t really interact directly with the native view tree in a sense that we don’t have access to mutable tree nodes — that’s what SwiftUI rendering engine takes care of. As a result, the responsibility of instantiating, removing and updating views can be shifted to SwiftUI. Instead of maintaining the native view hierarchy on our own, we can temporarily flatten it and only keep a registry of views instantiated by React and their corresponding attributes (including their children).

SwiftUI uses structs as the entry point of the individual view declaration. Structs in Swift are value types which results in them being recreated (copied, just like other primitive types) every time they’re mutated and therefore also when they’re rerendered. This complicates a bit, making us unable to keep references to those structs in order to properly map shadow nodes to SwiftUI view instances without having to write too much boilerplate code for each native component. To work around this, our React-compatible SwiftUI view representation needs to be of reference type — a class. This is what the class for view descriptors was made for. It knows what to render, and stores its view props, Yoga’s layout metrics, and an array of tags of React’s children nodes. These properties are updated accordingly to the mutations that happen in the React shadow tree. After each mutation, the descriptor bumps its revision number which then causes the SwiftUI view (a struct) to be rerendered.

Core components

The goal was to cover the most common functionality of React Native components — even without changing your JavaScript imports, if possible. In my implementation, all views are automatically positioned on the screen using Yoga layout metrics calculated from provided styles, such as flex and margins. The same happens with some basic styles like background, color and opacity.

The list of more component-specific props and other features is available in project’s readme.

Gestures

In UIKit there are two ways to handle touch gestures — one based on hit-testing, or using gesture recognizers. React Native’s default touch event system called JS Responder, relies on the former. It uses touch events delivered to the root view container and then traverses down the view hierarchy to find the view that should handle the touch event — usually it’s the frontmost visible view that contains the touched point. As it’s not possible to traverse SwiftUI hierarchy, we also can’t handle gestures that way. SwiftUI’s approach is more or less similar to what react-native-gesture-handler does, with the power of gesture recognizers. Given that React Native’s Button component is a JavaScript abstraction on top of Touchables, I couldn’t just switch its native implementation while keeping the original JavaScript code.

Creating native views

I’ve already mentioned about view descriptors and that they store all the data needed to render views. However, to make creating custom views more straightforward, the SwiftUI view declaration is provided by another class that conforms to `RSUIView` protocol. Its only requirement is to implement the render function that returns the view declaration based on given props. With the default UIKit renderer, you have to provide headers and the implementation for the view manager and the view itself. Usually it leads to writing a lot of boilerplate code for just one native view. As SwiftUI has some nice easy-to-use features built-in, here is such a simple example of a native view that adds a shadow around its children:

It looks like React component class in Swift! You can find the implementation of other components here.

Contributions

Contributions to this experimental project are more than welcome! There are some examples in the RNTester app to help you start. It’s worth emphasizing that this project isn’t production-ready and won’t be at least until SwiftUI has addressed the significant gaps in functionality. Either way, I believe that each upcoming SwiftUI version will bring some new features that would get us closer to satisfactory coverage of existing React Native capabilities.

Follow me (@tsapeta) and Software Mansion (@swmansion) on Twitter to get more updates in the future. If you do have any questions, send me a message there!

--

--