Integration of SwiftUI and Realm into React Native on a new architecture

Hi all! At the end of spring 2024 new architecture React Native came out in beta version. Although the team React Native While it does not recommend using it in production applications, many libraries have already been adapted to work with it or are on the way to full integration. React Native has always provided the ability to integrate with native code, and the new architecture makes this process even more efficient and flexible.

In this article I want to share my integration experience SwiftUI component using Fabric and databases Realm by using Turbo Modules. I implemented all this using an example iOS– an application that shows a list of popular movies, allows you to add them to your favorites, view the list of favorites and remove them from it.

The application itself is quite extensive, so in this article I will only touch on the key points regarding integration. We will not go into details of the implementation of native components, but will focus on the process of integration with React Native. I will leave a link to the repository with the application at the end of the article.

Terms

  • Fabric – new rendering system in React Nativeyou can read more Here.

  • Turbo Module is the next evolution of native modules in React Nativewhich provides additional benefits. In particular, Turbo Modules use JSI (JavaScript Interface), an interface for native code that allows for more efficient interaction between native and JavaScript code compared to bridge (Bridge).

  • Codegen is a tool used in the new architecture React Native for automatic code generation based on defined using TypeScript/Flow interfaces.

So, let's start with an overview of the main functionality of the application and places where native modules are used.

Application functionality

Demo: https://vimeo.com/1013777959?share=copy

  1. Home screen — we download movies and show them to the user in the form of an endlessly scrolling list. The request is executed on the side JSwe pass the loading status and received data to the component MovieListViewwhich is implemented on SwiftUI.

    When we click on a movie, we can go to a screen with more detailed information about the movie, which is fully implemented natively, but we still request data for this on the side JSand then pass it to the same component. We also use the functionality of the native module on the main screen favorite-movies-storagewhich is responsible for writing and reading to the database Realm. All communication also occurs through JS layer.

  2. Favorite Movie List Screen – this is the most common Flatlistbut we take the data for it from the database Realmusing the same module favorite-movies-storage.

There are several ways to create a native module on a new architecture. We can go and manually write the configuration for Codegenas described Here For Fabric component, or Here For Turbo Module.

We can also use the tool react-native-builder-bobwhich will create for us a primitive component or turbo module, which we can then use as a starting point for implementing our functionality.

I used the latter approach. By using Bob we can create both a local module and a library. In my case it was a local module. To do this, I ran the following command in the project root:

npx create-react-native-library@latest favourite-movies-storage

After this, we need to enter information about the configuration of our library. I created native modules without backward compatibility with the old architecture.

This command must be executed twice: the first time for the component, the second for the turbo module. But I wanted to have all the native functionality related to the movie list in one module, so after creation I made a number of manipulations to combine them into one module.

As a result, I got the following structure:

File structure of the native module

File structure of the native module

Folder Android we ignore.

Main configuration files

"codegenConfig": {
    "name": "RNMovieList",
    "type": "all",
    "jsSrcsDir": "src"
}

Here we describe the name of our module, the folder where our JS code, and module type.
If we want to have Fabric component and Turbo Module in one place, then it should be all.

Integration of native components

Fabric movie list component

First we go to index.tsx. Here we are interested in two lines that export our component and its associated types outside the module:

export {default as MovieListView} from './MovieListViewNativeComponent';
export * from './MovieListViewNativeComponent';

The configuration of our component occurs in the file MovieListViewNativeComponent.ts.

import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
import type {ViewProps} from 'react-native';
import {
  Double,
  WithDefault,
  DirectEventHandler,
  Int32,
} from 'react-native/Libraries/Types/CodegenTypes';

type Movie = {
  readonly id: Int32;
  readonly title: string;
  readonly url: string;
  readonly movieDescription: string;
  readonly rating: Double;
};

type Genre = {
  id: Int32;
  name: string;
};

type MovieDetails = {
  readonly id: Int32;
  readonly title: string;
  readonly posterURL: string;
  readonly overview: string;
  readonly genres: Genre[];
  readonly rating: Double;
  readonly isFavourite: boolean;
};

export type OnMoviePressEventData = {
  readonly movieID: Int32;
};

export type OnMovieAddedToFavorites = OnMoviePressEventData;

export type OnMovieRemovedFromFavorites = OnMovieAddedToFavorites;

