ikhvorost / ReactBridge

Swift Macros for React Native
MIT License
38 stars 4 forks source link
javascript react-native swift swift-macros swift-package-manager xcode

ReactBridge

Swift 6.0, 5.10, 5.9 Platforms: iOS, macOS, tvOS, visionOS, watchOS Swift Package Manager: compatible React Native: 0.60 Build codecov Swift Doc Coverage

Donate

ReactBridge provides Swift macros for React Native to expose Native Modules and UI Components to JavaScript.

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 JavaScript:

import React
import ReactBridge

@ReactModule
class CalendarModule: NSObject, RCTBridgeModule {
}

Note ReactBridge requires to import React library. Swift class must be inherited from NSObject and must conform to RCTBridgeModule protocol.

The @ReactModule macro also takes optional jsName argument that specifies the name that will be accessible as in your JavaScript code:

@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:

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:

@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():

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:

@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:

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:

@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:

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:

@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:

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.

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:

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:

@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 JavaScript:

<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:

@ReactView
class MapView: RCTViewManager {

  @ReactProperty
  var zoomEnabled: Bool?

  @ReactProperty(isCustom: true)
  var region: [String : Double]?

  @objc 
  func set_region(_ 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 set_Name(_ value: Type, forView: ViewType?, withDefaultView: ViewType?)

JavaScript code with region property:

<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:

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 with RCTBubblingEventBlock must be prefixed with on and marked with @objc.

Calling the onRegionChange event handler property results in calling the same callback property in JavaScript:

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.

@ReactModule(jsName: String? = nil, requiresMainQueueSetup: Bool = false, methodQueue: DispatchQueue? = nil)

Parameters

@ReactMethod

The macro exposes a method of a native module to JavaScript.

@ReactMethod(jsName: String? = nil, isSync: Bool = false)

Parameters

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.

@ReactView(jsName: String? = nil)

Parameters

@ReactProperty

The macro exports a property of a native view to JavaScript.

@ReactProperty(keyPath: String? = nil, isCustom: Bool = false)

Parameters

Requirements

Installation

XCode

  1. Select File > Add Package Dependencies.... (Note: The menu options may vary depending on the version of Xcode being used.)
  2. Enter the URL for the the package repository: https://github.com/ikhvorost/ReactBridge.git
  3. Input a specific version or a range of versions for Dependency Rule and a need target for Add to Project.
  4. 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.

Donate