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
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.
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:
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.
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 callRCT_EXPORT_MODULE
which 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 MovieListViewController
which 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 setupView
which 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 setupEventHandlers
which 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 _eventEmitter
which 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.
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 fetchAllFavouriteMoviesAsList
after 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 reject
since in the specification we indicated that this method returns Promise. When called removeAllFavouriteMovies
it 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 onSuccess
which activates resolve
with parameter @YES
completing the promise successfully. If an error occurs, the block is called onError
which 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