Rupesh Chaudhari Rupesh is a programming enthusiast who is currently working with React Native to build beautiful hybrid mobile apps. He likes to solve competitive programming puzzles and enjoys gaming in his free time.

Build native UI components in React Native

13 min read 3774

React Native Logo

In this article, we will learn what native UI components are and how to build and use them in React Native.

Native UI components are the way to go if you want to convert your existing native Java or Objective-C component to a React Native component while reusing your native code logic.

Ready? Let’s get started.

What is React Native?

React Native is a JavaScript framework created and maintained by Facebook used for developing natively rendered mobile applications. It is based on React, a JavaScript library for building user interfaces for web browsers.

React Native is not just limited to mobile apps. We can also develop macOS and Windows desktop applications with React Native, with the help of third-party libraries like react-native-windows.

What are Native UI components in React Native?

Under the hood, React Native uses native components like UIView and UIImageView in iOS and exports them to the JavaScript layer. We use these components in our daily development process.

For example, when we use the View component in JSX, we are consuming UIView from iOS in our JavaScript code. Similarly, for the Image component, we use the UIImage component from iOS, so that all the components we use in React Native have a native declaration.

React Native already wraps all the major and widely used UI components, such as ScrollView, Button, and Switch.

But what about components that are not already wrapped, or, if we want to reuse a custom native component that we’ve built for a native app? Well, we can wrap those components to create our native UI component.

Setting up the project in React Native

In this article, we’ll create a native UI component called CameraView that renders a live camera feed from the device camera to the UI. It will also capture images from the camera and save them to a user’s device. You can see the full repo here.

We made a custom demo for .
No really. Click here to check it out.

We will be creating a UI similar to the given below:

iOS UI
iOS UI
Android UI
Android UI

If you are here just to see the code then you can check it out here.

First, we need to create a bare React Native project. Open your terminal and change the current working directory to your preferred folder. There, type this command to create a new React Native project:

react-native init <your-project-name>

Or, if you want to create a project with a specific version of React Native, you can do so by adding a version flag with the version in command:

react-native init <your-project-name> --version 0.63.0

Now, go into the directory of the new project we just created so we can install pods for the iOS application:

cd ios
pod install

Now, let’s run the app!

We will run our app through XCode and Android studio. Go to the iOS directory of your project in finder with the below step:

Reveal Finder

Inside the iOS directory, you will see a <your-project-name>.xc workspace file. Open it.

Workspace File

If you have connected your iOS device correctly to your system, you should see your device name in the XCode window. To run the app on your device, select your device and click the play button to build and run the app.

After the build is successful, you will see some boilerplate UI in your application. Now, we need to run the application on an Android device.

Open your Android project in Android studio follow these steps:

  • Open Android Studio
  • Click Go to File, then Open
    Open
  • In the Android folder of your project, click Open
    Android Folder
  • Select your device at the top of the Android studio window and click play to start the build
    Play to Start

Once the app runs, you’ll see a UI similar to the one in iOS, which is the default boilerplate code for React Native projects.

We’ve now successfully run our app on both iOS and Android.

Creating common native UI for both Android and iOS

To create a common native UI component for both Android and iOS, we need to modify two files from JavaScript:

  • App.js
  • CameraView.js

First, remove the boilerplate JS and JSX code in App.js.

import React from 'react';
import { View, StyleSheet } from 'react-native';

const App = () => {
  return (
    <View style={styles.container} />
  );
};

const styles = StyleSheet.create({
  flex: 1,
  backgroundColor: 'white'
});

export default App;

For now, this is the only code that we need to change. Let’s write from Java code to create the CameraView component for Android.

Creating an Android native UI component

According to React Native’s official documentation, there are five steps to creating an Android UI component:

  • Create the ViewManager subclass
  • Implement the createViewInstance method
  • Expose view property setters using @ReactProp (or @ReactPropGroup) annotation. Note: we will not be using these
  • Register the manager in createViewManagers of the applications package
  • Implement the JavaScript module