export type OnMovieInteractionCallback =
  DirectEventHandler<OnMoviePressEventData>;

type NetworkStatus = WithDefault<'loading' | 'success' | 'error', 'loading'>;

interface NativeProps extends ViewProps {
  readonly movies: Movie[];
  readonly onMoviePress: DirectEventHandler<OnMoviePressEventData>;
  readonly onMovieAddedToFavorites: DirectEventHandler<OnMovieAddedToFavorites>;
  readonly onMovieRemovedFromFavorites: DirectEventHandler<OnMovieRemovedFromFavorites>;
  readonly movieListStatus?: NetworkStatus;
  readonly movieDetailsStatus?: NetworkStatus;
  readonly movieDetails?: MovieDetails;
  readonly onMoreMoviesRequested: DirectEventHandler<null>;
}

export default codegenNativeComponent<NativeProps>('MovieListView');

Here we must describe the props that our component will accept, and based on these types Codegen will generate our native part, which is what happens on the last line of the file.

This is the configuration of our module on the side JS completed, everything turned out to be quite simple. Now let's move on to the native part.

File structure of the native component implementation

File structure of the native component implementation

We are interested in the following files: MovieListView.h, MovieListView.mm, MovieListViewManager.mm.

  • MovieListView.h — a header file that defines the interface for our component. We could add methods here that we can call on the view, but in our case it is empty. Apart from this here we import the header file react_native_movie_list-Swift.hwhich contains interfaces for Swift code available to us in Objective-C.

// This guard prevents this file from being compiled in the old architecture.
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTViewComponentView.h>
#import <UIKit/UIKit.h>
#import "react_native_movie_list-Swift.h"

#ifndef MovieListViewNativeComponent_h
#define MovieListViewNativeComponent_h

NS_ASSUME_NONNULL_BEGIN

@interface MovieListView : RCTViewComponentView
@end

NS_ASSUME_NONNULL_END

#endif /* MovieListViewNativeComponent_h */
#endif /* RCT_NEW_ARCH_ENABLED */
  • MovieListViewManager.mm – this is the manager of our component, React Native uses it at runtime to register a module available in JS. The most important thing here is the method call RCT_EXPORT_MODULEwhich our module registers.

#import <React/RCTViewManager.h>
#import <React/RCTUIManager.h>
#import "RCTBridge.h"

@interface MovieListViewManager : RCTViewManager
@end

@implementation MovieListViewManager

RCT_EXPORT_MODULE(MovieListView)

@end
  • MovieListView.mm — the implementation file of our component; this is where the main work on creating the component takes place. The file itself is quite large and contains a lot of auxiliary code, so I will only touch on the main methods that are responsible for integration.

#import "MovieListView.h"
#import <react/renderer/components/RNMovieList/ComponentDescriptors.h>
#import <react/renderer/components/RNMovieList/EventEmitters.h>
#import <react/renderer/components/RNMovieList/Props.h>
#import <react/renderer/components/RNMovieList/RCTComponentViewHelpers.h>
#import "RCTFabricComponentsPlugins.h"
#import "React/RCTConversions.h"

using namespace facebook::react;

@interface MovieListView () <RCTMovieListViewViewProtocol>
@end

@implementation MovieListView {
    MovieListViewController *_movieListViewController;
    UIView *_view;
}

+ (ComponentDescriptorProvider)componentDescriptorProvider {
    return concreteComponentDescriptorProvider<MovieListViewComponentDescriptor>();
}

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        static const auto defaultProps = std::make_shared<const MovieListViewProps>();
        _props = defaultProps;

        _movieListViewController = [MovieListViewController createViewController];
    }
    return self;
}

- (void)didMoveToWindow {
    [super didMoveToWindow];
    if (self.window) {
        [self setupView];
    }
}

- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps {
    const auto &oldViewProps = *std::static_pointer_cast<MovieListViewProps const>(_props);
    const auto &newViewProps = *std::static_pointer_cast<MovieListViewProps const>(props);

    [self updateMovieListAndStatusIfNeeded:oldViewProps newProps:newViewProps];
    [self updateMovieDetailsStatusAndMovieDetilsIfNeeded:oldViewProps newProps:newViewProps];
    [self setupEventHandlers];

    [super updateProps:props oldProps:oldProps];
}

