Swift Macros for React Native
ReactBridge
ReactBridge provides Swift macros for React Native to expose Native Modules and UI Components to JavaScript.
@ReactModule
- @ReactMethod
- @ReactView
- @ReactProperty
- XCode
- Swift Package
Getting Started
Native Module
Attach @ReactModule macro to your class definition and it exports and registers the native module class with React Native and that will allow you to access its code from
swift
import React
import ReactBridge
@ReactModule class CalendarModule: NSObject, RCTBridgeModule { }
Note
ReactBridgerequires to importReactlibrary.
Swift class must be inherited fromNSObjectand must conform toRCTBridgeModuleprotocol.
The @ReactModule macro also takes optional jsName argument that specifies the name that will be accessible as in your JavaScript code:
swift
@ReactModule(jsName: "Calendar")
class CalendarModule: NSObject, RCTBridgeModule {
}
Note
If you do not specify a name, the JavaScript module name will match the Swift class name.
Now the native module can then be accessed in JavaScript like this:
js
import { NativeModules } from 'react-native';
const { Calendar } = NativeModules;
Methods
React Native will not expose any methods in a native module to JavaScript unless explicitly told to. This can be done using the @ReactMethod macro:
swift
@ReactModule(jsName: "Calendar")
class CalendarModule: NSObject, RCTBridgeModule {
@ReactMethod
@objc func createEvent(title: String, location: String) {
print("Create event '\(title)' at '\(location)'")
}
}
Note
The exported method must be marked with @objc attribute.
Now that you have the native module available, you can invoke your native method createEvent():
js
Calendar.createEvent('Wedding', 'Las Vegas');
Callbacks
Methods marked with @ReactMethod macro are asynchronous by default but if it's needed to pass data from Swift to JavaScript you can use the callback parameter with RCTResponseSenderBlock type:
swift
@ReactMethod
@objc func createEvent(title: String, location: String, callback: RCTResponseSenderBlock) {
print("Create event '\(title)' at '\(location)'")
let eventId = 10;
callback([eventId])
}
This method could then be accessed in JavaScript using the following:
js
Calendar.createEvent('Wedding', 'Las Vegas', eventId => {
console.log(Created a new event with id ${eventId});
});
Promises
Native modules can also fulfill promises, which can simplify your JavaScript, especially when using async/await syntax:
swift
@ReactMethod
@objc func createEvent(title: String, location: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
do {
let eventId = try createEvent(title: title, location: location)
resolve(eventId)
}
catch let error as NSError {
reject("\(error.code)", error.localizedDescription, error)
}
}
The JavaScript counterpart of this method returns a promise:
js
Calendar.createEvent('Wedding', 'Las Vegas')
.then(eventId => {
console.log(Created a new event with id ${eventId});
})
.catch(error => {
console.error(Error: ${error});
});
Inheritance
It's possible to inherit an other native module (which implements RCTBridgeModule protocol) and override existing or append additional functionality. For instance, to signal events to JavaScript you can subclass RCTEventEmitter:
swift
@ReactModule
class EventEmitter: RCTEventEmitter {
static private(set) var shared: EventEmitter?
override init() {
super.init()
Self.shared = self
}
override func supportedEvents() -> [String]! {
["EventReminder"]
}
func sendReminderEvent(title: String) {
sendEvent(withName: "EventReminder", body: ["title" : title])
}
}
...
EventEmitter.shared?.sendReminderEvent(title: "Dinner Party")
Then in JavaScript you can create NativeEventEmitter with your module and subscribe to a particular event:
js
const { EventEmitter } = NativeModules;
this.eventEmitter = new NativeEventEmitter(EventEmitter); this.emitterSubscription = this.eventEmitter.addListener('EventReminder', event => { console.log(event); // Prints: { title: 'Dinner Party' } });
For more details about Native Modules, see: https://reactnative.dev/docs/native-modules-ios.
Native UI Component
To expose a native view you should attach @ReactView macro to a subclass of RCTViewManager that is also typically the delegate for the view, sending events back to JavaScript via the bridge.
swift
import React
import ReactBridge
import MapKit
@ReactView class MapView: RCTViewManager {
override func view() -> UIView { MKMapView() } }
Then you need a little bit of JavaScript to make this a usable React component:
js
import {requireNativeComponent} from 'react-native';
const MapView = requireNativeComponent('MapView');
... render() { return <MapView style={{flex: 1}} />; }
Properties
To bridge over some native properties of a native view we can declare properties with the same name on our view manager class and mark them with @ReactProperty macro. Let's say we want to be able to disable zooming:
swift
@ReactView
class MapView: RCTViewManager {
@ReactProperty var zoomEnabled: Bool?
override func view() -> UIView { MKMapView() } }
Note
The target properties of a view must be visible for Objective-C.
Now to actually disable zooming, we set the property in
js
<MapView style={{flex: 1}} zoomEnabled={false} />
For more complex properties you can pass json from JavaScript directly to native properties of your view (if they are implemented) or use isCustom argument to inform React Native that a custom setter is implemented on your view manager:
swift
@ReactView
class MapView: RCTViewManager {
@ReactProperty var zoomEnabled: Bool? @ReactProperty(isCustom: true) var region: [String : Double]? @objc func setregion( json: [String : Double]?, forView: MKMapView?, withDefaultView: MKMapView?) { guard let latitude = json?["latitude"], let latitudeDelta = json?["latitudeDelta"], let longitude = json?["longitude"], let longitudeDelta = json?["longitudeDelta"] else { return } let region = MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: latitude, longitude: longitude), span: MKCoordinateSpan(latitudeDelta: latitudeDelta, longitudeDelta: longitudeDelta) ) forView?.setRegion(region, animated: true) } override func view() -> UIView { MKMapView() } }
Note
The custom setter must have the following signature: @objc func setName( value: Type, forView: ViewType?, withDefaultView: ViewType?)
JavaScript code with region property:
js
<MapView
style={{flex: 1}}
zoomEnabled={false}
region={{
latitude: 37.48,
longitude: -122.1,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
}}
/>
Events
To deal with events from the user like changing the visible region we can map input event handlers from JavaScript to native view properties with RCTBubblingEventBlock type.
Lets add new onRegionChange property to a subclass of MKMapView:
swift
class NativeMapView: MKMapView {
@objc var onRegionChange: RCTBubblingEventBlock?
}
@ReactView class MapView: RCTViewManager { @ReactProperty var onRegionChange: RCTBubblingEventBlock? override func view() -> UIView { let mapView = NativeMapView() mapView.delegate = self return mapView } }
extension MapView: MKMapViewDelegate { func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { guard let mapView = mapView as? NativeMapView else { return } let region = mapView.region mapView.onRegionChange?([ "latitude": region.center.latitude, "longitude": region.center.longitude, "latitudeDelta": region.span.latitudeDelta, "longitudeDelta": region.span.longitudeDelta, ]) } }
Note
All properties withRCTBubblingEventBlockmust be prefixed withonand marked with@objc.
Calling the onRegionChange event handler property results in calling the same callback property in
js
function App(): JSX.Element {
...
this.onRegionChange = event => { const region = event.nativeEvent; console.log(region.latitude) };
return ( <MapView style={{flex: 1}} onRegionChange={this.onRegionChange} /> ); }
For more details about Native UI Components, see: https://reactnative.dev/docs/native-components-ios.
Documentation
@ReactModule
The macro exports and registers a class as a native module for React Native.
swift
@ReactModule(jsName: String? = nil, requiresMainQueueSetup: Bool = false, methodQueue: DispatchQueue? = nil)
Parameters
- jsName: JavaScript module name. If omitted, the JavaScript module name will match the class name.
- requiresMainQueueSetup: Let React Native know if your module needs to be initialized on the main queue, before any JavaScript code executes. If value is
falsean class initializer will be called on a global queue. Defaults tofalse. - methodQueue: The queue that will be used to call all exported methods. By default exported methods will call on a global queue.
@ReactMethod
The macro exposes a method of a native module to JavaScript.
swift
@ReactMethod(jsName: String? = nil, isSync: Bool = false)
Parameters
- jsName: JavaScript method name. If omitted, the JavaScript method name will match the method name.
- isSync: Calling the method asynchronously or synchronously. If value is
truethe method is called from JavaScript synchronously on the JavaScript thread. Defaults tofalse.
Note
If you choose to use a method synchronously, your app can no longer use the Google Chrome debugger. This is because synchronous methods require the JS VM to share memory with the app. For the Google Chrome debugger, React Native runs inside the JS VM in Google Chrome, and communicates asynchronously with the mobile devices via WebSockets.
@ReactView
The macro exports and registers a class as a native UI component for React Native.
swift
@ReactView(jsName: String? = nil)
Parameters
- jsName: JavaScript UI component name. If omitted, the JavaScript UI component name will match the class name.
@ReactProperty
The macro exports a property of a native view to JavaScript.
swift
@ReactProperty(keyPath: String? = nil, isCustom: Bool = false)
Parameters
- keyPath: An arbitrary key path in the view to set a value.
- isCustom: Handling a property with a custom setter
@objc func setName( value: Type, forView: ViewType?, withDefaultView: ViewType?)on a native UI component. Defaults tofalse.
Requirements
- Xcode 15 or later.
- Swift 5.9 or later.
- React Native 0.60 or later.
Installation
XCode
- Select
File > Add Package Dependencies.... (Note: The menu options may vary depending on the version of Xcode being used.) - Enter the URL for the the package repository:
https://github.com/ikhvorost/ReactBridge.git - Input a specific version or a range of versions for
Dependency Ruleand a need target forAdd to Project. - Import the package in your source files:
import React
import ReactBridge
...
Swift Package
For a swift package you can add ReactBridge directly to your dependencies in your Package.swift file:
let package = Package(
...
dependencies: [
.package(url: "https://github.com/ikhvorost/ReactBridge.git", from: "1.0.0")
],
targets: [
.target(name: "YourPackage",
dependencies: [
.product(name: "ReactBridge", package: "ReactBridge")
]
),
...
...
)
Licensing
This project is licensed under the MIT License. See LICENSE for more information.