Seems easy, right?

Now, switch to the Android Studio window, where we’ll create a Java class that extends the ViewManger subclass. In our case, we’ll use the SimpleViewManager class with a type of TextureView, which we will use to render our camera feed.

Click File, New, then Java Class in the sub-menu:

Java Class

You will then be prompted to fill in the class name of the new Java class.

Class Name

Type in RNCSTMCameraManager inside the file name and press Enter. Now that we have a fresh Java class, we need to extend it with the SimpleViewManager class.

We’ll create a view from it to use it in React Native, and we’ll need to give it a name in the getName() method so we can access this view from JavaScript.

Here’s what your RNCSTMCameraManager.java file will look like:

/** All the imports will be here */

public class RNCSTMCameraManager extends SimpleViewManager<TextureView> {

    public static final String REACT_CLASS = "RNCSTMCamera";
    public static final String ComponentTag = "RNCSTMCamera";
    ReactApplicationContext mCallerContext;
    TextureView textureView;

    public RNCSTMCameraManager(ReactApplicationContext reactContext) {
        mCallerContext = reactContext;
        textureView = new TextureView(reactContext);
    }

    @Override
    public String getName() {
        return REACT_CLASS;
    }
}

We’ve added four global variables:

  1. REACT_CLASS: the name with which our component will be accessed in JS code. In this case, we have named it RNCSTMCamera
  2. ComponentTag: A tag name used to print in logs to easily track and debug our code
  3. mCallerContext: In Android, a context is used to access various underlying native features. To access these features, we need a context called ReactApplicationContext
  4. textureView: The view which we will return to JavaScript, and this is the view where our live camera feed will be displayed

We now need to implement the createViewInstance method, which is responsible for returning a View that we’ll display on the UI.

@Override
public TextureView createViewInstance(ThemedReactContext context) {
    return textureView;
}

Our native UI component will render a TextureView, but we have not added any properties or styles to it, so it will appear as a blank white screen.

Previewing the live feed in TextureView

Now, we need to access the device camera and return the live feed in TextureView, as well as add the image capture feature. But before proceeding, we need to make some changes to our AndroidManifest.xml file. We need permissions to:

  • Access the camera
  • Write and read from device storage
  • Record device audio for videos
  • Write to external storage on devices that do not have external memory cards, we need to access their LegacyExternalStorage files

Let’s add these permissions inside the manifest tag of AndroidManifest.xml.

<uses-permission Android:name="Android.permission.CAMERA" />
<uses-feature Android:name="Android.hardware.camera2.full" />
<uses-permission Android:name="Android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission Android:name="Android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission Android:name="Android.permission.RECORD_AUDIO" />

Then add this property to the application tag:

Android:requestLegacyExternalStorage="true"

Our AndroidManifest.xml file should look like this now.

Androidmanifest File

After we’ve implemented these changes, we need to write the logic to access the device camera and return its feed in TextureView to be able to capture an image and save it.

The file will look like this:

/** All the imports will go here */

public class RNCSTMCameraManager extends SimpleViewManager<TextureView> {

    public static final String REACT_CLASS = "RNCSTMCamera";
    public static final String ComponentTag = "RNCSTMCamera";
    public final int CAPTURE_COMMAND = 1111;
    ReactApplicationContext mCallerContext;
    TextureView textureView;

    public String cameraId;
    CameraDevice cameraDevice;
    CameraCaptureSession cameraCaptureSession;
    CaptureRequest captureRequest;
    CaptureRequest.Builder captureRequestBuilder;

    private Size imageDimensions;
    private ImageReader imageReader;
    private File file;
    Handler mBackgroundHandler;
    HandlerThread mBackgroundThread;

    TextureView.SurfaceTextureListener textureListener = new TextureView.SurfaceTextureListener() {
        @Override
        public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height) {
            try {
                openCamera();
            } catch (CameraAccessException e) {
                Log.d(ComponentTag, "Error in onSurfaceTextureAvailable:"+e);
            }
        }