Class<RCTComponentViewProtocol> MovieListViewCls(void) {
    return MovieListView.class;
}

First, note that the component must implement the protocol RCTMovieListViewViewProtocolwhich was generated using Codegen.

(ComponentDescriptorProvider)componentDescriptorProvider – the method that is used Fabric to obtain the handle needed to instantiate our component.

It's also worth paying attention to the instance variable declaration:

@implementation MovieListView {
    MovieListViewController *_movieListViewController;
}

This controller is responsible for creating, interacting and exchanging data with our SwiftUI view.

initWithFrame – method declared in the interface UIViewwhich is the base class for all view components in UIKit. It initializes a new instance UIView with the specified size and position (passed in the parameter CGRect frame. In our case, in the method initWithFrame not only the view is initialized with the given dimensions, but also the creation MovieListViewControllerwhich controls SwiftUI component. In addition, here we create and set default props.

didMoveToWindow is a life cycle method UIView. It is called when our view is added to the hierarchy attached to the window, when it is removed from it, and when it is moved to another window. When the view is deleted, then self.window will be equal nil. This method also calls setupViewwhich in turn sets constraints for the view containing our SwiftUI component. It is also important for us to add *_movieListViewController* into the hierarchy of view controllers, since from our SwiftUI component, we can go to a new screen in the form of a modal window (Sheet), where you can see more details about the selected movie.

updateProps is a method that is called Fabric every time in JavaScript any of the props changes. This method ensures state synchronization between JavaScript and native code, passing updated property values ​​to the native part of the component. Here the passed parameters are cast to the desired type corresponding to the expected props of the component (in our case this is MovieListViewProps). These settings are then used to update the native component as needed. It is important to note that the superclass method [super updateProps] must be called at the very end of the method updateProps. If this call is made earlier or not done at all, the structures props And oldProps will contain the same values, making it impossible to compare old and new property values. In addition, the method is called here setupEventHandlerswhich is responsible for creating callbacks that are later passed to SwiftUI component.

Let's look at one of them:

- (void)onMovieAddedToFavorites:(NSInteger)movieId {
    if (_eventEmitter != nullptr) {
        auto emitter = std::dynamic_pointer_cast<const facebook::react::MovieListViewEventEmitter>(_eventEmitter);
        if (emitter) {
            emitter->onMovieAddedToFavorites(facebook::react::MovieListViewEventEmitter::OnMovieAddedToFavorites{static_cast<int>(movieId)});
        }
    }
}

This is where the event is sent to JSwhen a movie is added to favorites. First it checks if the object exists _eventEmitterwhich is responsible for sending events. Then the cast is performed _eventEmitter to type MovieListViewEventEmitterwhich was generated Codegen based on the types we talked about at the beginning of the section. If the cast is successful, an event is created with the ID of the added movie and sent to React Native via method call onMovieAddedToFavorites. If everything went well, the required callback will be called on the side JS.

MovieListViewCls is a static method used to get the correct instance of the class MovieListView at runtime, which allows React Native correctly identify and render this native component.

These were the main points regarding integration Fabric component. We will not consider the implementation of the native component itself.

Realm database module

Next, we will consider the main points of integration Turbo Module using our database module as an example.

Here everything also begins with a description of our module on TypeScriptto Codegen was able to generate native interfaces for us.

import type {TurboModule} from 'react-native';
import {TurboModuleRegistry} from 'react-native';

export interface FavouriteMovie {
  id: number;
  url: string;
  title: string;
  rating: string;
}

export interface Spec extends TurboModule {
  getFavouriteMovies(): FavouriteMovie[];
  addFavouriteMovie(movie: FavouriteMovie): Promise<FavouriteMovie[]>;
  removeFavouriteMovie(movieId: number): Promise<FavouriteMovie[]>;
  removeAllFavouriteMovies(): Promise<void>;
}

export default TurboModuleRegistry.getEnforcing<Spec>('FavouriteMoviesStorage');

First we must create an interface for our module, which must inherit from interface TurboModule and be called Spec. Here we describe 4 methods that we want to implement. It is noteworthy that the method getFavouriteMovies is synchronous. This was possible in the old architecture, but it had its drawbacks and was not recommended for use.

At the end we call

TurboModuleRegistry.getEnforcing<Spec>('FavouriteMoviesStorage')

We do this in order to get the native FavoriteMoviesStorage module, if available. And this completes the module specification.

Next, let's look at what's happening in the native part.

File structure of the native implementation of the database module

File structure of the native implementation of the database module

Here we are interested in two key files for integration: FavoriteMoviesStorage.h And FavoriteMoviesStorage.mm.

  • FavouriteMoviesStorage.h – here we declare an interface that inherits from NSObject and implements the protocol NativeFavouriteMoviesStorageSpecgenerated for us using Codegen. If the new architecture is not enabled, this code will not compile.

#ifdef RCT_NEW_ARCH_ENABLED
#import "RNMovieList/RNMovieList.h"
#import "react_native_movie_list-Swift.h"

@interface FavouriteMoviesStorage : NSObject <NativeFavouriteMoviesStorageSpec>

@end
#endif
#import "FavouriteMoviesStorage.h"
#import "react_native_movie_list-Swift.h"

@implementation FavouriteMoviesStorage
RCT_EXPORT_MODULE()

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
    return std::make_shared<facebook::react::NativeFavouriteMoviesStorageSpecJSI>(params);
}
......
@end

