React-Native UI Native Components in Swift and Java

React Native already provides the basic components you would use to build your application: views, labels, buttons, tables, etc.

However, there are times when these components aren't enough. In this tutorial we’ll explain how we build our own native component that can send messages back and forth between Javascript and the iOS/Android code.

Note: Most tutorials focus on how to do this in Objective-C, but we'll be using Swift instead.

We will also be using Xcode 10.2 and Android Studio 3.3.2, running in Mac OS 10.14.4

Building the demo application

Make sure you have the React Native CLI installed:

npm install -g react-native-cli

Next, let's create our project

react-native init AwesomeProject

And now let's run our application

react-native run-ios --simulator="iPhone Xs"

Creating a Native UI Component

 
logo-ios.png
 

Open AwesomeProject/ios/AwesomeProject.xcodeproj in Xcode

Defining our Custom View in iOS

Create a new file: File -> New -> File... -> Cocoa Touch Class

 
Note:  When asked, choose  Create Bridging Header .

Note: When asked, choose Create Bridging Header.

 

Patch for Swift development

Since React-Native was built with ObjectiveC in mind, we need to do some tricks to be able to make it work with Swift.

First let's create the same file, but in ObjectiveC

 
Screen Shot 2019-06-24 at 10.59.38 AM.png
 

And delete the MyCustomView.h since we won't be using it.

Select the file AwesomeProject-Bridging-Header.h and add this content:

#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTComponent.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTViewManager.h>
#import <React/RCTDevLoadingView.h>

Defining our view

Open MyCustomView.swiftand paste the following code to define our view:

class MyCustomView: UIView {
 
  override init(frame: CGRect) {
    super.init(frame: frame)
    setupView()
  }
 
  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setupView()
  }
 
  // in here you can configure your view
  private func setupView() {
 
  }
 
}

Besides this view, we also need an RCTViewManager subclass that will be the one creating and manipulating this view.

Paste this at the bottom of MyCustomView.swift :

@objc (RCTMyCustomViewManager)
class RCTMyCustomViewManager: RCTViewManager {
 
  override static func requiresMainQueueSetup() -> Bool {
    return true
  }
 
  override func view() -> UIView! {
    return MyCustomView()
  }
 
}

Now, the only thing missing is to let ReactNative know about this manager. In order to do this, open MyCustomView.m and replace it's content with the following:

#import <React/RCTViewManager.h>
 
@interface RCT_EXTERN_MODULE(RCTMyCustomViewManager, RCTViewManager)
 
@end

Adding properties to our custom view

Let's bridge over some native properties. For this we'll create a simple prop and a function.

Open MyCustomView.swift and add the following to your CustomView class:

@objc var status = false {
  didSet {
    self.setupView()
  }
}
 
@objc var onClick: RCTBubblingEventBlock?
 
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
  guard let onClick = self.onClick else { return }
 
  let params: [String : Any] = ["value1":"react demo","value2":1]
  onClick(params)
 
}

and replace setupView with the code below:

private func setupView() {
  self.backgroundColor = self.status ? .green : .red
 
  self.isUserInteractionEnabled = true
 
}

Now, to make ReactNative aware of these new properties, open MyCustomView.m and replace it with the following:

@interface RCT_EXTERN_MODULE(RCTMyCustomViewManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(status, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onClick, RCTBubblingEventBlock)
 
@end

 
1718a076e29822051df8bcf8b5ce1124-logo-de-android-by-vexels.png
 

Import AwesomeProject/android/ into Android Studio

Defining our custom view in Android

Create a new file: File -> New -> File... and name it MyCustomView.java

Open MyCustomView.java and paste the following code to define our view:

public class MyCustomView extends RelativeLayout {
 
    public MyCustomView(Context context) {
        super(context);
    }
 
    public MyCustomView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
 
    public MyCustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
 
    public void onFinishInflate() {
        super.onFinishInflate();
    }
 
}

Besides this view, we also need a view manager that will be the one creating and manipulating this view.

Create a file called MyCustomViewManager.java and paste the following :

public class MyCustomViewManager extends SimpleViewManager<MyCustomView> {
 
    public static final String REACT_CLASS = "RCTMyCustomView";
    String eventName = "onClick";
 
    @Override
    public String getName() { return REACT_CLASS; }
 
    @Override
    public MyCustomView createViewInstance(ThemedReactContext context) {
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        MyCustomView view = (MyCustomView)inflater.inflate(R.layout.custom_view, null);
        return view;
    }
 
}

Now, the only thing missing is to let ReactNative know about this manager. To do this, create a file called MyCustomViewReactPackage.java and paste the following:

public class MyCustomViewReactPackage implements ReactPackage {
 
    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Arrays.<ViewManager>asList( new MyCustomViewManager() );
    }
 
    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        return modules;
    }
 
}

In the MainApplication.java file in your project let's add the MyCustomViewReactPackage like this:

public class MainApplication extends Application implements ReactApplication {
    ...
    @Override
    protected List<ReactPackage> getPackages() {
      return Arrays.<ReactPackage>asList(
              new MainReactPackage(),
              new MyCustomViewReactPackage() // new line!!!
      );
    }
 
    @Override
    protected String getJSMainModuleName() {
      return "index";
    }
  };
    …
}

Adding properties to our custom view

Now, let's bridge over some native properties. For this, we'll create the same prop and function like in iOS.

Open MyCustomView.java and add this to your class:

private boolean status = false;
 
public void onFinishInflate() {
    super.onFinishInflate();
 
    this.setOnClickListener(new View.OnClickListener() {
        public void onClick(View v) {
            MyCustomView.this.onClick();
        }
    });
}
 
public void setStatus(boolean status) {
    this.status = status;
    setBackgroundColor( this.status ? Color.GREEN : Color.RED);
}
 
public void onClick() {
    WritableMap event = Arguments.createMap();
 
    event.putString("value1","react demo");
    event.putInt("value2",1);
 
    ReactContext reactContext = (ReactContext)getContext();
    reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(getId(), "onClickEvent", event);
}

Observe how in onClick() we will send an event to Javascript but we still need the connections of the status prop and the onClickEvent to react-native. In order to do this, open the MyCustomViewManager.java and paste the following:

@ReactProp(name = "status")
public void setStatus(MyCustomView view, Boolean status) {
    view.setStatus(status);
}
 
public Map getExportedCustomBubblingEventTypeConstants() {
    String eventName = "onClickEvent";
    String propName = "onClick";
    Map onClickHandler = MapBuilder.of("phasedRegistrationNames",MapBuilder.of("bubbled", propName));
 
    Builder events = MapBuilder.builder();
    events.put(eventName, onClickHandler);
    return events.build();
}

 
javascript_logo.png
 

Defining our custom view in Javascript

Let's create a simple wrapper for our custom view:

// MyCustomView.js
 
import React from 'react';
import { requireNativeComponent } from 'react-native';
 
const RCTCustomView = requireNativeComponent('RCTMyCustomView', MyCustomView, {});
 
export default class MyCustomView extends React.PureComponent {
  _onClick = (event) => {
    if (!this.props.onClick) {
      return;
    }
 
    // process raw event...
    this.props.onClick(event.nativeEvent);
  }
 
  render() {
    return <RCTCustomView {...this.props} onClick={this._onClick}/>
  }
}

Using our custom view

To use it you can do the following:

// App.js
 
...
import MyCustomView from './MyCustomView.js';
 
export default class App extends Component {
  state = {
    status: false
  }
 
  onClick = (event: Object) => {
    alert("Received params: " + JSON.stringify(event))
 
    this.setState({status: !this.state.status})
  }
 
  render() {
    return (
      <View style={styles.container}>
        <MyCustomView 
          status={this.state.status}
          onClick={this.onClick}
          style={{ width: 100, height: 100 }} 
        />
      </View>
    );
  }
}
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  }
 
});

How this works:

In Javascript we map the native views RCTMyCustomView into our MyCustomView component using the requireNativeComponent function. It receives two props: a simple boolean value called status and a function called onClick. The two props are defined in iOS and Android respectively.

Once the onClick event is triggered natively it will call the mapped prop to switch the status props coming into the MyCustomView.

Source Code

The full source code for this project is available in https://github.com/calitb/RN-NativeViews