        @Override
        public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int width, int height) {

        }

        @Override
        public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) {
            return false;
        }

        @Override
        public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) {

        }
    };

    public RNCSTMCameraManager(ReactApplicationContext reactContext) {
        mCallerContext = reactContext;
        textureView = new TextureView(reactContext);
        if(checkCameraHardware(reactContext))  getCameraPermissions(reactContext);
    }

    @Override
    public String getName() {
        return REACT_CLASS;
    }

    /** Check if this device has a camera */
    private boolean checkCameraHardware(Context context) {
        if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)){
            // this device has a camera
            return true;
        } else {
            // no camera on this device
            return false;
        }
    }

    private void getCameraPermissions(Context context) {
        if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
                == PackageManager.PERMISSION_DENIED){
            ActivityCompat.requestPermissions(mCallerContext.getCurrentActivity(), new String[] {Manifest.permission.CAMERA}, 100);
        }
    }

    private final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(@NonNull CameraDevice camera) {
            cameraDevice = camera;
            try {
                createCameraPreview();
            } catch (CameraAccessException e) {
                Log.d(ComponentTag, "Error in onOpened:"+e);
            }
        }

        @Override
        public void onDisconnected(@NonNull CameraDevice camera) {
            cameraDevice.close();
        }

        @Override
        public void onError(@NonNull CameraDevice camera, int error) {
            cameraDevice.close();
            cameraDevice = null;
        }
    };

    private void createCameraPreview() throws CameraAccessException {
        SurfaceTexture surfaceTexture = textureView.getSurfaceTexture();
        surfaceTexture.setDefaultBufferSize(imageDimensions.getWidth(),imageDimensions.getHeight());
        Surface surface = new Surface(surfaceTexture);
        captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        captureRequestBuilder.addTarget(surface);

        cameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() {
            @Override
            public void onConfigured(@NonNull CameraCaptureSession session) {
                if(cameraDevice == null) return;
                cameraCaptureSession = session;
                try {
                    updatePreview();
                } catch (CameraAccessException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                Log.d(ComponentTag, "onConfigure failed");
            }
        }, null);
    }

    private void updatePreview() throws CameraAccessException {
        if(cameraDevice == null) return;
        captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
        cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(),null, mBackgroundHandler);
    }

    private  void openCamera() throws CameraAccessException {
        Log.d(ComponentTag, "Open Camera was called from onSurface Available");
        CameraManager cameraManager = (CameraManager) mCallerContext.getSystemService(Context.CAMERA_SERVICE);
        cameraId = cameraManager.getCameraIdList()[1];
        CameraCharacteristics cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId);
        StreamConfigurationMap map = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
        imageDimensions = map.getOutputSizes(SurfaceTexture.class)[0];

        if(ActivityCompat.checkSelfPermission(mCallerContext, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED){
            ActivityCompat.requestPermissions(mCallerContext.getCurrentActivity(), new String[] { Manifest.permission.CAMERA }, 101);
        }
        if(ActivityCompat.checkSelfPermission(mCallerContext, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
            ActivityCompat.requestPermissions(mCallerContext.getCurrentActivity(), new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, 101);
        }

        cameraManager.openCamera(cameraId, stateCallback,null);
    }

    @Override
    public TextureView createViewInstance(ThemedReactContext context) {
        textureView.setSurfaceTextureListener(textureListener);
        return textureView;
    }

    private void takePicture() throws CameraAccessException {
        Log.d(ComponentTag, "takePicture is initiated!");
        if(cameraDevice == null) return;
        CameraManager cameraManager = (CameraManager) mCallerContext.getSystemService(Context.CAMERA_SERVICE);
        CameraCharacteristics cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraDevice.getId());
        Size[] jpegSizes = null;
        jpegSizes = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).getOutputSizes(ImageFormat.JPEG);
        int width = 1080;
        int height = 1440;
        if(jpegSizes != null & jpegSizes.length > 0){
            width = jpegSizes[0].getWidth();
            height = jpegSizes[0].getHeight();
        }

        ImageReader imageReader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 1);
        List<Surface> outputSurfaces = new ArrayList<>(2);
        outputSurfaces.add(imageReader.getSurface());
        outputSurfaces.add(new Surface(textureView.getSurfaceTexture()));
        final CaptureRequest.Builder captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
        captureRequestBuilder.addTarget(imageReader.getSurface());
        captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);

        Long timeStampInLong = System.currentTimeMillis();
        String timeStamp = timeStampInLong.toString();

        file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)+"/"+timeStamp+".jpg");
        ImageReader.OnImageAvailableListener readerListener = new ImageReader.OnImageAvailableListener() {
            @Override
            public void onImageAvailable(ImageReader reader) {
                Image image = null;
                image = reader.acquireLatestImage();
                ByteBuffer byteBuffer = image.getPlanes()[0].getBuffer();
                byte[] bytes = new byte[byteBuffer.capacity()];
                byteBuffer.get(bytes);
                try {
                    saveImageBytes(bytes);
                } catch (IOException e) {
                    e.printStackTrace();
                }finally {
                    if(image != null) image.close();
                }
            }
        };
        imageReader.setOnImageAvailableListener(readerListner, mBackgroundHandler);

        final  CameraCaptureSession.CaptureCallback captureCallback = new CameraCaptureSession.CaptureCallback() {
            @Override
            public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
                super.onCaptureCompleted(session, request, result);
                Log.d(ComponentTag, "Image is saved!");
                new AlertDialog.Builder(mCallerContext.getCurrentActivity())
                        .setTitle("Photo Saved!")
                        .setMessage("The photo has been successfully saved to your Photos Gallery.")
                        .setPositiveButton("👍", null)
                        .setIcon(Android.R.drawable.ic_menu_gallery)
                        .show();
                try {
                    createCameraPreview();
                } catch (CameraAccessException e) {
                    e.printStackTrace();
                }
            }
        };

        cameraDevice.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() {
            @Override
            public void onConfigured(@NonNull CameraCaptureSession session) {
                try {
                    session.capture(captureRequestBuilder.build(), captureCallback, mBackgroundHandler);
                } catch (CameraAccessException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void onConfigureFailed(@NonNull CameraCaptureSession session) {

            }
        }, mBackgroundHandler);
    }

    private void saveImageBytes(byte[] bytes) throws IOException {
        OutputStream outputStream = null;
        outputStream = new FileOutputStream(file);
        outputStream.write(bytes);
        outputStream.close();
    }

    /**
     * Map the "captureImage" command to an integer
     */
    @Nullable
    @Override
    public Map<String, Integer> getCommandsMap() {
        return MapBuilder.of("captureImage", CAPTURE_COMMAND);
    }

    /**
     * Handle "captureImage" command called from JS
     */
    @Override
    public void receiveCommand(@NonNull TextureView root, String commandId, @Nullable ReadableArray args) {
        super.receiveCommand(root, commandId, args);
        int reactNativeViewId = args.getInt(0);
        int commandIdInt = Integer.parseInt(commandId);

        switch (commandIdInt) {
            case CAPTURE_COMMAND:
                try {
                    takePicture();
                } catch (CameraAccessException e) {
                    e.printStackTrace();
                }
                break;
            default: {}
        }
    }
}