The most important thing here is the challenge RCT_EXPORT_MODULE()which makes our module available on the side JS.

Method getTurboModule receives a copy of our Turbo Moduleso that its methods can be called from outside JS. This method is defined and required in the file FavoriteMoviesStorageSpec.hwhich was generated earlier using Codegen.

Next, let's look at examples of implementing methods for working with a database.

- (NSArray<NSDictionary *> *)getFavouriteMovies {
    NSArray *movies = [[FavouriteMoviesManager shared] fetchAllFavouriteMoviesAsList];
    NSMutableArray *result = [NSMutableArray array];
    for (IntermediateFavouriteMovie *movie in movies) {
        [result addObject:[self dictionaryFromFavouriteMovie:movie]];
    }
    return result;
}

This is a synchronous method. It doesn't accept anything and returns us an array of movies. It calls the method fetchAllFavouriteMoviesAsListafter which it converts the data into the expected format and returns it. Implementation FavoriteMoviesManager we will not consider, but there is nothing remarkable there, just an appeal to Realm and getting a list of movies.

Now let's look at the method to remove all movies from favorites − removeAllFavouriteMovies.

- (void)removeAllFavouriteMovies:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    [[FavouriteMoviesManager shared] removeAllFavouriteMoviesOnSuccess:^{
        resolve(@YES);
    } onError:^(NSError * _Nonnull error) {
        reject(@"remove_all_favourite_movies_error", error.localizedDescription, error);
    }];
}

This method takes two parameters: resolve And rejectsince in the specification we indicated that this method returns Promise. When called removeAllFavouriteMoviesit transmits two blocks − onSuccess And onError – into method removeAllFavouriteMoviesOnSuccess class FavoriteMoviesManager. If the operation to delete all selected movies is successful, the block is called onSuccesswhich activates resolve with parameter @YEScompleting the promise successfully. If an error occurs, the block is called onErrorwhich activates reject with a description of the error that will resolve a promise with an error.

The remaining methods work on a similar principle, so there is no point in considering them in detail, since we will not see anything new there.

These are the main points regarding the integration of a native database module using Turbo Modules in new architecture React Native. As I mentioned earlier, we will not go into details of the native implementation, since the main purpose of the article is to show the integration process.

Results

Using a new architecture to implement native modules, as shown in this application, is a completely doable task. Of course, it will take some time to get used to the syntax C++understand the nuances of assembly and operating features, especially if you have no experience with this language. However, these efforts are justified. The new architecture offers many advantages, especially when transferring large amounts of data between JavaScript and native code. With turbo modules we can use synchronous methods, for example, to access data. In addition, the new architecture allows efficient use of native UI– components. For example, in my case, the list of films on SwiftUI worked much better out of the box than FlatListbuilt in RN. Even though my implementation is far from optimal, since there is quite a lot of copying and creation of new objects in order to convert the data to work with SwiftUI. We could use UIKit And UITableViewwhich could solve some of the problems. But this is beyond the scope of this article.

That's all I wanted to share. I hope this article was useful to you. Thank you for your attention!

Repository link: https://github.com/tikhonNikita/movieApp

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *