4. What About Props?

Most of the native components implement configuration properties and callbacks. In this part of the tutorial, we’re going to bridge two simple properties: count: Int and onCountChange: RCTBubblingEventBlock .

This is probably the most complex part of this tutorial. Data forwarding is anything but easy.

First of all, we can’t use a standard RCT_EXPORT_VIEW_PROPERTY macro as it generates setters that attempt to access the view instance. Unfortunately, SwiftUI views can’t be exposed to Objective-C directly as there is no corresponding type for Swift’s struct .

This brings us to the point where the only way to pass data is through the custom Swift proxy that can access both runtimes: Objective-C to receive view properties from JavaScript and Swift to set those properties to the corresponding SwiftUI view.

Sounds pretty complicated, but don’t let it scare you! Here is how it works:

The manager of the native component receives new properties through the special setters, generated by the macro like RCT_EXPORT_VIEW_PROPERTY during the pre-compilation routine.

Every time you use a macro like above, you generate a custom setter for the property you wrap (see how #define macro works), so when you send a new set of props from JavaScript, React Native checks if the corresponding setter for the new properties exists in the manager.

TL;DR: RCT_EXPORT_VIEW_PROPERTY -like macros generate property setters that are invoked when the user assigns new properties to the JavaScript representation of the native component.

Since we can’t use RCT_EXPORT_VIEW_PROPERTY , I propose we create our own macros. Following the RCT_EXPORT_VIEW_PROPERTY naming convention, I called it RCT_EXPORT_SWIFTUI_PROPERTY and RCT_EXPORT_SWIFTUI_CALLBACK .

Each of these macros leverages the functionality of RCT_CUSTOM_VIEW_PROPERTY , which allows the developer to create a custom setter.

If you decide to use them, bear in mind that they work only under the following conditions:

You have a proxy class that exposes a static NSMutableDictionary called storage .

called . RCT_EXPORT_SWIFTUI_PROPERTY supports only basic properties like int , string , bool , etc. Complex types require proper casting and are beyond the intended scope of this article

As I previously mentioned, each of the custom macros relies on React Native’s RCT_CUSTOM_VIEW_PROPERTY macro that allows developers to create custom setters for the props. This macro requires a block that defines the setter.

In the scope of the block, along with the class properties, React Native exposes three parameters:

UIView *view — A recipient of the props.

— A recipient of the props. UIView *defaultView — A default view.

— A default view. id json — Pointer to the received data.

We are mostly interested in json and view . One may notice that the type of the view is different. Instead of a SwiftUI view, we get a regular UIView that UIHostingController exposes for bridging. Unfortunately, it is not enough.

To set the new props, we need to get a reference to the real SwiftUI view. The only way to do it at this point is through the UIHostingController , but the controller itself is also out of reach…

To solve this issue, I created a key:value storage ( NSMutableDicrionary )and assigned it to the SwiftUIButtonProxy as a static property.

Now, if I store a view:proxy pair at the moment when the view is created, I will have access to the required proxy in the setter. Since our proxies are containers for UIHostingController s, it takes no effort to get the right ref to the SwiftUI view.

However, don’t forget that we’re operating this from Objective-C, which makes it impossible to directly manipulate SwiftUI view props.

We can work around this constraint if we define custom count and onCountChange properties in the SwiftUIButtonProxy class, so they propagate the new values using the controller’s rootView property:

It is a little bit tedious to write getters and setters for each property by hand (especially if you have a few of them), but this is the most straightforward approach I’ve found.

Also, note the props here: SwiftUI allows components to have a local state @State , exposed state @ObservedObject (props), and a shared exposed state @EnvironmentObject (context).

In this example, I’m using @ObservedObject as we need to set the values from the outside. In the SwiftUI view, it looks like this:

// SwiftUIButton.swift @ObservedObject var props = ButtonProps()

Now, let’s add a button to the view. Every time the user clicks that button, we want to call props.onCountChange with the new incremented counter value. With some stylistic changes, the SwiftUIButton code may look similar to this:

Now, to round off this example, we need to cover only two missing bits: ButtonProps and the JavaScript part:

class ButtonProps : ObservableObject {

@Published var count: Int = 0

@Published var onCountChange: RCTDirectEventBlock = { _ in }

} ButtonProps :count: Int = 0onCountChange: RCTDirectEventBlock = { _ in }

ButtonProps is an ObservableObject that holds the values we pass it from JavaScript. If you are puzzled by @Published , it’s fine. This directive creates a binding that tells SwiftUI to re-render if one of the published properties has changed.

The JavaScript part doesn’t:

That’s it! It was a long bumpy road, but you’ve made it through.

With a few minor tweaks like adding a button to your SwiftUI view and hooking up corresponding data with callbacks, you should have an application that counts from 0 to +infinity.

The resulting code (with some native UI tweaks) can be found in the tutorial repository, under the working-example tag.