You can download the full source code here.

Notice we’ve added boilerplate code to access the camera and also included the getCommandsMap() and receiveCommand() functions. These are the functions used to track if we have received any command in the native UI component.

When they receive a certain command, they execute a particular logic. Think of it similar to Redux, when we receive a TYPE then dispatch an ACTION to execute specific logic.

Here, when we receive the CAPTURE_COMMAND, we call the takePicture() method to capture an image and save it to a user’s photo gallery.

Let’s register our viewManager next. First, create a package file that implements ReactPackage and will return an instance of our RNCSTMCameraManager through its createViewManagers method.

Now, export it to JavaScript code using the getPackages() method of MainApplication.java.

Create an RNCSTMCameraManagerPackage.java file using the same process we used to create the RNCSTMCameraManager.java file. After creating it, add the following code:

/** Imports will be here */

public class RNCSTMCameraManagerPackage implements ReactPackage {

    @NonNull
    @Override
    public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
        List<NativeModule> nativeModules = new ArrayList<>();
        return nativeModules;
    }

    @Override
    public List<ViewManager> createViewManagers(
            ReactApplicationContext reactContext) {
        return Arrays.<ViewManager>asList(
                new RNCSTMCameraManager(reactContext)
        );
    }
}

Now, add the package to the getPackages() method of MainApplication.java:

@Override
protected List<ReactPackage> getPackages() {
  @SuppressWarnings("UnnecessaryLocalVariable")
  List<ReactPackage> packages = new PackageList(this).getPackages();
  // Packages that cannot be autolinked yet can be added manually here, for example:
   packages.add(new RNCSTMCameraManagerPackage()); // Add this line to add our package
  return packages;
}

🎉 That’s it! We’ve successfully created an Android native UI component.

Let’s access it in our JavaScript code. Go to VSCode, open App.js, and add this code:

import React, { useRef } from 'react';
import {
  findNodeHandle,
  Platform,
  Pressable,
  StyleSheet,
  UIManager,
  View
} from 'react-native';
import { CameraView } from './CameraView';

const App = () => {
  const componentRef = useRef(null);
  const dispatchCaptureCommand = () => {
        UIManager?.dispatchViewManagerCommand(
          findNodeHandle(componentRef?.current),
          UIManager?.RNCSTMCamera?.Commands?.captureImage?.toString(),
          [findNodeHandle(componentRef?.current)],
        )
  };
  return (
    <View style={styles.container}>
      <CameraView ref={componentRef} style={styles.cameraView} />
      <View style={styles.captureBtnContainer}>
        <Pressable style={styles.captureBtn} onPress={dispatchCaptureCommand} />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'white',
  },
  cameraView: {
    flex: 1,
  },
  captureBtnContainer: {
    backgroundColor: '#0006',
    position: 'absolute',
    bottom: 60,
    alignSelf: 'center',
    alignItems: 'center',
    justifyContent: 'center',
    height: 100,
    width: 100,
    borderRadius: 60,
  },
  captureBtn: {
    height: 75,
    width: 75,
    borderRadius: 37.5,
    backgroundColor: '#fff',
  },
});

export default App;

You can see we have used a CameraView component that we’re importing from CameraView.

Create a new JS file in the same directory and name it CameraView.js, then paste the following:

import {requireNativeComponent} from 'react-native';

const CameraView = requireNativeComponent('RNCSTMCamera');

export {CameraView};

We are accessing our native Android UI component using requireNativeComponent from React Native and exporting it as CameraView.

Our JavaScript side implementation is also finished for Android. You can now test the application by running on your Android device using Android studio.

Creating an iOS native UI component

According to official documentation, we’ll need to complete these steps to create an iOS native UI component:

  • Subclass RCTViewManager to create a manager for the component
  • Add the RCT_EXPORT_MODULE() marker macro
  • Implement the -(UIView *)view method

Similar to Android, we also need to create a manager for our component that will extend the RCTViewManager class. We’ll export it through the RCT_EXPORT_MODULE() marker macro. -(UIView *)view is the method that returns what to display in the UI.

We will create two types of files in iOS for exporting our native UI component to JS code:

  1. A manager class that will export our view and will contain logic related to handling events or commands from JS side
  2. The view that’ll be sent to the manager class to render. This file will contain all the logic related to displaying a live camera feed on the UIView

For both files, we will have two files: an .h (header) file and an .m (implementation) file.

Switch to the XCode window. Click on File in the title bar, then New, then File in the sub-menu.

File Submenu

Set the source type to Header File and click Next in the Choose template dialog.

Choose Template

Save the file as RNCSTMCameraManager and click Create.

Create

Add the below code to RNCSTMCameraManager.h.

#import "AVFoundation/AVFoundation.h"
#import <React/RCTViewManager.h>

@interface RNCSTMCameraManager : RCTViewManager

@end

In this file, we have just declared the class RNCSTMCameraManager, which extends RCTViewManager. Its implementation will be in the .m file.

To create RNCSTMCameraManager.m, follow the same steps that we followed for creating the header file but select the type of source code to Objective-C.

Objective-c

Save as RNCSTMCameraManager, click Next, and then add the app target and click Create.

Add the following code:

#import "RNCSTMCameraManager.h"
#import "RNCTSTMCamera.h"
#import "React/RCTUIManager.h"
#import "React/RCTLog.h"

@implementation RNCSTMCameraManager

RCT_EXPORT_MODULE(RNCSTMCamera)

- (UIView *)view
{
  return [[RNCSTMCamera alloc] initWithFrame:CGRectMake(UIScreen.mainScreen.bounds.origin.x,
    UIScreen.mainScreen.bounds.origin.y,
    UIScreen.mainScreen.bounds.size.width,
    UIScreen.mainScreen.bounds.size.height)];
}

+ (BOOL)requiresMainQueueSetup {
    return YES;
}

@end

We tell JavaScript that we can access this class using RNCSTMCamera in the RCT_EXPORT_MODULE marker macro, and return an instance of the RNCSTMCamera class inside the view method, which returns a UIView.

We’ve also added a requiresMainQueueSetup method, which will run our component on the main thread.

You will also notice that we are returning an RNCSTMCamera class that we haven’t created yet, so let’s do so. Create a .h header file named RNCSTMCamera for the declarations of the RNCSTMCamera class and add the following code to it:

#import "UIKit/UIKit.h"
#import "AVFoundation/AVFoundation.h"

@interface RNCSTMCamera : UIView

@property (strong, nullable) AVCapturePhotoOutput *photoOutput;

@end

We’ve added the photoOutput property of type AVCapturePhotoOutput, which is used to capture photos.

Now, let’s create an implementation of this class. Follow the same steps to create an Objective-C implementation (.m) file and name this file RNCSTMCamera. To access the device camera and return its live feed, add this code:

#import "RNCTSTMCamera.h"
#import "AVFoundation/AVFoundation.h"

@implementation RNCSTMCamera

- (instancetype)initWithFrame:(CGRect)frame
{
  _photoOutput = [[AVCapturePhotoOutput alloc] init];
  self = [super initWithFrame:frame];
  if (self) {
    NSLog(@"RNCSTMCamera was initialized in init with Frame");
    [self initializeCamera];
  }
  return self;
}

- (void) initializeCamera {
  AVCaptureSession *session = [[AVCaptureSession alloc] init];
  session.sessionPreset = AVCaptureSessionPresetHigh;
  CALayer *viewLayer = self.layer;
  AVCaptureVideoPreviewLayer *captureVideoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
  captureVideoPreviewLayer.frame = CGRectMake(UIScreen.mainScreen.bounds.origin.x,UIScreen.mainScreen.bounds.origin.y,UIScreen.mainScreen.bounds.size.width,UIScreen.mainScreen.bounds.size.height);
  [captureVideoPreviewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
  [self.layer addSublayer:captureVideoPreviewLayer];
  AVCaptureDevice *device = nil;
  NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
  for(AVCaptureDevice *camera in devices) {
      if([camera position] == AVCaptureDevicePositionFront) {
          device = camera;
          break;
      }
  }
  NSError *error = nil;
  AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
  if (!input) NSLog(@"ERROR: trying to open camera: %@", error);
  [session addInput:input];
  [session addOutput:_photoOutput];
  [session startRunning];
  [self setClipsToBounds:true];
}

@end

When we call initWithFrame, we call the initializeCamera method, which starts an AVCaptureSession. We then create an AVCaptureVideoPreviewLayer and initialize it with AVCaptureSession.

Once the AVCaptureVideoPreviewLayer is ready, then we add AVCaptureVideoPreviewLayer as a sublayer to our root UIView and set the AVCaptureDevice to the front camera.

Next, we create an AVCaptureDeviceInput object that decides which feed to display in the session, then add photoOutput as an output to the session for capturing the photo from the device camera. Finally, we start the session.

To access the camera and be able to write in a user’s photo library, we need to add the following permissions to the info.plist file:

Permissions

To receive the capture photo command, open RNCSTMCameraManager.m and add the following:

RCT_EXPORT_METHOD(captureImage: (nonnull NSNumber *)viewTag)
{
  [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
    RNCSTMCamera *view = viewRegistry[viewTag];

    if ([view isKindOfClass:[UIView class]]) {
      NSLog(@"Capture Command was called");
      [self capturePhoto:view];
    } else {
      RCTLogError(@"view type must be UIView");
    }
  }];
}

-(void) capturePhoto:(RNCSTMCamera*) view{
  AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettingsWithFormat:@{AVVideoCodecKey: AVVideoCodecTypeJPEG}];
  [view.photoOutput capturePhotoWithSettings:settings delegate:self];
}

#pragma mark - AVCapturePhotoCaptureDelegate
- (void)captureOutput:(AVCapturePhotoOutput *)output didFinishProcessingPhoto:(AVCapturePhoto *)photo error:(nullable NSError *)error
{
  NSData *imageData = [photo fileDataRepresentation];
  UIImage *image = [UIImage imageWithData:imageData];
  NSLog(@"image is: %@",image);
  UIImageWriteToSavedPhotosAlbum(image, NULL, NULL, NULL);
  UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Photo Saved!" message:@"The Photo has been successfully saved in your photo library." delegate:self cancelButtonTitle:NULL otherButtonTitles:@"👍", nil];
  [alert show];
}

Here, we’ve exported captureImage to JS code, so, when called, it will check for type UIView. Then, we call capturePhoto to capture the image from the photoOutput property of the RNCSTMCamera class.

We’ve also added a macro to override AVCapturePhotoCaptureDelegate, which is triggered when we call captureWithSettings. In this method, we are converting AVCapturePhoto to imageData using fileDataRepresentation. Then, we use that data to create an UIImage and save it to the photo library.

Our final step is to send the command to capture the photo for iOS. Open App.js in VSCode and change the code in dispatchCaptureCommand to the following:

const dispatchCaptureCommand = () => {
  Platform.OS === 'Android'
    ? UIManager?.dispatchViewManagerCommand(
        findNodeHandle(componentRef?.current),
        UIManager?.RNCSTMCamera?.Commands?.captureImage?.toString(),
        [findNodeHandle(componentRef?.current)],
      )
    : UIManager.dispatchViewManagerCommand(
        findNodeHandle(componentRef?.current),
        UIManager.getViewManagerConfig('RNCSTMCamera').Commands.captureImage,
        [],
      );
};

You can see we are using different syntax for sending the command in iOS. This is because there is some different logic for underlying view management in iOS.

Conclusion

Building and using native UI components in React Native can be pretty handy if you are looking to implement a feature for your React Native application that is not supported out of the box. It’s also extremely useful if you’re converting your existing native iOS and Android applications to React Native. Thanks for reading!

LogRocket: Instantly recreate issues in your React Native apps.

LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.

LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.

Start proactively monitoring your React Native apps — .

Rupesh Chaudhari Rupesh is a programming enthusiast who is currently working with React Native to build beautiful hybrid mobile apps. He likes to solve competitive programming puzzles and enjoys gaming in his free time.

One Reply to “Build native UI components in React Native”

  1. An unrelated question, how we can implement server driven UI in react native ?

    Busting my head for few days.

Leave a Reply